diff --git a/tasks/check_usernames/core/__init__.py b/tasks/check_usernames/core/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tasks/check_usernames/core/connection/__init__.py b/tasks/check_usernames/core/connection/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tasks/check_usernames/core/connection/base_connection.py b/tasks/check_usernames/core/connection/base_connection.py new file mode 100644 index 00000000..24e8ce5e --- /dev/null +++ b/tasks/check_usernames/core/connection/base_connection.py @@ -0,0 +1,38 @@ +from abc import ABC, abstractmethod + + +class BaseConnection(ABC): + """ + Abstract base class for connection operations with databases and APIs. + This interface provides methods for connecting, disconnecting, and checking the connection status. + + Implementations of this interface can be used with MySQL, SQLite, or other databases, + as well as with API setups. + """ + + @abstractmethod + def connect(self): + """ + Connects to the database or API. + + Raises: + ConnectionError: If the connection fails. + """ + pass + + @abstractmethod + def disconnect(self): + """ + Disconnects from the database or API. + """ + pass + + @abstractmethod + def check(self): + """ + Checks the connection status. + + Returns: + bool: True if the connection is active, False otherwise. + """ + pass diff --git a/tasks/check_usernames/core/connection/mysql_connection.py b/tasks/check_usernames/core/connection/mysql_connection.py new file mode 100644 index 00000000..b1f971fc --- /dev/null +++ b/tasks/check_usernames/core/connection/mysql_connection.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +import pymysql + +from tasks.check_usernames.core.connection.base_connection import BaseConnection + + +class MySQLBaseConnection(BaseConnection): + """ + Implementation of the BaseConnection interface for MySQL databases using the pymysql library. + """ + + def __init__(self, host: str, port: str | int, database: str, db_connect_file: str = None, user: str = None, + password: str = None): + """ + Initializes a MySQLBaseConnection object with the provided connection parameters. + + Parameters: + host (str): The host name or IP address of the MySQL server. + port (int): The port number of the MySQL server. + database (str): The name of the MySQL database to connect to. + db_connect_file (str, optional): The path to the MySQL database connection file. Defaults to None. + user (str, optional): The name of the MySQL user. Defaults to None. + password (str, optional): The password of the MySQL user. Defaults to None. + """ + + self.host = host + self.port = port + self.user = user + self.password = password + self.database = database + self.connection = None + self.db_connect_file = db_connect_file + + def connect(self): + """ + Connects to the MySQL database using the provided parameters. + + Raises: + pymysql.err.OperationalError: If the connection fails. + """ + if self.db_connect_file is not None: + self.connection = pymysql.connect( + host=self.host, + read_default_file=self.db_connect_file, + db=self.database, + charset='utf8mb4', + port=self.port, + cursorclass=pymysql.cursors.DictCursor, + ) + else: + self.connection = pymysql.connect( + host=self.host, + port=self.port, + charset='utf8mb4', + user=self.user, + password=self.password, + db=self.database + ) + + def disconnect(self): + """ + Disconnects from the MySQL database. + """ + if self.connection: + self.connection.close() + self.connection = None + + def check(self): + """ + Checks the status of the MySQL database connection. + + Returns: + bool: True if the connection is open, False otherwise. + """ + if self.connection: + return self.connection.open + return False diff --git a/tasks/check_usernames/core/connection/sqlite_connection.py b/tasks/check_usernames/core/connection/sqlite_connection.py new file mode 100644 index 00000000..cb2856c3 --- /dev/null +++ b/tasks/check_usernames/core/connection/sqlite_connection.py @@ -0,0 +1,47 @@ +import sqlite3 + +from tasks.check_usernames.core.connection.base_connection import BaseConnection + + +class SQLiteBaseConnection(BaseConnection): + """ + Implementation of the BaseConnection interface for SQLite databases using the sqlite3 library. + """ + + def __init__(self, database_file: str): + """ + Initializes a SQLiteBaseConnection object with the path to the SQLite database file. + + Parameters: + database_file (str): The path to the SQLite database file. + """ + self.database_file = database_file + self.connection = None + + def connect(self): + """ + Connects to the SQLite database using the provided database file path. + + Raises: + sqlite3.Error: If the connection fails. + """ + self.connection = sqlite3.connect(self.database_file) + + def disconnect(self): + """ + Disconnects from the SQLite database. + """ + if self.connection: + self.connection.close() + self.connection = None + + def check(self): + """ + Checks the status of the SQLite database connection. + + Returns: + bool: True if the connection is open, False otherwise. + """ + if self.connection: + return True + return False diff --git a/tasks/check_usernames/core/factory/__init__.py b/tasks/check_usernames/core/factory/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tasks/check_usernames/core/factory/factory.py b/tasks/check_usernames/core/factory/factory.py new file mode 100644 index 00000000..018be420 --- /dev/null +++ b/tasks/check_usernames/core/factory/factory.py @@ -0,0 +1,36 @@ +from pywikibot import config as _config + +from tasks.check_usernames.core.connection.mysql_connection import MySQLBaseConnection +from tasks.check_usernames.core.connection.sqlite_connection import SQLiteBaseConnection +from tasks.check_usernames.core.persistence.mysql_persistence import MySQLPersistence +from tasks.check_usernames.core.persistence.sqlite_persistence import SQLitePersistence +from tasks.check_usernames.core.repositories.mysql_repository import MySQLRepository +from tasks.check_usernames.core.repositories.sqlite_repository import SQLiteRepository + + +class DatabaseFactory: + def create_mysql_repository(self) -> MySQLRepository: + connection = MySQLBaseConnection( + host=_config.db_hostname_format.format("arwiki"), + port=_config.db_port, + database=_config.db_name_format.format("arwiki"), + db_connect_file=_config.db_connect_file, + ) + connection.connect() + return MySQLRepository( + persistence=MySQLPersistence( + connection=connection + ) + ) + + def create_sqlite_memory_repository(self) -> SQLiteRepository: + connection = SQLiteBaseConnection( + # database_file="file::memory:?cache=shared", + database_file="/home/lokas/PycharmProjects/pythonProject3/code/tasks/check_usernames/core/demo.db", + ) + connection.connect() + return SQLiteRepository( + persistence=SQLitePersistence( + connection=connection + ) + ) diff --git a/tasks/check_usernames/core/models/__init__.py b/tasks/check_usernames/core/models/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tasks/check_usernames/core/models/user.py b/tasks/check_usernames/core/models/user.py new file mode 100644 index 00000000..20c4e42d --- /dev/null +++ b/tasks/check_usernames/core/models/user.py @@ -0,0 +1,14 @@ +class UserStatus: + STATE_GET_FROM_DB = 0 + STATE_SEND_TO_API = 1 + STATE_ACCEPT_ON_API = 2 + STATE_REJECT_ON_API = 3 + + +class User: + + def __init__(self, id, user_name, created_at, status=UserStatus.STATE_GET_FROM_DB): + self.id = id + self.user_name = user_name + self.created_at = created_at + self.status = status diff --git a/tasks/check_usernames/core/persistence/__init__.py b/tasks/check_usernames/core/persistence/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tasks/check_usernames/core/persistence/base_persistence.py b/tasks/check_usernames/core/persistence/base_persistence.py new file mode 100644 index 00000000..7449f689 --- /dev/null +++ b/tasks/check_usernames/core/persistence/base_persistence.py @@ -0,0 +1,70 @@ +from abc import ABC, abstractmethod + + +class BasePersistence(ABC): + + @abstractmethod + def execute(self, query: str, params=None): + """ + Executes a custom SQL query. + + :param query: The SQL query to execute. + + :type query: str + + :param params: Optional parameters to be substituted in the query. + + :type params: Union[None, tuple] + + """ + pass + + @abstractmethod + def select(self, query: str, params=None): + """ + Executes a SELECT query and returns the result as a list of rows. + + Parameters: + query (str): The SQL query string to execute. + params (tuple or dict, optional): Parameters for the query (default: None). + + Returns: + list: A list of rows returned by the query. + """ + pass + + @abstractmethod + def select_one(self, query: str, params=None): + """ + Executes a SELECT query and returns a single row. + + Parameters: + query (str): The SQL query string to execute. + params (tuple or dict, optional): Parameters for the query (default: None). + + Returns: + tuple or None: A single row returned by the query or None if no rows are found. + """ + pass + + @abstractmethod + def delete(self, query: str, params=None): + """ + Executes a DELETE query. + + Parameters: + query (str): The SQL query string to execute. + params (tuple or dict, optional): Parameters for the query (default: None). + """ + pass + + @abstractmethod + def update(self, query: str, params=None): + """ + Executes an UPDATE query. + + Parameters: + query (str): The SQL query string to execute. + params (tuple or dict, optional): Parameters for the query (default: None). + """ + pass diff --git a/tasks/check_usernames/core/persistence/mysql_persistence.py b/tasks/check_usernames/core/persistence/mysql_persistence.py new file mode 100644 index 00000000..d61ec970 --- /dev/null +++ b/tasks/check_usernames/core/persistence/mysql_persistence.py @@ -0,0 +1,87 @@ +from tasks.check_usernames.core.connection.base_connection import BaseConnection +from tasks.check_usernames.core.persistence.base_persistence import BasePersistence + + +class MySQLPersistence(BasePersistence): + def __init__(self, connection: BaseConnection): + """ + Initializes a new instance of the MySQLPersistence class. + + :param connection: The connection object to use for database operations. + :type connection: BaseConnection + """ + self.connection = connection + + def delete(self, query, params=None): + """ + Deletes data from the database based on the given query and parameters. + + :param query: The SQL query used to delete data from the database. + :type query: str + :param params: Optional parameters to be substituted in the query. + :type params: Union[None, tuple] + """ + with self.connection.connection.cursor() as cursor: + cursor.execute(query, params) + self.connection.connection.commit() + + def execute(self, query, params=None): + """ + Executes a custom SQL query. + + :param query: The SQL query to execute. + :type query: str + :param params: Optional parameters to be substituted in the query. + :type params: Union[None, tuple] + """ + with self.connection.connection.cursor() as cursor: + cursor.execute(query, params) + self.connection.connection.commit() + + def update(self, query, params=None): + """ + Updates data in the database based on the given query and parameters. + + :param query: The SQL query used to update data in the database. + :type query: str + :param params: Optional parameters to be substituted in the query. + :type params: Union[None, tuple] + """ + with self.connection.connection.cursor() as cursor: + cursor.execute(query, params) + self.connection.connection.commit() + + def select(self, query, params=None): + """ + Executes a SELECT query and returns the results as a list of rows. + + :param query: The SQL query to execute. + :type query: str + :param params: Optional parameters to be substituted in the query. + :type params: Union[None, tuple] + + :return: A list of rows returned by the query. + :rtype: list + """ + with self.connection.connection.cursor() as cursor: + print(query, params) + cursor.execute(query, params) + result = cursor.fetchall() + return result + + def select_one(self, query, params=None): + """ + Executes a SELECT query and returns a single row. + + :param query: The SQL query to execute. + :type query: str + :param params: Optional parameters to be substituted in the query. + :type params: Union[None, tuple] + + :return: A single row returned by the query, or None if no rows are found. + :rtype: Union[tuple, None] + """ + with self.connection.connection.cursor() as cursor: + cursor.execute(query, params) + result = cursor.fetchone() + return result diff --git a/tasks/check_usernames/core/persistence/sqlite_persistence.py b/tasks/check_usernames/core/persistence/sqlite_persistence.py new file mode 100644 index 00000000..70eab18a --- /dev/null +++ b/tasks/check_usernames/core/persistence/sqlite_persistence.py @@ -0,0 +1,131 @@ +from tasks.check_usernames.core.connection.base_connection import BaseConnection +from tasks.check_usernames.core.persistence.base_persistence import BasePersistence + + +class SQLitePersistence(BasePersistence): + def __init__(self, connection: BaseConnection): + """ + Initializes a new instance of the SQLitePersistence class. + + :param connection: The SQLiteConnection object to use for database operations. + :type connection: BaseConnection + """ + self.connection = connection + + def delete(self, query: str, params=None): + """ + Deletes data from the SQLite database based on the given query and parameters. + + :param query: The SQL query used to delete data from the database. + :type query: str + :param params: Optional parameters to be substituted in the query. + :type params: Union[None, tuple] + """ + self.connection.connect() + cursor = self.connection.connection.cursor() + if params is None: + cursor.execute(query) + elif isinstance(params, tuple): + cursor.execute(query, params) + elif isinstance(params, dict): + cursor.execute(query, params) + else: + raise ValueError("Unsupported type for 'params' argument") + self.connection.connection.commit() + self.connection.disconnect() + + def execute(self, query: str, params=None): + """ + Executes a custom SQL query against the SQLite database. + + :param query: The SQL query to execute. + :type query: str + :param params: Optional parameters to be substituted in the query. + :type params: Union[None, tuple] + """ + self.connection.connect() + cursor = self.connection.connection.cursor() + if params is None: + cursor.execute(query) + elif isinstance(params, tuple): + cursor.execute(query, params) + elif isinstance(params, dict): + cursor.execute(query, params) + else: + raise ValueError("Unsupported type for 'params' argument") + self.connection.connection.commit() + self.connection.disconnect() + + def update(self, query: str, params=None): + """ + Updates data in the SQLite database based on the given query and parameters. + + :param query: The SQL query used to update data in the database. + :type query: str + :param params: Optional parameters to be substituted in the query. + :type params: Union[None, tuple] + """ + self.connection.connect() + cursor = self.connection.connection.cursor() + if params is None: + cursor.execute(query) + elif isinstance(params, tuple): + cursor.execute(query, params) + elif isinstance(params, dict): + cursor.execute(query, params) + else: + raise ValueError("Unsupported type for 'params' argument") + self.connection.connection.commit() + self.connection.disconnect() + + def select(self, query: str, params=None): + """ + Executes a SELECT query against the SQLite database and returns the results as a list of rows. + + :param query: The SQL query to execute. + :type query: str + :param params: Optional parameters to be substituted in the query. + :type params: Union[None, tuple] + + :return: A list of rows returned by the query. + :rtype: list + """ + self.connection.connect() + cursor = self.connection.connection.cursor() + if params is None: + cursor.execute(query) + elif isinstance(params, tuple): + cursor.execute(query, params) + elif isinstance(params, dict): + cursor.execute(query, params) + else: + raise ValueError("Unsupported type for 'params' argument") + result = cursor.fetchall() + self.connection.disconnect() + return result + + def select_one(self, query: str, params=None): + """ + Executes a SELECT query against the SQLite database and returns a single row. + + :param query: The SQL query to execute. + :type query: str + :param params: Optional parameters to be substituted in the query. + :type params: Union[None, tuple] + + :return: A single row returned by the query, or None if no rows are found. + :rtype: Union[tuple, None] + """ + self.connection.connect() + cursor = self.connection.connection.cursor() + if params is None: + cursor.execute(query) + elif isinstance(params, tuple): + cursor.execute(query, params) + elif isinstance(params, dict): + cursor.execute(query, params) + else: + raise ValueError("Unsupported type for 'params' argument") + result = cursor.fetchone() + self.connection.disconnect() + return result diff --git a/tasks/check_usernames/core/repositories/__init__.py b/tasks/check_usernames/core/repositories/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tasks/check_usernames/core/repositories/base_repository.py b/tasks/check_usernames/core/repositories/base_repository.py new file mode 100644 index 00000000..a87feb73 --- /dev/null +++ b/tasks/check_usernames/core/repositories/base_repository.py @@ -0,0 +1,30 @@ +from abc import ABC, abstractmethod + + +class BaseRepository(ABC): + + @abstractmethod + def createUserTable(self): + pass + + @abstractmethod + def selectAllUsers(self, query: str, params=None): + """ + Executes a SELECT query and returns the results. + + :param query: The SQL query to execute. + :type query: str + :param params: Optional parameters to be substituted in the query. + :type params: Union[None, tuple] + + :return: The results of the query. + :rtype: Any + """ + pass + + @abstractmethod + def deleteAllUsers(self): + """ + Deletes all users from the database. + """ + pass diff --git a/tasks/check_usernames/core/repositories/mysql_repository.py b/tasks/check_usernames/core/repositories/mysql_repository.py new file mode 100644 index 00000000..d9309a40 --- /dev/null +++ b/tasks/check_usernames/core/repositories/mysql_repository.py @@ -0,0 +1,18 @@ +from abc import ABC + +from tasks.check_usernames.core.persistence.base_persistence import BasePersistence +from tasks.check_usernames.core.repositories.base_repository import BaseRepository + + +class MySQLRepository(BaseRepository, ABC): + def __init__(self, persistence: BasePersistence): + self.persistence = persistence + + def selectAllUsers(self, query: str, params=None): + return self.persistence.select(query, params) + + def createUserTable(self): + pass + + def deleteAllUsers(self): + pass diff --git a/tasks/check_usernames/core/repositories/sqlite_repository.py b/tasks/check_usernames/core/repositories/sqlite_repository.py new file mode 100644 index 00000000..8274f704 --- /dev/null +++ b/tasks/check_usernames/core/repositories/sqlite_repository.py @@ -0,0 +1,38 @@ +from abc import ABC +from typing import List + +from tasks.check_usernames.core.models.user import User +from tasks.check_usernames.core.persistence.base_persistence import BasePersistence +from tasks.check_usernames.core.repositories.base_repository import BaseRepository + + +class SQLiteRepository(BaseRepository, ABC): + def __init__(self, persistence: BasePersistence): + self.persistence = persistence + + def selectAllUsers(self, query: str, params=None): + pass + + def createUserTable(self): + query = """ + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_name TEXT NOT NULL UNIQUE, + created_at DATETIME NOT NULL, + status INTEGER + ) + """ + self.persistence.execute(query=query) + + def deleteAllUsers(self): + query = "DELETE FROM users" + self.persistence.delete(query=query) + + def saveUsers(self, users: List[User]): + for user in users: + query = """ INSERT INTO users + (user_name, created_at, status) + VALUES + ('{}', '{}', {}) + """.format(user.user_name, user.created_at, user.status) + self.persistence.execute(query=query) diff --git a/tasks/check_usernames/core/test.py b/tasks/check_usernames/core/test.py new file mode 100644 index 00000000..ca87ab5e --- /dev/null +++ b/tasks/check_usernames/core/test.py @@ -0,0 +1,57 @@ +import datetime + +from tasks.check_usernames.core.factory.factory import DatabaseFactory +from tasks.check_usernames.core.models.user import User + +# Get yesterday's date +yesterday = datetime.date.today() - datetime.timedelta(days=1) + +# Get start time for yesterday +start_time = datetime.datetime.combine(yesterday, datetime.time.min) + +# Get last time for yesterday +last_time = datetime.datetime.combine(yesterday, datetime.time.max) + +# Format dates for SQL query +start_time_sql = start_time.strftime("%Y%m%d%H%M%S") +# start_time_sql = 20221207000000 +last_time_sql = last_time.strftime("%Y%m%d%H%M%S") +# last_time_sql = 20230322235959 + + +factory = DatabaseFactory() + +mysql_repository = factory.create_mysql_repository() + +query = """ select log_title as "q_log_title",log_id as "q_log_id" + from logging + where log_type in ("newusers") + and log_timestamp BETWEEN {} AND {} + and log_title not in ( + select page.page_title from categorylinks + inner join page on page.page_id = categorylinks.cl_from + where cl_to like "أسماء_مستخدمين_مخالفة_مرشحة_للمنع" + and cl_type in ("page") + ) + and log_title not in ( + select replace(user.user_name," ","_") as "user_name_temp" from ipblocks + inner join user on ipblocks.ipb_user = user.user_id + ) + """.format(start_time_sql, last_time_sql) + +users_models = [] +raw_users = mysql_repository.selectAllUsers(query=query) + +for row in raw_users: + users_models.append( + User( + id=row["q_log_id"], + user_name=str(row["q_log_title"], 'utf-8'), + created_at=datetime.datetime.now() + ) + ) + +sqlite_repository = factory.create_sqlite_memory_repository() +sqlite_repository.createUserTable() +sqlite_repository.deleteAllUsers() +sqlite_repository.saveUsers(users_models)