From 1438d2d061201cd4343efc52ccfda43f5519a2a7 Mon Sep 17 00:00:00 2001 From: Ibrahem Date: Tue, 5 May 2026 01:56:19 +0300 Subject: [PATCH 01/16] . --- newapi/api_client/client.py | 203 ++++++++++++++++-------------- newapi/super/S_API/bot_api.py | 113 +++++++++-------- newapi/super/S_Page/super_page.py | 4 +- 3 files changed, 169 insertions(+), 151 deletions(-) diff --git a/newapi/api_client/client.py b/newapi/api_client/client.py index 32f227f..62ec259 100644 --- a/newapi/api_client/client.py +++ b/newapi/api_client/client.py @@ -485,29 +485,16 @@ def site(self) -> mwclient.Site: """The underlying ``mwclient.Site`` — use for high-level wiki access.""" return self._site - # ── Public methods ───────────────────────────────────────────────────── - - def login(self, force: bool = False) -> None: - """ - Force a fresh login regardless of cookie state. - - Call this if you know the session has expired and want to re-authenticate - without creating a new WikiLoginClient instance. - """ - if force or not self._site.logged_in: - logger.info( - "Forcing re-login for %s on %s.%s", - self.username, - self.lang, - self.family, - ) - self._do_login() + # ------------------------------------------------------------------ + # Private helpers + # ------------------------------------------------------------------ def _client_request( self, params: dict, method: str = "post", files: Optional[Any] = None, + **kwargs, ) -> dict: """ Send a GET or POST request to the wiki API and return parsed JSON. @@ -553,9 +540,10 @@ def _client_request( action = params.get("action") if action in self._WRITE_ACTIONS: method = "post" + if method == "get": return self._request_with_retry("GET", self.api_url, params=params) - #return self._site.get(action, **params) + # return self._site.get(action, **params) else: # Fetch a CSRF token now if the caller didn't supply one. # The retry loop will refresh it automatically on CSRF errors. @@ -563,19 +551,108 @@ def _client_request( params["token"] = self._site.get_token("csrf") return self._request_with_retry("POST", self.api_url, data=params, files=files) - #return self._site.post(action, **params, files=files) + # return self._site.post(action, **params, files=files) + + def _ensure_logged_in(self) -> None: + """ + Check whether the current session is authenticated. + """ + # if self._site.logged_in: + if getattr(self._site, "logged_in", None): + logger.info(f"Session already authenticated {self._site.logged_in=}") + return + if self._cookie_path.exists(): + try: + self._site.site_init() + if self._site.logged_in: + logger.info("Revived session via cookies as %s", self._site.username) + return + except Exception: + logger.exception("Error in site_init") + + # if not self._site.logged_in: self._do_login() + # don't login yet, user can use login() method + + def _enrich_params(self, params: dict) -> dict: + """ + Inject write-action safety parameters. + + For write actions: + - ``bot=1`` marks edits as bot edits in recent changes. + - ``assertuser`` ensures the API rejects requests from the wrong + account (guards against accidental edits). + + Query actions have write-only keys scrubbed instead. + """ + params = dict(params) + action = params.get("action", "") + + # Strip write-only params from query actions + if action == "query": + params.pop("bot", None) + params.pop("summary", None) + return params + + # Inject bot marker and identity assertion for all write actions + is_write = action in self._WRITE_ACTIONS or action.startswith("wb") or self.family == "wikidata" + if is_write and self.username: + params.setdefault("bot", 1) + params.setdefault("assertuser", self.username) + + return params + + def _do_login(self) -> None: + """ + Execute the mwclient login handshake and persist the resulting cookies. + + Raises: + LoginError: if mwclient rejects the credentials. + """ + try: + self._site.login(self.username, self._password) + except mwclient.errors.LoginError as exc: + raise LoginError(f"login failed for {self.username} on {self.lang}.{self.family}: {exc}") from exc + + if self._site.logged_in: + logger.info( + "Logged in successfully as %s on %s.%s", + self.username, + self.lang, + self.family, + ) + self.save_cookies(self.cj) + + # ── Public methods ───────────────────────────────────────────────────── + + def login(self, force: bool = False) -> None: + """ + Force a fresh login regardless of cookie state. + + Call this if you know the session has expired and want to re-authenticate + without creating a new WikiLoginClient instance. + """ + if force or not self._site.logged_in: + logger.info( + "Forcing re-login for %s on %s.%s", + self.username, + self.lang, + self.family, + ) + self._do_login() def client_request( self, params: dict, method: str = "post", files: Optional[Any] = None, + **kwargs, ) -> dict: """ """ return self._client_request( params=params, method=method, files=files, + **kwargs, ) def client_request_safe( @@ -583,6 +660,7 @@ def client_request_safe( params: dict, method: str = "post", files: Optional[Any] = None, + **kwargs, ) -> dict: """ """ try: @@ -590,6 +668,7 @@ def client_request_safe( params=params, method=method, files=files, + **kwargs, ) except Exception as exc: logger.warning("client_request_safe: %s", exc) @@ -600,6 +679,7 @@ def client_request_retry( params: dict, method: str = "post", files: Optional[Any] = None, + **kwargs, ) -> dict: """ Send a GET or POST request to the wiki API and return parsed JSON. @@ -628,18 +708,25 @@ def client_request_retry( raise ValueError(f"method must be 'get' or 'post', got {method!r}") # Files can only travel via multipart POST - if files is not None: + action = params.get("action") + if action in self._WRITE_ACTIONS or files is not None: method = "post" # Always request JSON and inject write-action safety params params = self._enrich_params({"format": "json", **params}) + skip_log_params = [ + "token", + "password", + "lgpassword", + "text", + ] logger.debug( "%s %s params=%s files=%s", method.upper(), self.api_url, # Never log token values - {k: ("***" if k == "token" else v) for k, v in params.items()}, + {k: ("***" if k in skip_log_params else v) for k, v in params.items()}, list(files.keys()) if files else None, ) @@ -672,6 +759,7 @@ def post_continue( first: bool = False, _p_2: str = "", _p_2_empty: Optional[Union[list, dict]] = None, + **kwargs, ) -> Union[list, dict]: """ Drive a MediaWiki API continuation query to completion. @@ -755,79 +843,6 @@ def post_continue( logger.debug("done, %d total results", len(results)) return results - # ------------------------------------------------------------------ - # Private helpers - # ------------------------------------------------------------------ - - def _ensure_logged_in(self) -> None: - """ - Check whether the current session is authenticated. - """ - # if self._site.logged_in: - if getattr(self._site, "logged_in", None): - logger.info(f"Session already authenticated {self._site.logged_in=}") - return - if self._cookie_path.exists(): - try: - self._site.site_init() - if self._site.logged_in: - logger.info("Revived session via cookies as %s", self._site.username) - return - except Exception: - logger.exception("Error in site_init") - - # if not self._site.logged_in: self._do_login() - # don't login yet, user can use login() method - - def _enrich_params(self, params: dict) -> dict: - """ - Inject write-action safety parameters. - - For write actions: - - ``bot=1`` marks edits as bot edits in recent changes. - - ``assertuser`` ensures the API rejects requests from the wrong - account (guards against accidental edits). - - Query actions have write-only keys scrubbed instead. - """ - params = dict(params) - action = params.get("action", "") - - # Strip write-only params from query actions - if action == "query": - params.pop("bot", None) - params.pop("summary", None) - return params - - # Inject bot marker and identity assertion for all write actions - is_write = action in self._WRITE_ACTIONS or action.startswith("wb") or self.family == "wikidata" - if is_write and self.username: - params.setdefault("bot", 1) - params.setdefault("assertuser", self.username) - - return params - - def _do_login(self) -> None: - """ - Execute the mwclient login handshake and persist the resulting cookies. - - Raises: - LoginError: if mwclient rejects the credentials. - """ - try: - self._site.login(self.username, self._password) - except mwclient.errors.LoginError as exc: - raise LoginError(f"login failed for {self.username} on {self.lang}.{self.family}: {exc}") from exc - - if self._site.logged_in: - logger.info( - "Logged in successfully as %s on %s.%s", - self.username, - self.lang, - self.family, - ) - self.save_cookies(self.cj) - def __repr__(self) -> str: return f"WikiLoginClient(lang={self.lang!r}, family={self.family!r}, username={self.username!r})" diff --git a/newapi/super/S_API/bot_api.py b/newapi/super/S_API/bot_api.py index cc16b2a..5140ecf 100644 --- a/newapi/super/S_API/bot_api.py +++ b/newapi/super/S_API/bot_api.py @@ -8,17 +8,16 @@ import tqdm +from ...api_client import WikiLoginClient + from ...api_utils.lang_codes import change_codes from .bot import BotsAPIS logger = logging.getLogger(__name__) -logger = logging.getLogger(__name__) - - class NewApi(BotsAPIS): - def __init__(self, login_bot, lang="", family="wikipedia"): + def __init__(self, login_bot: WikiLoginClient, lang: str="", family: str="wikipedia"): # --- self.login_bot = login_bot # --- @@ -33,56 +32,6 @@ def __init__(self, login_bot, lang="", family="wikipedia"): self.cxtoken = "" # --- super().__init__() - def post_params( - self, - params, - request_type="get", - addtoken=False, - get_csrf=True, - files=None, - do_error=False, - max_retry=0, - ): - # --- - return self.login_bot.client_request( - params, - method=request_type, - files=files, - ) - def client_request( - self, - params, - request_type="get", - files=None, - ): - # --- - return self.login_bot.client_request( - params, - method=request_type, - files=files, - ) - - def post_continue( - self, - params, - action, - _p_="pages", - p_empty=None, - max=500000, - first=False, - _p_2="", - _p_2_empty=None, - ): - return self.login_bot.post_continue( - params, - action, - _p_=_p_, - p_empty=p_empty, - max=max, - first=first, - _p_2=_p_2, - _p_2_empty=_p_2_empty, - ) def get_username(self): return self.username @@ -944,7 +893,7 @@ def get_cxtoken(self): # --- params = {"action": "cxtoken", "format": "json"} # --- - data = self.client_request(params, request_type="post") + data = self.client_request(params, method="post") # --- if not data: return "" @@ -996,3 +945,57 @@ def users_infos(self, ususers=[]): results = [dict(x) for x in results] # --- return results + + def post_params( + self, + params, + method="get", + files=None, + **kwargs, + ): + # --- + return self.login_bot.client_request( + params, + method=method, + files=files, + **kwargs, + ) + + def client_request( + self, + params, + method="get", + files=None, + **kwargs, + ): + # --- + return self.login_bot.client_request( + params, + method=method, + files=files, + **kwargs, + ) + + def post_continue( + self, + params, + action, + _p_="pages", + p_empty=None, + max=500000, + first=False, + _p_2="", + _p_2_empty=None, + **kwargs, + ): + return self.login_bot.post_continue( + params, + action, + _p_=_p_, + p_empty=p_empty, + max=max, + first=first, + _p_2=_p_2, + _p_2_empty=_p_2_empty, + **kwargs, + ) diff --git a/newapi/super/S_Page/super_page.py b/newapi/super/S_Page/super_page.py index 17d8494..031fe76 100644 --- a/newapi/super/S_Page/super_page.py +++ b/newapi/super/S_Page/super_page.py @@ -65,13 +65,13 @@ def __init__( def client_request( self, params: Dict[str, Any], - request_type: str = "get", + method: str = "get", files: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: # --- return self.login_bot.client_request( params, - method=request_type, + method=method, files=files, ) From 842f0f72ddffa6b817edb00bddc5a12d15e4e4ad Mon Sep 17 00:00:00 2001 From: Ibrahem Date: Tue, 5 May 2026 02:14:00 +0300 Subject: [PATCH 02/16] . --- newapi/__init__.py | 4 ++-- newapi/{ => client_wiki}/api_utils/__init__.py | 0 newapi/{ => client_wiki}/api_utils/ask_bot.py | 3 ++- newapi/{ => client_wiki}/api_utils/botEdit.py | 0 .../api_utils/bot_edit/__init__.py | 0 .../api_utils/bot_edit/bot_edit_by_templates.py | 2 +- .../api_utils/bot_edit/bot_edit_by_time.py | 0 .../{ => client_wiki}/api_utils/lang_codes.py | 0 newapi/{ => client_wiki}/api_utils/printe.py | 2 +- newapi/{ => client_wiki}/api_utils/txtlib.py | 0 .../{ => client_wiki}/api_utils/user_agent.py | 0 newapi/{ => client_wiki}/api_utils/wd_sparql.py | 0 newapi/super/S_API/bot.py | 2 +- newapi/super/S_API/bot_api.py | 8 +++++--- newapi/super/S_Page/super_page.py | 17 +++++++++-------- .../test_bot_edit_by_templates.py | 2 +- .../test_bot_edit_by_templates2.py | 4 ++-- .../test_bot_edit_by_templates_pypass.py | 12 ++++++------ .../bot_edit_by_time/test_bot_edit_by_time.py | 2 +- 19 files changed, 31 insertions(+), 27 deletions(-) rename newapi/{ => client_wiki}/api_utils/__init__.py (100%) rename newapi/{ => client_wiki}/api_utils/ask_bot.py (98%) rename newapi/{ => client_wiki}/api_utils/botEdit.py (100%) rename newapi/{ => client_wiki}/api_utils/bot_edit/__init__.py (100%) rename newapi/{ => client_wiki}/api_utils/bot_edit/bot_edit_by_templates.py (99%) rename newapi/{ => client_wiki}/api_utils/bot_edit/bot_edit_by_time.py (100%) rename newapi/{ => client_wiki}/api_utils/lang_codes.py (100%) rename newapi/{ => client_wiki}/api_utils/printe.py (96%) rename newapi/{ => client_wiki}/api_utils/txtlib.py (100%) rename newapi/{ => client_wiki}/api_utils/user_agent.py (100%) rename newapi/{ => client_wiki}/api_utils/wd_sparql.py (100%) diff --git a/newapi/__init__.py b/newapi/__init__.py index 7437658..724655d 100644 --- a/newapi/__init__.py +++ b/newapi/__init__.py @@ -1,11 +1,11 @@ """ """ -from .api_utils.lang_codes import change_codes +from .client_wiki.api_utils.lang_codes import change_codes +from .client_wiki.api_utils import botEdit, txtlib, wd_sparql from . import page from .all_apis import AllAPIS from .api_client.client import WikiLoginClient -from .api_utils import botEdit, txtlib, wd_sparql from .DB_bots import db_bot, pymysql_bot __all__ = [ diff --git a/newapi/api_utils/__init__.py b/newapi/client_wiki/api_utils/__init__.py similarity index 100% rename from newapi/api_utils/__init__.py rename to newapi/client_wiki/api_utils/__init__.py diff --git a/newapi/api_utils/ask_bot.py b/newapi/client_wiki/api_utils/ask_bot.py similarity index 98% rename from newapi/api_utils/ask_bot.py rename to newapi/client_wiki/api_utils/ask_bot.py index 727f2f7..333c46b 100644 --- a/newapi/api_utils/ask_bot.py +++ b/newapi/client_wiki/api_utils/ask_bot.py @@ -5,9 +5,10 @@ import pywikibot -from ..config import settings +from ...config import settings logger = logging.getLogger(__name__) + _save_or_ask: dict[str, bool] = {} diff --git a/newapi/api_utils/botEdit.py b/newapi/client_wiki/api_utils/botEdit.py similarity index 100% rename from newapi/api_utils/botEdit.py rename to newapi/client_wiki/api_utils/botEdit.py diff --git a/newapi/api_utils/bot_edit/__init__.py b/newapi/client_wiki/api_utils/bot_edit/__init__.py similarity index 100% rename from newapi/api_utils/bot_edit/__init__.py rename to newapi/client_wiki/api_utils/bot_edit/__init__.py diff --git a/newapi/api_utils/bot_edit/bot_edit_by_templates.py b/newapi/client_wiki/api_utils/bot_edit/bot_edit_by_templates.py similarity index 99% rename from newapi/api_utils/bot_edit/bot_edit_by_templates.py rename to newapi/client_wiki/api_utils/bot_edit/bot_edit_by_templates.py index 85a5d2c..9f503d7 100644 --- a/newapi/api_utils/bot_edit/bot_edit_by_templates.py +++ b/newapi/client_wiki/api_utils/bot_edit/bot_edit_by_templates.py @@ -5,7 +5,7 @@ import wikitextparser as wtp -from ...config import settings +from ....config import settings logger = logging.getLogger(__name__) edit_username = {1: "Mr.Ibrahembot"} diff --git a/newapi/api_utils/bot_edit/bot_edit_by_time.py b/newapi/client_wiki/api_utils/bot_edit/bot_edit_by_time.py similarity index 100% rename from newapi/api_utils/bot_edit/bot_edit_by_time.py rename to newapi/client_wiki/api_utils/bot_edit/bot_edit_by_time.py diff --git a/newapi/api_utils/lang_codes.py b/newapi/client_wiki/api_utils/lang_codes.py similarity index 100% rename from newapi/api_utils/lang_codes.py rename to newapi/client_wiki/api_utils/lang_codes.py diff --git a/newapi/api_utils/printe.py b/newapi/client_wiki/api_utils/printe.py similarity index 96% rename from newapi/api_utils/printe.py rename to newapi/client_wiki/api_utils/printe.py index 8129bb1..02dcdad 100644 --- a/newapi/api_utils/printe.py +++ b/newapi/client_wiki/api_utils/printe.py @@ -4,7 +4,7 @@ import pywikibot -from ..config import settings +from ...config import settings logger = logging.getLogger(__name__) diff --git a/newapi/api_utils/txtlib.py b/newapi/client_wiki/api_utils/txtlib.py similarity index 100% rename from newapi/api_utils/txtlib.py rename to newapi/client_wiki/api_utils/txtlib.py diff --git a/newapi/api_utils/user_agent.py b/newapi/client_wiki/api_utils/user_agent.py similarity index 100% rename from newapi/api_utils/user_agent.py rename to newapi/client_wiki/api_utils/user_agent.py diff --git a/newapi/api_utils/wd_sparql.py b/newapi/client_wiki/api_utils/wd_sparql.py similarity index 100% rename from newapi/api_utils/wd_sparql.py rename to newapi/client_wiki/api_utils/wd_sparql.py diff --git a/newapi/super/S_API/bot.py b/newapi/super/S_API/bot.py index 6b7f09a..86ec79e 100644 --- a/newapi/super/S_API/bot.py +++ b/newapi/super/S_API/bot.py @@ -7,7 +7,7 @@ import logging import sys -from ...api_utils.ask_bot import AskBot +from ...client_wiki.api_utils.ask_bot import AskBot from ..handel_errors import HandelErrors logger = logging.getLogger(__name__) diff --git a/newapi/super/S_API/bot_api.py b/newapi/super/S_API/bot_api.py index 5140ecf..d0ef8fc 100644 --- a/newapi/super/S_API/bot_api.py +++ b/newapi/super/S_API/bot_api.py @@ -9,8 +9,7 @@ import tqdm from ...api_client import WikiLoginClient - -from ...api_utils.lang_codes import change_codes +from ...client_wiki.api_utils.lang_codes import change_codes from .bot import BotsAPIS logger = logging.getLogger(__name__) @@ -908,7 +907,10 @@ def get_cxtoken(self): # --- return jwt - def users_infos(self, ususers=[]): + def users_infos(self, ususers=None) -> list[dict]: + # --- + if not isinstance(ususers, list): + ususers = [] # --- params = { "action": "query", diff --git a/newapi/super/S_Page/super_page.py b/newapi/super/S_Page/super_page.py index 031fe76..cea2842 100644 --- a/newapi/super/S_Page/super_page.py +++ b/newapi/super/S_Page/super_page.py @@ -5,9 +5,9 @@ import wikitextparser as wtp -from ...api_utils import botEdit, txtlib -from ...api_utils.ask_bot import AskBot -from ...api_utils.lang_codes import change_codes +from ...client_wiki.api_utils import botEdit, txtlib +from ...client_wiki.api_utils.ask_bot import AskBot +from ...client_wiki.api_utils.lang_codes import change_codes from ...config import settings from .ar_err import find_edit_error from .bot import PageAPIS @@ -955,7 +955,7 @@ def page_links_query(self, plnamespace="*"): # --- return self.links_data.links - def get_revisions(self, rvprops=[]): + def get_revisions(self, rvprops=None) -> list: # --- rvprop = [ "comment", @@ -965,9 +965,10 @@ def get_revisions(self, rvprops=[]): "ids", ] # --- - for x in rvprops: - if x not in rvprop: - rvprop.append(x) + if rvprops: + for x in rvprops: + if x not in rvprop: + rvprop.append(x) # --- params = { "action": "query", @@ -998,4 +999,4 @@ def __getitem__(self, key): if key == "q": return self.get_qid() else: - raise + raise # noqa: PLE0704 diff --git a/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates.py b/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates.py index 82f09d8..1dfe843 100644 --- a/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates.py +++ b/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates.py @@ -8,7 +8,7 @@ import sys import pytest -from newapi.api_utils.bot_edit.bot_edit_by_templates import ( +from newapi.client_wiki.api_utils.bot_edit.bot_edit_by_templates import ( Bot_Cache, edit_username, is_bot_edit_allowed, diff --git a/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates2.py b/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates2.py index bd67a75..159fd40 100644 --- a/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates2.py +++ b/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates2.py @@ -12,7 +12,7 @@ from unittest.mock import MagicMock, patch import pytest -from newapi.api_utils.bot_edit.bot_edit_by_templates import ( +from newapi.client_wiki.api_utils.bot_edit.bot_edit_by_templates import ( Bot_Cache, is_bot_edit_allowed, stop_edit_temps, @@ -39,7 +39,7 @@ def reset_environment(): @pytest.fixture def mock_wtp(): """Provide a mocked wikitextparser.""" - with patch("newapi.api_utils.bot_edit.bot_edit_by_templates.wtp") as mock: + with patch("newapi.client_wiki.api_utils.bot_edit.bot_edit_by_templates.wtp") as mock: yield mock diff --git a/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates_pypass.py b/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates_pypass.py index 9b6533a..87bfa3e 100644 --- a/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates_pypass.py +++ b/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates_pypass.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch import pytest -from newapi.api_utils.bot_edit.bot_edit_by_templates import ( +from newapi.client_wiki.api_utils.bot_edit.bot_edit_by_templates import ( Bot_Cache, is_bot_edit_allowed, ) @@ -38,7 +38,7 @@ def reset_environment(): @pytest.fixture def mock_wtp(): """Provide a mocked wikitextparser.""" - with patch("newapi.api_utils.bot_edit.bot_edit_by_templates.wtp") as mock: + with patch("newapi.client_wiki.api_utils.bot_edit.bot_edit_by_templates.wtp") as mock: yield mock @@ -103,7 +103,7 @@ def test_argv_bypasses_all_checks(self, argv_value, setup_parser): setup_parser([{"name": "nobots", "arguments": None}]) text = "{{nobots}}" - with patch("newapi.api_utils.bot_edit.bot_edit_by_templates.settings") as mock_settings: + with patch("newapi.client_wiki.api_utils.bot_edit.bot_edit_by_templates.settings") as mock_settings: if argv_value in ("botedit", "editbot"): mock_settings.bot.force_edit = True else: @@ -118,21 +118,21 @@ class TestBypassConditions: def test_bypass_with_botedit_arg(self, original_argv): """Should return True when 'botedit' is in sys.argv.""" text = "{{nobots}}" - with patch("newapi.api_utils.bot_edit.bot_edit_by_templates.settings") as mock_settings: + with patch("newapi.client_wiki.api_utils.bot_edit.bot_edit_by_templates.settings") as mock_settings: mock_settings.bot.force_edit = True assert is_bot_edit_allowed(text=text, title_page="Test", botjob="all") def test_bypass_with_editbot_arg(self, original_argv): """Should return True when 'editbot' is in sys.argv.""" text = "{{nobots}}" - with patch("newapi.api_utils.bot_edit.bot_edit_by_templates.settings") as mock_settings: + with patch("newapi.client_wiki.api_utils.bot_edit.bot_edit_by_templates.settings") as mock_settings: mock_settings.bot.force_edit = True assert is_bot_edit_allowed(text=text, title_page="Test", botjob="all") def test_bypass_with_workibrahem_arg(self, original_argv): """Should return True when 'workibrahem' is in sys.argv.""" text = "{{nobots}}" - with patch("newapi.api_utils.bot_edit.bot_edit_by_templates.settings") as mock_settings: + with patch("newapi.client_wiki.api_utils.bot_edit.bot_edit_by_templates.settings") as mock_settings: mock_settings.bot.workibrahem = True assert is_bot_edit_allowed(text=text, title_page="Test", botjob="all") diff --git a/tests/unit/api_utils/bot_edit/bot_edit_by_time/test_bot_edit_by_time.py b/tests/unit/api_utils/bot_edit/bot_edit_by_time/test_bot_edit_by_time.py index 594d828..3efe669 100644 --- a/tests/unit/api_utils/bot_edit/bot_edit_by_time/test_bot_edit_by_time.py +++ b/tests/unit/api_utils/bot_edit/bot_edit_by_time/test_bot_edit_by_time.py @@ -3,7 +3,7 @@ import sys import pytest -from newapi.api_utils.bot_edit.bot_edit_by_time import ( +from newapi.client_wiki.api_utils.bot_edit.bot_edit_by_time import ( check_create_time, check_last_edit_time, ) From e0910a4e3afb5430cd593f26f1cf0a1fcc79698c Mon Sep 17 00:00:00 2001 From: Ibrahem Date: Tue, 5 May 2026 02:14:16 +0300 Subject: [PATCH 03/16] . --- newapi/client_wiki/__init__.py | 0 newapi/client_wiki/api_utils/__init__.py | 32 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 newapi/client_wiki/__init__.py diff --git a/newapi/client_wiki/__init__.py b/newapi/client_wiki/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/newapi/client_wiki/api_utils/__init__.py b/newapi/client_wiki/api_utils/__init__.py index e69de29..23c9202 100644 --- a/newapi/client_wiki/api_utils/__init__.py +++ b/newapi/client_wiki/api_utils/__init__.py @@ -0,0 +1,32 @@ +from .ask_bot import AskBot, showDiff +from .botEdit import ( + bot_May_Edit, + check_create_time, + check_last_edit_time, + extract_templates_and_params, +) + +change_codes = { + "bat_smg": "bat-smg", + "be-x-old": "be-tarask", + "be_x_old": "be-tarask", + "cbk_zam": "cbk-zam", + "fiu_vro": "fiu-vro", + "map_bms": "map-bms", + "nb": "no", + "nds_nl": "nds-nl", + "roa_rup": "roa-rup", + "zh_classical": "zh-classical", + "zh_min_nan": "zh-min-nan", + "zh_yue": "zh-yue", +} + +__all__ = [ + "AskBot", + "change_codes", + "showDiff", + "bot_May_Edit", + "check_create_time", + "check_last_edit_time", + "extract_templates_and_params", +] From 3998f3aa273948e7a27fccd69b52cd4287b4f210 Mon Sep 17 00:00:00 2001 From: Ibrahem Date: Tue, 5 May 2026 02:17:08 +0300 Subject: [PATCH 04/16] . --- newapi/client_wiki/api_utils/__init__.py | 2 -- .../categories}/catdepth_new.py | 3 +-- .../categories/category_db.py} | 19 +++++++++++++++---- newapi/pages_bots/all_apis.py | 2 +- newapi/super/S_Category/__init__.py | 0 newapi/super/__init__.py | 2 +- 6 files changed, 18 insertions(+), 10 deletions(-) rename newapi/{super/S_Category => client_wiki/categories}/catdepth_new.py (98%) rename newapi/{super/S_Category/bot.py => client_wiki/categories/category_db.py} (92%) delete mode 100644 newapi/super/S_Category/__init__.py diff --git a/newapi/client_wiki/api_utils/__init__.py b/newapi/client_wiki/api_utils/__init__.py index 23c9202..65b835e 100644 --- a/newapi/client_wiki/api_utils/__init__.py +++ b/newapi/client_wiki/api_utils/__init__.py @@ -3,7 +3,6 @@ bot_May_Edit, check_create_time, check_last_edit_time, - extract_templates_and_params, ) change_codes = { @@ -28,5 +27,4 @@ "bot_May_Edit", "check_create_time", "check_last_edit_time", - "extract_templates_and_params", ] diff --git a/newapi/super/S_Category/catdepth_new.py b/newapi/client_wiki/categories/catdepth_new.py similarity index 98% rename from newapi/super/S_Category/catdepth_new.py rename to newapi/client_wiki/categories/catdepth_new.py index d154990..432f829 100644 --- a/newapi/super/S_Category/catdepth_new.py +++ b/newapi/client_wiki/categories/catdepth_new.py @@ -2,10 +2,9 @@ import functools import logging -import sys import time -from .bot import CategoryDepth +from .category_db import CategoryDepth logger = logging.getLogger(__name__) diff --git a/newapi/super/S_Category/bot.py b/newapi/client_wiki/categories/category_db.py similarity index 92% rename from newapi/super/S_Category/bot.py rename to newapi/client_wiki/categories/category_db.py index 719a746..e93ff3f 100644 --- a/newapi/super/S_Category/bot.py +++ b/newapi/client_wiki/categories/category_db.py @@ -5,6 +5,8 @@ from tqdm import tqdm +from ...api_client import WikiLoginClient + logger = logging.getLogger(__name__) @@ -15,7 +17,12 @@ class CategoryDepth: Provides methods for recursively querying category members. """ - def __init__(self, login_bot, title: str = "", **kwargs) -> None: + def __init__( + self, + login_bot: WikiLoginClient, + title: str = "", + **kwargs, + ) -> None: self.login_bot = login_bot self.title: str = title @@ -57,11 +64,12 @@ def _parse_params(self, **kwargs) -> None: self.len_pages = 0 self.revids, self.timestamps, self.result_table = {}, {}, {} self.title = kwargs.get("title", "") + logger.debug(f"parsing params for {self.title}: depth={kwargs.get('depth')}, ns={kwargs.get('ns')}") try: self.depth = int(kwargs.get("depth", 0)) except ValueError: - print(f"self.depth != int: {kwargs.get('depth')}") + logger.error(f"self.depth != int: {kwargs.get('depth')}") self.depth = 0 self.props = [] @@ -249,10 +257,10 @@ def get_cat_new(self, cac: str) -> dict: if continue_params: params.update(continue_params) - api_data = self.client_request(params) + api_data = self.login_bot.client_request(params, method="get") if not api_data: - print(f"api is False for {cac}") + logger.info(f"api is False for {cac}") break continue_params = api_data.get("continue", {}) @@ -289,6 +297,7 @@ def add_to_result_table(self, x: str, tab: dict) -> None: self.result_table[x] = tab def subcatquery_(self) -> dict: + logger.info(f"starting subcatquery for {self.title}, depth={self.depth}") tablemember = self.get_cat_new(self.title) for x, zz in tablemember.items(): @@ -308,6 +317,7 @@ def subcatquery_(self) -> dict: break depth_done += 1 + logger.info(f"depth {depth_done}/{self.depth}: {len(new_list)} subcategories to process") for cat in tqdm(new_list): table2 = self.get_cat_new(cat) @@ -323,4 +333,5 @@ def subcatquery_(self) -> dict: soro = sorted(self.result_table.items(), key=lambda item: self.timestamps.get(item[0], 0), reverse=True) self.result_table = dict(soro) + logger.debug(f"subcatquery done: {len(self.result_table)} total results") return self.result_table diff --git a/newapi/pages_bots/all_apis.py b/newapi/pages_bots/all_apis.py index 9fcc85a..28d46d6 100644 --- a/newapi/pages_bots/all_apis.py +++ b/newapi/pages_bots/all_apis.py @@ -10,7 +10,7 @@ from ..api_client.client import WikiLoginClient from ..super.S_API import bot_api -from ..super.S_Category import catdepth_new +from ..client_wiki.categories import catdepth_new from ..super.S_Page import super_page logger = logging.getLogger(__name__) diff --git a/newapi/super/S_Category/__init__.py b/newapi/super/S_Category/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/newapi/super/__init__.py b/newapi/super/__init__.py index a91b56b..7ab7abe 100644 --- a/newapi/super/__init__.py +++ b/newapi/super/__init__.py @@ -1,7 +1,7 @@ """ """ from .S_API import bot_api -from .S_Category import catdepth_new +from ..client_wiki.categories import catdepth_new from .S_Page import super_page __all__ = [ From e6d3db38542a41d8a0615eaf19cecea1e686442a Mon Sep 17 00:00:00 2001 From: Ibrahem Date: Tue, 5 May 2026 02:21:24 +0300 Subject: [PATCH 05/16] . --- newapi/all_apis.py | 2 +- newapi/{pages_bots => client_wiki}/all_apis.py | 0 newapi/pages_bots/__init__.py | 7 ------- tests/TestALL_APIS.py | 8 ++++---- 4 files changed, 5 insertions(+), 12 deletions(-) rename newapi/{pages_bots => client_wiki}/all_apis.py (100%) delete mode 100644 newapi/pages_bots/__init__.py diff --git a/newapi/all_apis.py b/newapi/all_apis.py index cbf2fb3..ae12a02 100644 --- a/newapi/all_apis.py +++ b/newapi/all_apis.py @@ -1,6 +1,6 @@ """ """ -from .pages_bots.all_apis import ( +from .client_wiki.all_apis import ( AllAPIS, ) diff --git a/newapi/pages_bots/all_apis.py b/newapi/client_wiki/all_apis.py similarity index 100% rename from newapi/pages_bots/all_apis.py rename to newapi/client_wiki/all_apis.py diff --git a/newapi/pages_bots/__init__.py b/newapi/pages_bots/__init__.py deleted file mode 100644 index b316c11..0000000 --- a/newapi/pages_bots/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -import os - -if not os.getenv("BOTNAME"): - botname = "newapi" - os.environ["BOTNAME"] = botname - - # os.getenv("BOTNAME") diff --git a/tests/TestALL_APIS.py b/tests/TestALL_APIS.py index 79fe4e7..fbd9d26 100644 --- a/tests/TestALL_APIS.py +++ b/tests/TestALL_APIS.py @@ -7,10 +7,10 @@ @pytest.fixture def mock_dependencies(): with ( - patch("newapi.pages_bots.all_apis.WikiLoginClient") as mock_login, - patch("newapi.pages_bots.all_apis.super_page.MainPage") as mock_main_page, - patch("newapi.pages_bots.all_apis.catdepth_new.subcatquery") as mock_subcatquery, - patch("newapi.pages_bots.all_apis.bot_api.NewApi") as mock_new_api, + patch("newapi.client_wiki.all_apis.WikiLoginClient") as mock_login, + patch("newapi.client_wiki.all_apis.super_page.MainPage") as mock_main_page, + patch("newapi.client_wiki.all_apis.catdepth_new.subcatquery") as mock_subcatquery, + patch("newapi.client_wiki.all_apis.bot_api.NewApi") as mock_new_api, ): mock_login_instance = MagicMock() mock_login.return_value = mock_login_instance From dbd6978d918a3a9f5ec558016e76b69cddc9ff84 Mon Sep 17 00:00:00 2001 From: Ibrahem Date: Tue, 5 May 2026 02:31:40 +0300 Subject: [PATCH 06/16] . --- newapi/__init__.py | 5 +- newapi/client_wiki/all_apis.py | 12 ++--- .../api_utils}/handel_errors.py | 49 ++++++++++--------- newapi/client_wiki/categories/__init__.py | 0 newapi/client_wiki/categories/catdepth_new.py | 23 ++------- newapi/client_wiki/constants.py | 12 +++++ newapi/super/S_API/bot.py | 6 +-- newapi/super/S_API/bot_api.py | 2 +- newapi/super/S_Page/bot.py | 4 +- newapi/super/S_Page/super_page.py | 4 +- newapi/super/__init__.py | 2 +- newapi/utils/__init__.py | 6 +++ newapi/utils/functions_timer.py | 20 ++++++++ 13 files changed, 80 insertions(+), 65 deletions(-) rename newapi/{super => client_wiki/api_utils}/handel_errors.py (80%) create mode 100644 newapi/client_wiki/categories/__init__.py create mode 100644 newapi/client_wiki/constants.py create mode 100644 newapi/utils/__init__.py create mode 100644 newapi/utils/functions_timer.py diff --git a/newapi/__init__.py b/newapi/__init__.py index 724655d..aa256e2 100644 --- a/newapi/__init__.py +++ b/newapi/__init__.py @@ -1,11 +1,10 @@ """ """ -from .client_wiki.api_utils.lang_codes import change_codes -from .client_wiki.api_utils import botEdit, txtlib, wd_sparql - from . import page from .all_apis import AllAPIS from .api_client.client import WikiLoginClient +from .client_wiki.api_utils import botEdit, txtlib, wd_sparql +from .client_wiki.api_utils.lang_codes import change_codes from .DB_bots import db_bot, pymysql_bot __all__ = [ diff --git a/newapi/client_wiki/all_apis.py b/newapi/client_wiki/all_apis.py index 28d46d6..2ab4f02 100644 --- a/newapi/client_wiki/all_apis.py +++ b/newapi/client_wiki/all_apis.py @@ -1,16 +1,10 @@ -""" - -main_api = AllAPIS(lang='en', family='wikipedia', username='your_username', password='your_password') -page = main_api.MainPage('Main Page Title') -cat_members = main_api.CatDepth('Category Title') -new_api = main_api.NewApi() -""" +""" """ import logging -from ..api_client.client import WikiLoginClient +from ..api_client import WikiLoginClient +from .categories import catdepth_new from ..super.S_API import bot_api -from ..client_wiki.categories import catdepth_new from ..super.S_Page import super_page logger = logging.getLogger(__name__) diff --git a/newapi/super/handel_errors.py b/newapi/client_wiki/api_utils/handel_errors.py similarity index 80% rename from newapi/super/handel_errors.py rename to newapi/client_wiki/api_utils/handel_errors.py index 3547809..6b14b94 100644 --- a/newapi/super/handel_errors.py +++ b/newapi/client_wiki/api_utils/handel_errors.py @@ -1,13 +1,5 @@ """ -from .super.handel_errors import HandelErrors - -""" - -import logging -import sys -from typing import Any, Dict, Optional, Union - -from ..core.exceptions import ( +from ...core.exceptions import ( AbuseFilterError, ApiError, ArticleExistsError, @@ -16,11 +8,14 @@ ProtectedPageError, parse_api_error, ) +""" + +import logging logger = logging.getLogger(__name__) -class HandelErrors: +class HandleErrors: """ Error handler for MediaWiki API errors. @@ -36,13 +31,13 @@ def __init__(self) -> None: config: Optional BotConfig for behavior settings. """ - def handel_err( + def handle_err( self, - error: Dict[str, Any], + error: dict, function: str = "", - params: Optional[Dict[str, Any]] = None, + params: dict | None = None, do_error: bool = True, - ) -> Union[str, bool, ApiError]: + ) -> dict | str | bool: """Handle errors based on the provided error dictionary. This function processes an error dictionary and performs actions based @@ -76,14 +71,14 @@ def handel_err( err_info = error.get("info", "") _tt = f"<>{function} ERROR: <>code:{err_code}." - ["protectedpage", "تأخير البوتات 3 ساعات", False] + # ["protectedpage", "تأخير البوتات 3 ساعات", False] if err_code == "abusefilter-disallowed": # oioioi = {'error': {'code': 'abusefilter-disallowed', 'info': 'This', 'abusefilter': {'id': '169', 'description': 'تأخير البوتات 3 ساعات', 'actions': ['disallow']}, '*': 'See https'}, 'servedby': 'mw1374'} abusefilter = error.get("abusefilter", "") - description = abusefilter.get("description", "") - logger.info(f"<> ** abusefilter-disallowed: {description} ") + description = abusefilter.get("description", "") if isinstance(abusefilter, dict) else "" + logger.debug(f"<> ** abusefilter-disallowed: {description} ") if description in [ "تأخير البوتات 3 ساعات", "تأخير البوتات 3 ساعات- 3 من 3", @@ -94,23 +89,29 @@ def handel_err( return description if err_code == "no-such-entity": - logger.info("<> ** no-such-entity. ") + logger.debug("<> ** no-such-entity. ") return False if err_code == "protectedpage": - logger.info("<> ** protectedpage. ") - # return "protectedpage" + logger.debug("<> ** protectedpage. ") return False if err_code == "articleexists": - logger.info("<> ** article already created. ") + logger.debug("<> ** article already created. ") return "articleexists" if err_code == "maxlag": - logger.info("<> ** maxlag. ") + logger.debug("<> ** maxlag. ") return False if do_error: - params["data"] = {} - params["text"] = {} + if params: + params["data"] = {} + params["text"] = {} logger.error(f"<>{function} ERROR: <>info: {err_info}, {params=}") + return error + + +__all__ = [ + "HandleErrors", +] diff --git a/newapi/client_wiki/categories/__init__.py b/newapi/client_wiki/categories/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/newapi/client_wiki/categories/catdepth_new.py b/newapi/client_wiki/categories/catdepth_new.py index 432f829..20e5ccc 100644 --- a/newapi/client_wiki/categories/catdepth_new.py +++ b/newapi/client_wiki/categories/catdepth_new.py @@ -2,8 +2,9 @@ import functools import logging -import time +from ...utils import function_timer +from ..constants import CATEGORY_PREFIXES from .category_db import CategoryDepth logger = logging.getLogger(__name__) @@ -11,25 +12,6 @@ SITECODE = "en" FAMILY = "wikipedia" -CATEGORY_PREFIXES: dict[str, str] = { - "ar": "تصنيف:", - "en": "Category:", - "www": "Category:", -} - - -def function_timer(func): - """Log how long a function takes to run.""" - - @functools.wraps(func) - def wrapper(*args, **kwargs): - start = time.perf_counter() - result = func(*args, **kwargs) - logger.debug(f"{func.__name__} finished in {time.perf_counter() - start:.4f}s") - return result - - return wrapper - @functools.lru_cache(maxsize=256) def title_process(title: str, sitecode: str) -> str: @@ -76,6 +58,7 @@ def subcatquery(login_bot, title: str, sitecode: str = SITECODE, family: str = F f"<> catdepth_new.py sub cat query for {sitecode}:{title}, depth:{args2['depth']}, ns:{args2['ns']}, onlyns:{args2['onlyns']}" ) + logger.info(f"starting subcategory query: {sitecode}:{title}") bot = CategoryDepth(login_bot, title, **kwargs) result = bot.subcatquery_() diff --git a/newapi/client_wiki/constants.py b/newapi/client_wiki/constants.py new file mode 100644 index 0000000..0a96504 --- /dev/null +++ b/newapi/client_wiki/constants.py @@ -0,0 +1,12 @@ +""" """ + +CATEGORY_PREFIXES: dict[str, str] = { + "ar": "تصنيف:", + "en": "Category:", + "www": "Category:", +} + + +__all__ = [ + "CATEGORY_PREFIXES", +] diff --git a/newapi/super/S_API/bot.py b/newapi/super/S_API/bot.py index 86ec79e..610587e 100644 --- a/newapi/super/S_API/bot.py +++ b/newapi/super/S_API/bot.py @@ -8,7 +8,7 @@ import sys from ...client_wiki.api_utils.ask_bot import AskBot -from ..handel_errors import HandelErrors +from ...client_wiki.api_utils.handel_errors import HandleErrors logger = logging.getLogger(__name__) @@ -16,7 +16,7 @@ file_name = "bot_api.py" -class BotsAPIS(HandelErrors, AskBot): +class BotsAPIS(HandleErrors, AskBot): def __init__(self): # print("class BotsAPIS:") # --- @@ -80,7 +80,7 @@ def Add_To_Bottom(self, text, summary, title, poss="Head|Bottom"): # --- if error != {}: print(results) - er = self.handel_err(error, function="Add_To_Bottom", params=params) + er = self.handle_err(error, function="Add_To_Bottom", params=params) # --- return er # --- diff --git a/newapi/super/S_API/bot_api.py b/newapi/super/S_API/bot_api.py index d0ef8fc..e4d3ac5 100644 --- a/newapi/super/S_API/bot_api.py +++ b/newapi/super/S_API/bot_api.py @@ -16,7 +16,7 @@ class NewApi(BotsAPIS): - def __init__(self, login_bot: WikiLoginClient, lang: str="", family: str="wikipedia"): + def __init__(self, login_bot: WikiLoginClient, lang: str = "", family: str = "wikipedia"): # --- self.login_bot = login_bot # --- diff --git a/newapi/super/S_Page/bot.py b/newapi/super/S_Page/bot.py index c6ea1f5..43a487f 100644 --- a/newapi/super/S_Page/bot.py +++ b/newapi/super/S_Page/bot.py @@ -4,10 +4,10 @@ """ -from ..handel_errors import HandelErrors +from ...client_wiki.api_utils.handel_errors import HandleErrors -class PageAPIS(HandelErrors): +class PageAPIS(HandleErrors): def __init__(self, login_bot): # print("class PageAPIS:") self.login_bot = login_bot diff --git a/newapi/super/S_Page/super_page.py b/newapi/super/S_Page/super_page.py index cea2842..cd3f764 100644 --- a/newapi/super/S_Page/super_page.py +++ b/newapi/super/S_Page/super_page.py @@ -742,7 +742,7 @@ def save( # --- if error != {}: print(pop) - er = self.handel_err(error, function="Save", params=params) + er = self.handle_err(error, function="Save", params=params) # --- return er # --- @@ -863,7 +863,7 @@ def create( # --- if error != {}: print(pop) - er = self.handel_err(error, function="Create", params=params) + er = self.handle_err(error, function="Create", params=params) # --- return er # --- diff --git a/newapi/super/__init__.py b/newapi/super/__init__.py index 7ab7abe..a811d1f 100644 --- a/newapi/super/__init__.py +++ b/newapi/super/__init__.py @@ -1,7 +1,7 @@ """ """ -from .S_API import bot_api from ..client_wiki.categories import catdepth_new +from .S_API import bot_api from .S_Page import super_page __all__ = [ diff --git a/newapi/utils/__init__.py b/newapi/utils/__init__.py new file mode 100644 index 0000000..8f88255 --- /dev/null +++ b/newapi/utils/__init__.py @@ -0,0 +1,6 @@ + +from .functions_timer import function_timer + +__all__ = [ + "function_timer", +] diff --git a/newapi/utils/functions_timer.py b/newapi/utils/functions_timer.py new file mode 100644 index 0000000..35d87ce --- /dev/null +++ b/newapi/utils/functions_timer.py @@ -0,0 +1,20 @@ +"""Timing decorator for profiling function execution.""" + +import functools +import logging +import time + +logger = logging.getLogger(__name__) + + +def function_timer(func): + """Log how long a function takes to run.""" + + @functools.wraps(func) + def wrapper(*args, **kwargs): + start = time.perf_counter() + result = func(*args, **kwargs) + logger.debug(f"{func.__name__} finished in {time.perf_counter() - start:.4f}s") + return result + + return wrapper From b1a319e0b8ca107810772e44953d7c375f854032 Mon Sep 17 00:00:00 2001 From: Ibrahem Date: Tue, 5 May 2026 02:38:48 +0300 Subject: [PATCH 07/16] . --- newapi/client_wiki/all_apis.py | 4 +- .../S_Page => client_wiki/pages}/__init__.py | 0 .../S_Page => client_wiki/pages}/ar_err.py | 8 +- .../S_Page => client_wiki/pages}/data.py | 4 +- newapi/super/S_Page/bot.py | 39 - newapi/super/S_Page/super_page.py | 1002 ----------------- newapi/super/__init__.py | 5 - newapi/super/cookies_bot.py | 40 - newapi/utils/__init__.py | 1 - tests/TestMainPage.py | 6 +- 10 files changed, 7 insertions(+), 1102 deletions(-) rename newapi/{super/S_Page => client_wiki/pages}/__init__.py (100%) rename newapi/{super/S_Page => client_wiki/pages}/ar_err.py (90%) rename newapi/{super/S_Page => client_wiki/pages}/data.py (94%) delete mode 100644 newapi/super/S_Page/bot.py delete mode 100644 newapi/super/S_Page/super_page.py diff --git a/newapi/client_wiki/all_apis.py b/newapi/client_wiki/all_apis.py index 2ab4f02..9f2b46f 100644 --- a/newapi/client_wiki/all_apis.py +++ b/newapi/client_wiki/all_apis.py @@ -3,9 +3,9 @@ import logging from ..api_client import WikiLoginClient -from .categories import catdepth_new from ..super.S_API import bot_api -from ..super.S_Page import super_page +from .categories import catdepth_new +from .pages import super_page logger = logging.getLogger(__name__) diff --git a/newapi/super/S_Page/__init__.py b/newapi/client_wiki/pages/__init__.py similarity index 100% rename from newapi/super/S_Page/__init__.py rename to newapi/client_wiki/pages/__init__.py diff --git a/newapi/super/S_Page/ar_err.py b/newapi/client_wiki/pages/ar_err.py similarity index 90% rename from newapi/super/S_Page/ar_err.py rename to newapi/client_wiki/pages/ar_err.py index 2c680c3..9b34575 100644 --- a/newapi/super/S_Page/ar_err.py +++ b/newapi/client_wiki/pages/ar_err.py @@ -1,10 +1,4 @@ -#!/usr/bin/python3 - -""" - -from .super.S_Page.ar_err import find_edit_error -if find_edit_error(old, new): return -""" +""" """ def find_edit_error(old, new): diff --git a/newapi/super/S_Page/data.py b/newapi/client_wiki/pages/data.py similarity index 94% rename from newapi/super/S_Page/data.py rename to newapi/client_wiki/pages/data.py index 88950d8..a0f33d9 100644 --- a/newapi/super/S_Page/data.py +++ b/newapi/client_wiki/pages/data.py @@ -2,7 +2,7 @@ from .data import Content, Meta, RevisionsData, LinksData, CategoriesData, TemplateData -(Exists|all_categories_with_hidden|back_links|can_be_edit|categories|create_data|extlinks|flagged|hidden_categories|info|is_Disambig|is_redirect|iwlinks|length|links_here|links|newrevid|pageid|revid|revisions|summary|templates|templates_API|text_html|timestamp|touched|userinfo|username|wikibase_item|words) +(Exists|all_categories_with_hidden|back_links|can_be_edit|categories|create_data|extlinks|flagged|hidden_categories|info|is_disambig|is_redirect|iwlinks|length|links_here|links|newrevid|pageid|revid|revisions|summary|templates|templates_API|text_html|timestamp|touched|userinfo|username|wikibase_item|words) """ @@ -19,7 +19,7 @@ class Content: @dataclass class Meta: - is_Disambig: bool = False + is_disambig: bool = False can_be_edit: bool = False # ns: int = 0 userinfo: dict = field(default_factory=dict) diff --git a/newapi/super/S_Page/bot.py b/newapi/super/S_Page/bot.py deleted file mode 100644 index 43a487f..0000000 --- a/newapi/super/S_Page/bot.py +++ /dev/null @@ -1,39 +0,0 @@ -""" - -from .super.S_Page.bot import PageAPIS - -""" - -from ...client_wiki.api_utils.handel_errors import HandleErrors - - -class PageAPIS(HandleErrors): - def __init__(self, login_bot): - # print("class PageAPIS:") - self.login_bot = login_bot - # --- - self.title = getattr(self, "title", "") - # --- - super().__init__() - - def post_continue( - self, - params, - action, - _p_="pages", - p_empty=None, - max=500000, - first=False, - _p_2="", - _p_2_empty=None, - ): - return self.login_bot.post_continue( - params, - action, - _p_=_p_, - p_empty=p_empty, - max=max, - first=first, - _p_2=_p_2, - _p_2_empty=_p_2_empty, - ) diff --git a/newapi/super/S_Page/super_page.py b/newapi/super/S_Page/super_page.py deleted file mode 100644 index cd3f764..0000000 --- a/newapi/super/S_Page/super_page.py +++ /dev/null @@ -1,1002 +0,0 @@ -""" """ - -import logging -from typing import Any, Dict, Optional, Union - -import wikitextparser as wtp - -from ...client_wiki.api_utils import botEdit, txtlib -from ...client_wiki.api_utils.ask_bot import AskBot -from ...client_wiki.api_utils.lang_codes import change_codes -from ...config import settings -from .ar_err import find_edit_error -from .bot import PageAPIS -from .data import CategoriesData, Content, LinksData, Meta, RevisionsData, TemplateData - -logger = logging.getLogger(__name__) - - -class MainPage(PageAPIS, AskBot): - """ - Main page class for interacting with MediaWiki pages. - - Provides methods for reading, editing, and managing wiki pages. - """ - - def __init__( - self, - login_bot: Any, - title: str, - lang: str = "", - family: str = "wikipedia", - ) -> None: - # print(f"class MainPage: {lang=}") - # --- - """ - Initializes a MainPage instance for interacting with a MediaWiki page. - - Sets up page attributes including title, language, family, API endpoint, and metadata fields. Normalizes the language code, loads user tables if available, and logs into the wiki if required. - """ - # --- - self.login_bot = login_bot - # --- - # --- - self.title: str = title - self.lang: str = change_codes.get(lang) or lang - self.family: str = family - self.endpoint: str = f"https://{self.lang}.{self.family}.org/w/api.php" - # --- - self.text: str = "" - self.newtext: str = "" - self.ns: Union[bool, int] = False - self.langlinks: Dict[str, str] = {} - # --- - self.meta = Meta() - self.content = Content() - self.revisions_data = RevisionsData() - self.links_data = LinksData() - self.categories_data = CategoriesData() - self.template_data = TemplateData() - # --- - self.user: str = "" - # --- - super().__init__(login_bot) - - def client_request( - self, - params: Dict[str, Any], - method: str = "get", - files: Optional[Dict[str, Any]] = None, - ) -> Dict[str, Any]: - # --- - return self.login_bot.client_request( - params, - method=method, - files=files, - ) - - def false_edit(self) -> bool: - # self.newtext - # self.text - # --- - """ - Determines if a proposed edit should be considered erroneous and aborted. - - Returns True if the edit is likely to be a false or destructive edit, such as removing over 90% of the page content or matching language-specific error conditions (e.g., for Arabic pages). Returns False if the edit is allowed or if the page is not in the main namespace. - """ - if self.ns is False or self.ns != 0: - return False - # --- - if settings.bot.no_fa: - return False - # --- - if not self.text: - self.text = self.get_text() - # --- - # If the new edit will remove 90% of the text, return False - if len(self.newtext) < 0.1 * len(self.text): - text_err = f"Edit will remove 90% of the text. {len(self.newtext)} < 0.1 * {len(self.text)}" - text_err += f"title: {self.title}, summary: {self.content.summary}" - logger.exception(text_err) - return True - # --- - if self.lang == "ar" and self.ns == 0: - if find_edit_error(self.text, self.newtext): - return True - # --- - return False - - def import_page(self, family="wikipedia"): - """ - Imports the page from another wiki family using the MediaWiki API. - - Args: - family: The source wiki family from which to import the page (default is "wikipedia"). - - Returns: - The API response data from the import operation. - """ - params = { - "action": "import", - "format": "json", - "interwikisource": family, - "interwikipage": self.title, - "fullhistory": 1, - "assignknownusers": 1, - } - # --- - data = self.client_request(params) - # --- - done = data.get("import", [{}])[0].get("revisions", 0) - # --- - logger.info(f"<> imported {done} revisions") - # --- - return data - - def find_create_data(self): - """ - Retrieves and stores the creation metadata of the page's first revision. - - Queries the MediaWiki API for the earliest revision of the page and extracts the creation timestamp, user, and anonymity status. The data is stored in the `create_data` attribute and returned as a dictionary. - - Returns: - dict: A dictionary containing 'timestamp', 'user', and 'anon' keys for the page's creation. - """ - params = { - "action": "query", - "format": "json", - "prop": "revisions", - "titles": self.title, - "rvprop": "timestamp|ids|user", - "rvslots": "*", - "rvlimit": "1", - "rvdir": "newer", - } - # --- - data = self.client_request(params) - # --- - pages = data.get("query", {}).get("pages", {}) - # --- - for _, v in pages.items(): - # --- - page_data = v.get("revisions", [{}])[0] - # --- - if "parentid" in page_data and page_data["parentid"] == 0: - self.meta.create_data = { - "timestamp": page_data["timestamp"], - "user": page_data.get("user", ""), - "anon": page_data.get("anon", False), - } - # --- - break - # --- - return self.meta.create_data - - def get_text(self, redirects=False): - """ - Retrieves the current wikitext content and metadata for the page. - - Fetches the latest revision's wikitext, user, revision ID, timestamp, namespace, Wikibase item, and flagged status. Updates instance attributes with the retrieved data. If the revision is the first (creation), stores creation metadata. Returns the page's wikitext content. - - Args: - redirects: If True, follows redirects to the target page. - - Returns: - The wikitext content of the page. - """ - params = { - "action": "query", - "prop": "revisions|pageprops|flagged", - "titles": self.title, - "ppprop": "wikibase_item", - "rvprop": "timestamp|content|user|ids", - "rvslots": "*", - } # pageprops # revisions # revisions - # --- - if redirects: - params["redirects"] = 1 - data = self.client_request(params) - # --- - # _dat_ = { "batchcomplete": "", "query": { "normalized": [{ "from": "وب:ملعب", "to": "ويكيبيديا:ملعب" }], "pages": { "361534": { "pageid": 361534, "ns": 4, "title": "ويكيبيديا:ملعب", "revisions": [{ "revid": 61421668, "parentid": 61421528, "user": "Al-shazali Sabeel", "timestamp": "2023-03-07T13:50:29Z", "slots": { "main": { "contentmodel": "wikitext", "contentformat": "text/x-wiki", "*": "{{عنوان الملعب}}" } } }], "pageprops": { "wikibase_item": "Q3938" } } } }, } - # --- - pages = data.get("query", {}).get("pages", {}) - # --- - for k, v in pages.items(): - # --- - if "ns" in v: - self.ns = v["ns"] # ns = 0 ! - # --- - if "missing" in v or k == "-1": - self.meta.Exists = False - # break - else: - self.meta.Exists = True - # --- - # title = v["title"] - # --- - pageprops = v.get("pageprops", {}) - self.meta.wikibase_item = pageprops.get("wikibase_item") or self.meta.wikibase_item - # --- - # "flagged": { "stable_revid": 61366100, "level": 0, "level_text": "stable"} - self.meta.flagged = v.get("flagged", False) is not False - # --- - self.revisions_data.pageid = v.get("pageid") or self.revisions_data.pageid - # --- - page_data = v.get("revisions", [{}])[0] - # --- - self.text = page_data.get("slots", {}).get("main", {}).get("*", "") - self.user = page_data.get("user") or self.user - self.revisions_data.revid = page_data.get("revid") or self.revisions_data.revid - # --- - self.revisions_data.timestamp = page_data.get("timestamp") or self.revisions_data.timestamp - # --- - if "parentid" in page_data and page_data["parentid"] == 0: - self.meta.create_data = { - "timestamp": page_data["timestamp"], - "user": page_data.get("user", ""), - "anon": page_data.get("anon", False), - } - # --- - break - # --- - return self.text - - def get_qid(self): - """Retrieve the QID from the wikibase item. - - This function checks if the `wikibase_item` attribute is set. If it is - not set, it calls the `get_text` method to populate the `wikibase_item`. - Finally, it returns the current value of `wikibase_item`. - - Returns: - str: The QID associated with the wikibase item. - """ - - if not self.meta.wikibase_item: - self.get_text() - return self.meta.wikibase_item - - def get_infos(self): - # --- - """ - Fetches and updates comprehensive metadata for the current page from the MediaWiki API. - - Retrieves and stores categories (including hidden), language links, templates, backlinks, interwiki links, and page information such as namespace, page ID, length, last revision ID, and last touched timestamp. Updates instance attributes with the fetched data for later access. - """ - params = { - "action": "query", - "titles": self.title, - "prop": "categories|langlinks|templates|linkshere|iwlinks|info", - "clprop": "sortkey|hidden", - "cllimit": "max", # categories - "lllimit": "max", # langlinks - "tllimit": "max", # templates - "lhlimit": "max", # linkshere - "iwlimit": "max", # iwlinks - "formatversion": "2", - # "normalize": 1, - "tlnamespace": "10", - } - # --- - # _data_ = { "continue": {}, "query": { "pages": { "9124097": { "pageid": 9124097, "ns": 0, "title": "طواف العالم للدراجات 2023", "categories": [], "langlinks": [], "templates": [{ "ns": 10, "title": "قالب:-" }], "linkshere": [{ "pageid": 189150, "ns": 0, "title": "طواف فرنسا" }], "iwlinks": [{ "prefix": "commons", "*": "Category:2023_UCI_World_Tour" }], "contentmodel": "wikitext", "pagelanguage": "ar", "pagelanguagehtmlcode": "ar", "pagelanguagedir": "rtl", "touched": "2023-03-07T11:53:53Z", "lastrevid": 61366100, "length": 985, } } }, } - # --- - data = self.client_request(params) - # --- - # xs = { 'batchcomplete': True, 'query': { 'pages': [{ 'pageid': 151314, 'ns': 10, 'title': 'قالب:أوب', 'categories': [{ 'ns': 14, 'title': 'تصنيف:قوالب تستخدم أنماط القوالب', 'sortkey': '', 'sortkeyprefix': '', 'hidden': False }, { 'ns': 14, 'title': 'تصنيف:cc', 'sortkey': 'v', 'sortkeyprefix': 'أوب', 'hidden': True }], 'langlinks': [{ 'lang': 'bh', 'title': 'टेम्पलेट:AWB' }], 'templates': [{ 'ns': 10, 'title': 'قالب:No redirect' }], 'linkshere': [{ 'pageid': 308641, 'ns': 10, 'title': 'قالب:AWB', 'redirect': True }], 'iwlinks': [{ 'prefix': 'd', 'title': 'Q4063270' }], 'contentmodel': 'wikitext', 'pagelanguage': 'ar', 'pagelanguagehtmlcode': 'ar', 'pagelanguagedir': 'rtl', 'touched': '2023-03-05T22:10:23Z', 'lastrevid': 61388266, 'length': 3477, }] }, } - # --- - ta = data.get("query", {}).get("pages", [{}])[0] - # --- - # for _, ta in pages.items(): - # --- - # self.ns = ta.get("ns") or self.ns - if "ns" in ta: - self.ns = ta["ns"] # ns = 0 ! - # --- - self.revisions_data.pageid = ta.get("pageid") or self.revisions_data.pageid - self.content.length = ta.get("length") or self.content.length - self.revisions_data.revid = ta.get("lastrevid") or self.revisions_data.revid - self.revisions_data.touched = ta.get("touched") or self.revisions_data.touched - # --- - self.meta.is_redirect = True if "redirect" in ta else False - # --- - for cat in ta.get("categories", []): - # --- - # _cat_ = { "ns": 14, "title": "تصنيف:بوابة سباق الدراجات الهوائية/مقالات متعلقة", "sortkey": "d8b7", "sortkeyprefix": "", "hidden": True } - # --- - if "sortkey" in cat: - del cat["sortkey"] - # --- - category_title = cat["title"] - # --- - self.categories_data.all_categories_with_hidden[category_title] = cat - # --- - if cat.get("hidden") is True: - self.categories_data.hidden_categories[category_title] = cat - else: - del cat["hidden"] - self.categories_data.categories[category_title] = cat - # --- - if ta.get("langlinks", []) != []: - # --- - # {"lang": "ca", "*": "UCI World Tour 2023"} or {'lang': 'bh', 'title': 'टेम्पलेट:AWB'} - # --- - self.langlinks = {ta["lang"]: ta.get("*") or ta.get("title") for ta in ta.get("langlinks", [])} - # --- - if ta.get("templates", []) != []: - # --- - # 'templates': [{'ns': 10, 'title': 'قالب:No redirect'}], - # --- - self.template_data.templates_API = [ta["title"] for ta in ta.get("templates", [])] - # --- - # "linkshere": [{"pageid": 189150,"ns": 0,"title": "طواف فرنسا"}, {"pageid": 308641,"ns": 10,"title": "قالب:AWB","redirect": ""}] - self.links_data.links_here = ta.get("linkshere", []) - # --- - self.links_data.iwlinks = ta.get("iwlinks", []) - # --- - self.meta.info["done"] = True - - def get_text_html(self): - params = { - "action": "parse", - "page": self.title, - "formatversion": "2", - "prop": "text", - } - # --- - data = self.client_request(params) - # --- - # _data_ = { 'warnings': { 'main': { 'warnings': 'Unrecognized parameter: bot.' } }, 'parse': { 'title': 'ويكيبيديا:ملعب', 'pageid': 361534, 'text': '' } } - # --- - self.content.text_html = data.get("parse", {}).get("text", "") - # --- - return self.content.text_html - - def get_redirect_target(self): - # --- - params = { - "action": "query", - "titles": self.title, - "prop": "info", - "redirects": 1, - } - # --- - data = self.client_request(params) - # --- - # _pages_ = { 'batchcomplete': '', 'query': { 'redirects': [{ 'from': 'Yemen', 'to': 'اليمن' }], 'pages': {}, 'normalized': [{ 'from': 'yemen', 'to': 'Yemen' }] } } - # --- - _redirects = {"from": "Yemen", "to": "اليمن"} - # --- - redirects = data.get("query", {}).get("redirects", [{}])[0] - # --- - to = redirects.get("to", "") - # --- - if to: - logger.info(f"<>Page:({self.title}) redirect to ({to})") - # --- - return to - - def get_words(self): - srlimit = "30" - params = { - "action": "query", - "list": "search", - "srsearch": self.title, - "srlimit": srlimit, - } - data = self.client_request(params) - # --- - if not data: - return 0 - # --- - search = data.get("query", {}).get("search", []) - # --- - for pag in search: - tit = pag["title"] - if tit == self.title: - count = pag["wordcount"] - self.content.words = count - break - # --- - return self.content.words - - def get_extlinks(self): - params = { - "action": "query", - "format": "json", - "prop": "extlinks", - "titles": self.title, - "formatversion": "2", - "utf8": 1, - "ellimit": "max", - } - # --- - links = [] - # --- - continue_params = {} - # --- - d = 0 - # --- - while continue_params != {} or d == 0: - # --- - d += 1 - # --- - if continue_params: - # params = {**params, **continue_params} - params.update(continue_params) - # --- - json1 = self.client_request(params) - # --- - continue_params = json1.get("continue", {}) - # --- - linkso = json1.get("query", {}).get("pages", [{}])[0].get("extlinks", []) - # --- - links.extend(linkso) - # --- - links = [x["url"] for x in links] - # --- - # remove duplicates - liste1 = sorted(set(links)) - # --- - self.links_data.extlinks = liste1 - return liste1 - - def get_userinfo(self): - if len(self.meta.userinfo) == 0: - params = { - "action": "query", - "format": "json", - "list": "users", - "formatversion": "2", - "usprop": "groups", - "ususers": self.user, - } - # --- - data = self.client_request(params) - # --- - # _userinfo_ = { "id": 229481, "name": "Mr. Ibrahem", "groups": ["editor", "reviewer", "rollbacker", "*", "user", "autoconfirmed"] } - # --- - ff = data.get("query", {}).get("users", [{}]) - # --- - if ff: - self.meta.userinfo = ff[0] - # --- - return self.meta.userinfo - - def isRedirect(self): - # --- - if not self.meta.is_redirect: - self.get_infos() - # --- - return self.meta.is_redirect - - def isDisambiguation(self): - # --- - # if the title ends with '(توضيح)' or '(disambiguation)' - self.meta.is_Disambig = self.title.endswith("(توضيح)") or self.title.endswith("(disambiguation)") - # --- - if self.meta.is_Disambig: - logger.info(f'<> page "{self.title}" is Disambiguation / توضيح') - # --- - return self.meta.is_Disambig - - def get_categories(self, with_hidden=False): - # --- - # if not self.categories_data.categories: self.get_infos() - if not self.meta.info["done"]: - self.get_infos() - # --- - if with_hidden: - return self.categories_data.all_categories_with_hidden - # --- - return self.categories_data.categories - - def get_hidden_categories(self): - # --- - if self.categories_data.categories == {} and self.categories_data.hidden_categories == {}: - self.get_infos() - # --- - return self.categories_data.hidden_categories - - def get_langlinks(self): - # --- - if not self.meta.info["done"]: - self.get_infos() - # --- - return self.langlinks - - def get_templates_API(self): - # --- - if not self.meta.info["done"]: - self.get_infos() - # --- - return self.template_data.templates_API - - def get_links_here(self): - # --- - if not self.meta.info["done"]: - self.get_infos() - # --- - return self.links_data.links_here - - def get_wiki_links_from_text(self): - if not self.text: - self.text = self.get_text() - # --- - parsed = wtp.parse(self.text) - wikilinks = parsed.wikilinks - # --- - # logger.info(f'wikilinks:{str(wikilinks)}') - # --- - # for x in wikilinks: - # print(x.title) - # --- - return wikilinks - - def Get_tags(self, tag=""): - if not self.text: - self.text = self.get_text() - # --- - self.text = self.text.replace("", '', 1) - # --- - parsed = wtp.parse(self.text) - tags = parsed.get_tags() - # --- - # logger.info(f'tags:{str(tags)}') - # --- - if not tag: - return tags - # --- - new_tags = [] - # --- - for x in tags: - if x.name == tag: - new_tags.append(x) - # --- - # return tags if tag == '' else [x for x in tags if x.name == tag] - # --- - return new_tags - - def can_edit(self, script="", delay=0): - # --- - if self.family != "wikipedia": - return True - # --- - if not self.text: - self.text = self.get_text() - # --- - self.meta.can_be_edit = botEdit.bot_May_Edit( - text=self.text, title_page=self.title, botjob=script, page=self, delay=delay - ) - # --- - return self.meta.can_be_edit - - def is_flagged(self): - # --- - """ - Returns whether the page is flagged for review or quality control. - - If the page text has not been loaded, it is retrieved before checking the flagged status. - - Returns: - bool: True if the page is flagged, False otherwise. - """ - if not self.text: - self.text = self.get_text() - # --- - return self.meta.flagged - - def get_create_data(self): - """ - Returns the page creation metadata, fetching it if not already loaded. - - The creation metadata includes the timestamp, user, and anonymity status of the first revision. - """ - if not self.meta.create_data: - self.find_create_data() - return self.meta.create_data - - def get_timestamp(self): - """ - Returns the timestamp of the latest revision of the page. - - If the timestamp is not already loaded, retrieves the page content to obtain it. - """ - if not self.revisions_data.timestamp: - self.get_text() - return self.revisions_data.timestamp - - def get_newrevid(self): - # newrevid is only populated on successful edit/create responses. - if not self.revisions_data.newrevid: - # Ensure we at least have the current revid loaded. - if not self.revisions_data.revid: - self.get_text() - # Fallback to current revid if no newrevid exists. - return self.revisions_data.newrevid or self.revisions_data.revid - - def get_revid(self): - if not self.revisions_data.revid: - self.get_text() - return self.revisions_data.revid - - def exists(self): - if not self.meta.Exists: - self.get_text() - if not self.meta.Exists: - logger.info(f'page "{self.title}" not exists in {self.lang}:{self.family}') - return self.meta.Exists - - def namespace(self): - if self.ns is False: - self.get_text() - return self.ns - - def get_user(self): - if not self.user: - self.get_text() - return self.user - - def get_templates(self) -> dict: - if not self.text: - self.text = self.get_text() - self.template_data.templates = txtlib.extract_templates_and_params(self.text) - return self.template_data.templates - - def save( - self, - newtext="", - summary="", - nocreate=1, - minor="0", - tags="", - nodiff=False, - ask=False, - ) -> bool | str: - """ - Saves new text to the page, updating its content and metadata. - - Prompts for confirmation and checks for invalid edits before submitting the change. Updates instance attributes with the latest revision and timestamps on success. - - Args: - newtext: The new wikitext to save to the page. - summary: Edit summary for the change. - nocreate: If 1 (default), prevents creating the page if it does not exist. - minor: Indicates if the edit should be marked as minor. - tags: Optional tags to associate with the edit. - nodiff: If True, skips showing a diff before saving. - ask: If True, prompts the user for confirmation before saving. - - Returns: - True if the edit was successful, False otherwise. - """ - # --- - self.newtext = newtext - if summary: - self.content.summary = summary - # --- - if self.false_edit(): - return False - # --- - message = f"Do you want to save this page? ({self.lang}:{self.title})" - # --- - user = self.meta.username - # --- - if ( - self.ask_put( - nodiff=nodiff, - newtext=newtext, - text=self.text, - message=message, - job="save", - username=user, - summary=self.content.summary, - ) - is False - ): - return False - # --- - params = { - "action": "edit", - "title": self.title, - "text": newtext, - "summary": self.content.summary, - "minor": minor, - "nocreate": nocreate, - } - # --- - if nocreate != 1: - del params["nocreate"] - # --- - if self.revisions_data.revid: - params["baserevid"] = self.revisions_data.revid - # --- - if tags: - params["tags"] = tags - # --- - # params['basetimestamp'] = self.revisions_data.timestamp - # --- - pop = self.client_request(params) - # --- - if not pop: - return False - # --- - error = pop.get("error", {}) - edit = pop.get("edit", {}) - result = edit.get("result", "") - # --- - # {'edit': {'result': 'Success', 'pageid': 5013, 'title': 'User:Mr. Ibrahem/sandbox', 'contentmodel': 'wikitext', 'oldrevid': 1336986, 'newrevid': 1343447, 'newtimestamp': '2023-04-01T23:14:07Z', 'watched': ''}} - # --- - if result.lower() == "success": - self.text = newtext - self.user = "" - logger.info(f"<> ** true .. [[{self.lang}:{self.family}:{self.title}]] ") - # logger.info('Done True...') - # --- - self.revisions_data.pageid = edit.get("pageid") or self.revisions_data.pageid - self.revisions_data.revid = edit.get("newrevid") or self.revisions_data.revid - self.revisions_data.newrevid = edit.get("newrevid") or self.revisions_data.newrevid - self.revisions_data.touched = edit.get("touched") or self.revisions_data.touched - self.revisions_data.timestamp = edit.get("newtimestamp") or self.revisions_data.timestamp - # --- - return True - # --- - if error != {}: - print(pop) - er = self.handle_err(error, function="Save", params=params) - # --- - return er - # --- - return False - - def purge(self): - # --- - params = { - "action": "purge", - "forcelinkupdate": 1, - "forcerecursivelinkupdate": 1, - "titles": self.title, - } - # --- - data = self.client_request(params) - # --- - if not data: - logger.info("<> ** purge error. ") - return False - # --- - title2 = self.title - # --- - # 'normalized': [{'from': 'وب:ملعب', 'to': 'ويكيبيديا:ملعب'}]} - # --- - for x in data.get("normalized", []): - # logger.info(f"normalized from {x['from']} to {x['to']}") - if x["from"] == self.title: - title2 = x["to"] - break - # --- - for t in data.get("purge", []): - # t = [{'ns': 4, 'title': 'ويكيبيديا:ملعب', 'purged': '', 'linkupdate': ''}] - ti = t["title"] - if title2 == ti and "purged" in t: - return True - if "missing" in t: - logger.info(f'page "{ti}" missing') - return "missing" - return False - - def create( - self, - text="", - summary="", - nodiff="", - noask=False, - ) -> bool: - # --- - """ - Creates a new page with the specified text and summary. - - If user confirmation is not suppressed, prompts the user before creating the page. Only succeeds if the page does not already exist. Updates instance attributes with the new page state on success. - - Args: - text: The wikitext content to use for the new page. - summary: Edit summary for the page creation. - nodiff: If set, disables showing a diff before confirmation. - noask: If True, skips the user confirmation prompt. - - Returns: - True if the page was created successfully, False otherwise or if the user aborts. - """ - self.newtext = text - # --- - if not noask: - # --- - message = f"Do you want to create this page? ({self.lang}:{self.title})" - # --- - user = self.meta.username - # --- - if ( - self.ask_put( - nodiff=nodiff, - newtext=text, - message=message, - job="create", - username=user, - summary=summary, - ) - is False - ): - return False - # --- - params = { - "action": "edit", - "title": self.title, - "text": text, - "summary": summary, - "notminor": 1, - "createonly": 1, - } - # --- - pop = self.client_request(params) - # --- - if not pop: - return False - # --- - error = pop.get("error", {}) - edit = pop.get("edit", {}) - result = edit.get("result", "") - # --- - if result.lower() == "success": - # --- - # {'edit': {'new': '', 'result': 'Success', 'pageid': 9090918, 'title': 'مستخدم:Mr. Ibrahem/test2024', 'contentmodel': 'wikitext', 'oldrevid': 0, 'newrevid': 61016221, 'newtimestamp': '2023-02-01T21:52:42Z'}} - # --- - self.text = text - # --- - logger.info(f"<> ** true .. [[{self.lang}:{self.family}:{self.title}]] ") - # logger.info('Done True... time.sleep() ') - # --- - self.revisions_data.pageid = edit.get("pageid") or self.revisions_data.pageid - self.revisions_data.revid = edit.get("newrevid") or self.revisions_data.revid - self.revisions_data.touched = edit.get("touched") or self.revisions_data.touched - self.revisions_data.newrevid = edit.get("newrevid") or self.revisions_data.newrevid - self.revisions_data.timestamp = edit.get("newtimestamp") or self.revisions_data.timestamp - # --- - return True - # --- - if error != {}: - print(pop) - er = self.handle_err(error, function="Create", params=params) - # --- - return er - # --- - return False - - def Create( - self, - text="", - summary="", - nodiff="", - noask=False, - ) -> bool: - return self.create(text=text, summary=summary, nodiff=nodiff, noask=noask) - - def page_backlinks(self, ns=0): - params = { - "action": "query", - "maxlag": "3", - # "prop": "info", - "generator": "backlinks", - # "redirects": 1, - # 'gblfilterredir': 'redirects', - "gbltitle": self.title, - "gblnamespace": ns, - "gbllimit": "max", - "formatversion": "2", - "gblredirect": 1, - } - # --- - # x = { 'batchcomplete': True, 'limits': { 'backlinks': 2500 }, 'query': { 'redirects': [{ 'from': 'فريدريش زيمرمان', 'to': 'فريدريش تسيمرمان' }], 'pages': [{ 'pageid': 2941285, 'ns': 0, 'title': 'فولفغانغ شويبله' }, { 'pageid': 4783977, 'ns': 0, 'title': 'وزارة الشؤون الرقمية والنقل' }, { 'pageid': 5218323, 'ns': 0, 'title': 'فريدريش تسيمرمان' }, { 'pageid': 6662649, 'ns': 0, 'title': 'غونتر كراوزه' }] } } - # --- - # data = self.client_request(params) - # pages = data.get("query", {}).get("pages", []) - # --- - pages = self.post_continue(params, "query", _p_="pages", p_empty=[]) - # --- - back_links = [x for x in pages if x["title"] != self.title] - # --- - self.links_data.back_links = back_links - # --- - return self.links_data.back_links - - def page_links(self) -> list: - """ - Get the links on the page. - Return: - list: A list of links on the page, where each link is represented as a dictionary containing its namespace, title, and existence status. - Example of returned data: - [ - {'ns': 14, 'title': 'تصنيف:مقالات بحاجة لشريط بوابات', 'exists': True}, - {'ns': 14, 'title': 'تصنيف:مقالات بحاجة لصندوق معلومات', 'exists': False} - ] - """ - params = { - "action": "parse", - "prop": "links", - "formatversion": "2", - "page": self.title, - } - # data = self.client_request(params) - # data = data.get('parse', {}).get('links', []) - # --- - data: list = self.post_continue(params, "parse", _p_="links", p_empty=[]) - # --- - # [{'ns': 14, 'title': 'تصنيف:مقالات بحاجة لشريط بوابات', 'exists': True}, {'ns': 14, 'title': 'تصنيف:مقالات بحاجة لصندوق معلومات', 'exists': False}] - # --- - self.links_data.links2 = data - # --- - return self.links_data.links2 - - def page_links_query(self, plnamespace="*"): - params = { - "action": "query", - "prop": "links", - "formatversion": "2", - "titles": self.title, - "plnamespace": plnamespace, - "pllimit": "max", - "converttitles": 1, - } - # data = self.client_request(params) - # data = data.get('query', {}).get('links', []) - # --- - data = self.post_continue(params, "query", _p_="links", p_empty=[]) - # --- - # [{'ns': 14, 'title': 'تصنيف:مقالات بحاجة لشريط بوابات', 'exists': True}, {'ns': 14, 'title': 'تصنيف:مقالات بحاجة لصندوق معلومات', 'exists': False}] - # --- - self.links_data.links = data - # --- - return self.links_data.links - - def get_revisions(self, rvprops=None) -> list: - # --- - rvprop = [ - "comment", - "timestamp", - "user", - # "content", - "ids", - ] - # --- - if rvprops: - for x in rvprops: - if x not in rvprop: - rvprop.append(x) - # --- - params = { - "action": "query", - "format": "json", - "prop": "revisions", - "titles": self.title, - "utf8": 1, - "formatversion": "2", - "rvdir": "newer", - "rvslots": "*", - "rvlimit": "max", - # "rvprop": "comment|timestamp|user|content|ids", - "rvprop": "|".join(rvprop), - } - # --- - _revisions = self.post_continue(params, "query", _p_="pages", p_empty=[]) - # --- - revisions = [] - # --- - for x in _revisions: - revisions.extend(x["revisions"]) - # --- - self.revisions_data.revisions = revisions - # --- - return revisions - - def __getitem__(self, key): - if key == "q": - return self.get_qid() - else: - raise # noqa: PLE0704 diff --git a/newapi/super/__init__.py b/newapi/super/__init__.py index a811d1f..ab273b8 100644 --- a/newapi/super/__init__.py +++ b/newapi/super/__init__.py @@ -1,12 +1,7 @@ """ """ -from ..client_wiki.categories import catdepth_new from .S_API import bot_api -from .S_Page import super_page __all__ = [ - "S_API", "bot_api", - "super_page", - "catdepth_new", ] diff --git a/newapi/super/cookies_bot.py b/newapi/super/cookies_bot.py index 5751ded..a48ff84 100644 --- a/newapi/super/cookies_bot.py +++ b/newapi/super/cookies_bot.py @@ -77,43 +77,3 @@ def get_file_name(lang, family, username) -> Path: del_cookies_file(file) # --- return file - - -def from_folder(lang, family, username): - # --- - file = get_file_name(lang, family, username) - # --- - cookies = False - # --- - if file.exists(): - # --- - if not file.stat().st_size: - return False - # --- - # check if file old is > 3 days - # --- - file_time = datetime.fromtimestamp(file.stat().st_mtime) - # --- - if datetime.now() - file_time > timedelta(days=3): - del_cookies_file(file) - return False - # --- - with open(file, "r", encoding="utf-8") as f: - cookies = f.read() - else: - file.touch() - os.chmod(str(file), statgroup) - # --- - return cookies - - -@functools.lru_cache(maxsize=128) -def get_cookies(lang, family, username): - # --- - cookies = from_folder(lang, family, username) - # --- - if not cookies: - logger.info(f" <> get_cookies: <> [[{lang}:{family}]] user:{username} <> not found") - return "make_new" - # --- - return cookies diff --git a/newapi/utils/__init__.py b/newapi/utils/__init__.py index 8f88255..92e4633 100644 --- a/newapi/utils/__init__.py +++ b/newapi/utils/__init__.py @@ -1,4 +1,3 @@ - from .functions_timer import function_timer __all__ = [ diff --git a/tests/TestMainPage.py b/tests/TestMainPage.py index dbba92b..1e250e0 100644 --- a/tests/TestMainPage.py +++ b/tests/TestMainPage.py @@ -1,7 +1,7 @@ from unittest.mock import MagicMock, patch import pytest -from newapi.super.S_Page.super_page import MainPage +from newapi.client_wiki.pages.super_page import MainPage class TestMainPage: @@ -69,7 +69,6 @@ def test_nonexistent_page(self, mock_login_bot): def test_empty_page_content(self): """Test page with empty content""" - pass def test_page_without_edit_permission(self, mock_login_bot): """Test page where user cannot edit""" @@ -86,9 +85,8 @@ def test_page_without_edit_permission(self, mock_login_bot): } } page = MainPage(mock_login_bot, "الصفحة الرئيسة", "ar") - with patch("newapi.super.S_Page.super_page.botEdit.bot_May_Edit", return_value=False): + with patch("newapi.client_wiki.pages.super_page.botEdit.bot_May_Edit", return_value=False): assert page.can_edit() is False def test_page_title_validation(self): """Test various page title formats""" - pass From b0c2399d7041e4d53fd39d9438f8a8bee23e95d3 Mon Sep 17 00:00:00 2001 From: Ibrahem Date: Tue, 5 May 2026 02:38:52 +0300 Subject: [PATCH 08/16] Create super_page.py --- newapi/client_wiki/pages/super_page.py | 1027 ++++++++++++++++++++++++ 1 file changed, 1027 insertions(+) create mode 100644 newapi/client_wiki/pages/super_page.py diff --git a/newapi/client_wiki/pages/super_page.py b/newapi/client_wiki/pages/super_page.py new file mode 100644 index 0000000..b51a314 --- /dev/null +++ b/newapi/client_wiki/pages/super_page.py @@ -0,0 +1,1027 @@ +""" """ + +import logging +from typing import Any, Dict, Optional, Union + +import wikitextparser as wtp + +from ...api_client import WikiLoginClient +from ...client_wiki.api_utils.handel_errors import HandleErrors +from ...config import settings +from ..api_utils import botEdit, txtlib +from ..api_utils.ask_bot import AskBot +from ..api_utils.lang_codes import change_codes +from .ar_err import find_edit_error +from .data import CategoriesData, Content, LinksData, Meta, RevisionsData, TemplateData + +logger = logging.getLogger(__name__) + + +class MainPage(HandleErrors, AskBot): + """ + Main page class for interacting with MediaWiki pages. + + Provides methods for reading, editing, and managing wiki pages. + """ + + def __init__( + self, + login_bot: WikiLoginClient, + title: str, + lang: str = "", + family: str = "wikipedia", + ) -> None: + # print(f"class MainPage: {lang=}") + # --- + """ + Initializes a MainPage instance for interacting with a MediaWiki page. + + Sets up page attributes including title, language, family, API endpoint, and metadata fields. Normalizes the language code, loads user tables if available, and logs into the wiki if required. + """ + # --- + self.login_bot = login_bot + # --- + # --- + self.title: str = title + self.lang: str = change_codes.get(lang) or lang + self.family: str = family + self.endpoint: str = f"https://{self.lang}.{self.family}.org/w/api.php" + # --- + self.text: str = "" + self.newtext: str = "" + self.ns: Union[bool, int] = False + self.langlinks: Dict[str, str] = {} + # --- + self.meta = Meta() + self.content = Content() + self.revisions_data = RevisionsData() + self.links_data = LinksData() + self.categories_data = CategoriesData() + self.template_data = TemplateData() + # --- + self.user: str = "" + # --- + super().__init__() + + def client_request( + self, + params: Dict[str, Any], + method: str = "get", + files: Optional[Dict[str, Any]] = None, + ) -> Dict[str, Any]: + # --- + return self.login_bot.client_request( + params, + method=method, + files=files, + ) + + def false_edit(self) -> bool: + # self.newtext + # self.text + # --- + """ + Determines if a proposed edit should be considered erroneous and aborted. + + Returns True if the edit is likely to be a false or destructive edit, such as removing over 90% of the page content or matching language-specific error conditions (e.g., for Arabic pages). Returns False if the edit is allowed or if the page is not in the main namespace. + """ + if self.ns is False or self.ns != 0: + return False + # --- + if settings.bot.no_fa: + return False + # --- + if not self.text: + self.text = self.get_text() + # --- + # If the new edit will remove 90% of the text, return False + if len(self.newtext) < 0.1 * len(self.text): + text_err = f"Edit will remove 90% of the text. {len(self.newtext)} < 0.1 * {len(self.text)}" + text_err += f"title: {self.title}, summary: {self.content.summary}" + logger.exception(text_err) + return True + # --- + if self.lang == "ar" and self.ns == 0: + if find_edit_error(self.text, self.newtext): + return True + # --- + return False + + def import_page(self, family="wikipedia"): + """ + Imports the page from another wiki family using the MediaWiki API. + + Args: + family: The source wiki family from which to import the page (default is "wikipedia"). + + Returns: + The API response data from the import operation. + """ + params = { + "action": "import", + "format": "json", + "interwikisource": family, + "interwikipage": self.title, + "fullhistory": 1, + "assignknownusers": 1, + } + # --- + data = self.client_request(params) + # --- + done = data.get("import", [{}])[0].get("revisions", 0) + # --- + logger.info(f"<> imported {done} revisions") + # --- + return data + + def find_create_data(self): + """ + Retrieves and stores the creation metadata of the page's first revision. + + Queries the MediaWiki API for the earliest revision of the page and extracts the creation timestamp, user, and anonymity status. The data is stored in the `create_data` attribute and returned as a dictionary. + + Returns: + dict: A dictionary containing 'timestamp', 'user', and 'anon' keys for the page's creation. + """ + params = { + "action": "query", + "format": "json", + "prop": "revisions", + "titles": self.title, + "rvprop": "timestamp|ids|user", + "rvslots": "*", + "rvlimit": "1", + "rvdir": "newer", + } + # --- + data = self.client_request(params) + # --- + pages = data.get("query", {}).get("pages", {}) + # --- + for _, v in pages.items(): + # --- + page_data = v.get("revisions", [{}])[0] + # --- + if "parentid" in page_data and page_data["parentid"] == 0: + self.meta.create_data = { + "timestamp": page_data["timestamp"], + "user": page_data.get("user", ""), + "anon": page_data.get("anon", False), + } + # --- + break + # --- + return self.meta.create_data + + def get_text(self, redirects=False): + """ + Retrieves the current wikitext content and metadata for the page. + + Fetches the latest revision's wikitext, user, revision ID, timestamp, namespace, Wikibase item, and flagged status. Updates instance attributes with the retrieved data. If the revision is the first (creation), stores creation metadata. Returns the page's wikitext content. + + Args: + redirects: If True, follows redirects to the target page. + + Returns: + The wikitext content of the page. + """ + params = { + "action": "query", + "prop": "revisions|pageprops|flagged", + "titles": self.title, + "ppprop": "wikibase_item", + "rvprop": "timestamp|content|user|ids", + "rvslots": "*", + } # pageprops # revisions # revisions + # --- + if redirects: + params["redirects"] = 1 + data = self.client_request(params) + # --- + # _dat_ = { "batchcomplete": "", "query": { "normalized": [{ "from": "وب:ملعب", "to": "ويكيبيديا:ملعب" }], "pages": { "361534": { "pageid": 361534, "ns": 4, "title": "ويكيبيديا:ملعب", "revisions": [{ "revid": 61421668, "parentid": 61421528, "user": "Al-shazali Sabeel", "timestamp": "2023-03-07T13:50:29Z", "slots": { "main": { "contentmodel": "wikitext", "contentformat": "text/x-wiki", "*": "{{عنوان الملعب}}" } } }], "pageprops": { "wikibase_item": "Q3938" } } } }, } + # --- + pages = data.get("query", {}).get("pages", {}) + # --- + for k, v in pages.items(): + # --- + if "ns" in v: + self.ns = v["ns"] # ns = 0 ! + # --- + if "missing" in v or k == "-1": + self.meta.Exists = False + # break + else: + self.meta.Exists = True + # --- + # title = v["title"] + # --- + pageprops = v.get("pageprops", {}) + self.meta.wikibase_item = pageprops.get("wikibase_item") or self.meta.wikibase_item + # --- + # "flagged": { "stable_revid": 61366100, "level": 0, "level_text": "stable"} + self.meta.flagged = v.get("flagged", False) is not False + # --- + self.revisions_data.pageid = v.get("pageid") or self.revisions_data.pageid + # --- + page_data = v.get("revisions", [{}])[0] + # --- + self.text = page_data.get("slots", {}).get("main", {}).get("*", "") + self.user = page_data.get("user") or self.user + self.revisions_data.revid = page_data.get("revid") or self.revisions_data.revid + # --- + self.revisions_data.timestamp = page_data.get("timestamp") or self.revisions_data.timestamp + # --- + if "parentid" in page_data and page_data["parentid"] == 0: + self.meta.create_data = { + "timestamp": page_data["timestamp"], + "user": page_data.get("user", ""), + "anon": page_data.get("anon", False), + } + # --- + break + # --- + return self.text + + def get_qid(self): + """Retrieve the QID from the wikibase item. + + This function checks if the `wikibase_item` attribute is set. If it is + not set, it calls the `get_text` method to populate the `wikibase_item`. + Finally, it returns the current value of `wikibase_item`. + + Returns: + str: The QID associated with the wikibase item. + """ + + if not self.meta.wikibase_item: + self.get_text() + return self.meta.wikibase_item + + def get_infos(self): + # --- + """ + Fetches and updates comprehensive metadata for the current page from the MediaWiki API. + + Retrieves and stores categories (including hidden), language links, templates, backlinks, interwiki links, and page information such as namespace, page ID, length, last revision ID, and last touched timestamp. Updates instance attributes with the fetched data for later access. + """ + params = { + "action": "query", + "titles": self.title, + "prop": "categories|langlinks|templates|linkshere|iwlinks|info", + "clprop": "sortkey|hidden", + "cllimit": "max", # categories + "lllimit": "max", # langlinks + "tllimit": "max", # templates + "lhlimit": "max", # linkshere + "iwlimit": "max", # iwlinks + "formatversion": "2", + # "normalize": 1, + "tlnamespace": "10", + } + # --- + # _data_ = { "continue": {}, "query": { "pages": { "9124097": { "pageid": 9124097, "ns": 0, "title": "طواف العالم للدراجات 2023", "categories": [], "langlinks": [], "templates": [{ "ns": 10, "title": "قالب:-" }], "linkshere": [{ "pageid": 189150, "ns": 0, "title": "طواف فرنسا" }], "iwlinks": [{ "prefix": "commons", "*": "Category:2023_UCI_World_Tour" }], "contentmodel": "wikitext", "pagelanguage": "ar", "pagelanguagehtmlcode": "ar", "pagelanguagedir": "rtl", "touched": "2023-03-07T11:53:53Z", "lastrevid": 61366100, "length": 985, } } }, } + # --- + data = self.client_request(params) + # --- + # xs = { 'batchcomplete': True, 'query': { 'pages': [{ 'pageid': 151314, 'ns': 10, 'title': 'قالب:أوب', 'categories': [{ 'ns': 14, 'title': 'تصنيف:قوالب تستخدم أنماط القوالب', 'sortkey': '', 'sortkeyprefix': '', 'hidden': False }, { 'ns': 14, 'title': 'تصنيف:cc', 'sortkey': 'v', 'sortkeyprefix': 'أوب', 'hidden': True }], 'langlinks': [{ 'lang': 'bh', 'title': 'टेम्पलेट:AWB' }], 'templates': [{ 'ns': 10, 'title': 'قالب:No redirect' }], 'linkshere': [{ 'pageid': 308641, 'ns': 10, 'title': 'قالب:AWB', 'redirect': True }], 'iwlinks': [{ 'prefix': 'd', 'title': 'Q4063270' }], 'contentmodel': 'wikitext', 'pagelanguage': 'ar', 'pagelanguagehtmlcode': 'ar', 'pagelanguagedir': 'rtl', 'touched': '2023-03-05T22:10:23Z', 'lastrevid': 61388266, 'length': 3477, }] }, } + # --- + ta = data.get("query", {}).get("pages", [{}])[0] + # --- + # for _, ta in pages.items(): + # --- + # self.ns = ta.get("ns") or self.ns + if "ns" in ta: + self.ns = ta["ns"] # ns = 0 ! + # --- + self.revisions_data.pageid = ta.get("pageid") or self.revisions_data.pageid + self.content.length = ta.get("length") or self.content.length + self.revisions_data.revid = ta.get("lastrevid") or self.revisions_data.revid + self.revisions_data.touched = ta.get("touched") or self.revisions_data.touched + # --- + self.meta.is_redirect = True if "redirect" in ta else False + # --- + for cat in ta.get("categories", []): + # --- + # _cat_ = { "ns": 14, "title": "تصنيف:بوابة سباق الدراجات الهوائية/مقالات متعلقة", "sortkey": "d8b7", "sortkeyprefix": "", "hidden": True } + # --- + if "sortkey" in cat: + del cat["sortkey"] + # --- + category_title = cat["title"] + # --- + self.categories_data.all_categories_with_hidden[category_title] = cat + # --- + if cat.get("hidden") is True: + self.categories_data.hidden_categories[category_title] = cat + else: + del cat["hidden"] + self.categories_data.categories[category_title] = cat + # --- + if ta.get("langlinks", []) != []: + # --- + # {"lang": "ca", "*": "UCI World Tour 2023"} or {'lang': 'bh', 'title': 'टेम्पलेट:AWB'} + # --- + self.langlinks = {ta["lang"]: ta.get("*") or ta.get("title") for ta in ta.get("langlinks", [])} + # --- + if ta.get("templates", []) != []: + # --- + # 'templates': [{'ns': 10, 'title': 'قالب:No redirect'}], + # --- + self.template_data.templates_API = [ta["title"] for ta in ta.get("templates", [])] + # --- + # "linkshere": [{"pageid": 189150,"ns": 0,"title": "طواف فرنسا"}, {"pageid": 308641,"ns": 10,"title": "قالب:AWB","redirect": ""}] + self.links_data.links_here = ta.get("linkshere", []) + # --- + self.links_data.iwlinks = ta.get("iwlinks", []) + # --- + self.meta.info["done"] = True + + def get_text_html(self): + params = { + "action": "parse", + "page": self.title, + "formatversion": "2", + "prop": "text", + } + # --- + data = self.client_request(params) + # --- + # _data_ = { 'warnings': { 'main': { 'warnings': 'Unrecognized parameter: bot.' } }, 'parse': { 'title': 'ويكيبيديا:ملعب', 'pageid': 361534, 'text': '' } } + # --- + self.content.text_html = data.get("parse", {}).get("text", "") + # --- + return self.content.text_html + + def get_redirect_target(self): + # --- + params = { + "action": "query", + "titles": self.title, + "prop": "info", + "redirects": 1, + } + # --- + data = self.client_request(params) + # --- + # _pages_ = { 'batchcomplete': '', 'query': { 'redirects': [{ 'from': 'Yemen', 'to': 'اليمن' }], 'pages': {}, 'normalized': [{ 'from': 'yemen', 'to': 'Yemen' }] } } + # --- + _redirects = {"from": "Yemen", "to": "اليمن"} + # --- + redirects = data.get("query", {}).get("redirects", [{}])[0] + # --- + to = redirects.get("to", "") + # --- + if to: + logger.info(f"<>Page:({self.title}) redirect to ({to})") + # --- + return to + + def get_words(self): + srlimit = "30" + params = { + "action": "query", + "list": "search", + "srsearch": self.title, + "srlimit": srlimit, + } + data = self.client_request(params) + # --- + if not data: + return 0 + # --- + search = data.get("query", {}).get("search", []) + # --- + for pag in search: + tit = pag["title"] + if tit == self.title: + count = pag["wordcount"] + self.content.words = count + break + # --- + return self.content.words + + def get_extlinks(self): + params = { + "action": "query", + "format": "json", + "prop": "extlinks", + "titles": self.title, + "formatversion": "2", + "utf8": 1, + "ellimit": "max", + } + # --- + links = [] + # --- + continue_params = {} + # --- + d = 0 + # --- + while continue_params != {} or d == 0: + # --- + d += 1 + # --- + if continue_params: + # params = {**params, **continue_params} + params.update(continue_params) + # --- + json1 = self.client_request(params) + # --- + continue_params = json1.get("continue", {}) + # --- + linkso = json1.get("query", {}).get("pages", [{}])[0].get("extlinks", []) + # --- + links.extend(linkso) + # --- + links = [x["url"] for x in links] + # --- + # remove duplicates + liste1 = sorted(set(links)) + # --- + self.links_data.extlinks = liste1 + return liste1 + + def get_userinfo(self): + if len(self.meta.userinfo) == 0: + params = { + "action": "query", + "format": "json", + "list": "users", + "formatversion": "2", + "usprop": "groups", + "ususers": self.user, + } + # --- + data = self.client_request(params) + # --- + # _userinfo_ = { "id": 229481, "name": "Mr. Ibrahem", "groups": ["editor", "reviewer", "rollbacker", "*", "user", "autoconfirmed"] } + # --- + ff = data.get("query", {}).get("users", [{}]) + # --- + if ff: + self.meta.userinfo = ff[0] + # --- + return self.meta.userinfo + + def isRedirect(self): + # --- + if not self.meta.is_redirect: + self.get_infos() + # --- + return self.meta.is_redirect + + def isDisambiguation(self): + # --- + # if the title ends with '(توضيح)' or '(disambiguation)' + self.meta.is_disambig = self.title.endswith("(توضيح)") or self.title.endswith("(disambiguation)") + # --- + if self.meta.is_disambig: + logger.info(f'<> page "{self.title}" is Disambiguation / توضيح') + # --- + return self.meta.is_disambig + + def get_categories(self, with_hidden=False): + # --- + # if not self.categories_data.categories: self.get_infos() + if not self.meta.info["done"]: + self.get_infos() + # --- + if with_hidden: + return self.categories_data.all_categories_with_hidden + # --- + return self.categories_data.categories + + def get_hidden_categories(self): + # --- + if self.categories_data.categories == {} and self.categories_data.hidden_categories == {}: + self.get_infos() + # --- + return self.categories_data.hidden_categories + + def get_langlinks(self): + # --- + if not self.meta.info["done"]: + self.get_infos() + # --- + return self.langlinks + + def get_templates_API(self): + # --- + if not self.meta.info["done"]: + self.get_infos() + # --- + return self.template_data.templates_API + + def get_links_here(self): + # --- + if not self.meta.info["done"]: + self.get_infos() + # --- + return self.links_data.links_here + + def get_wiki_links_from_text(self): + if not self.text: + self.text = self.get_text() + # --- + parsed = wtp.parse(self.text) + wikilinks = parsed.wikilinks + # --- + # logger.info(f'wikilinks:{str(wikilinks)}') + # --- + # for x in wikilinks: + # print(x.title) + # --- + return wikilinks + + def Get_tags(self, tag=""): + if not self.text: + self.text = self.get_text() + # --- + self.text = self.text.replace("", '', 1) + # --- + parsed = wtp.parse(self.text) + tags = parsed.get_tags() + # --- + # logger.info(f'tags:{str(tags)}') + # --- + if not tag: + return tags + # --- + new_tags = [] + # --- + for x in tags: + if x.name == tag: + new_tags.append(x) + # --- + # return tags if tag == '' else [x for x in tags if x.name == tag] + # --- + return new_tags + + def can_edit(self, script="", delay=0): + # --- + if self.family != "wikipedia": + return True + # --- + if not self.text: + self.text = self.get_text() + # --- + self.meta.can_be_edit = botEdit.bot_May_Edit( + text=self.text, title_page=self.title, botjob=script, page=self, delay=delay + ) + # --- + return self.meta.can_be_edit + + def is_flagged(self): + # --- + """ + Returns whether the page is flagged for review or quality control. + + If the page text has not been loaded, it is retrieved before checking the flagged status. + + Returns: + bool: True if the page is flagged, False otherwise. + """ + if not self.text: + self.text = self.get_text() + # --- + return self.meta.flagged + + def get_create_data(self): + """ + Returns the page creation metadata, fetching it if not already loaded. + + The creation metadata includes the timestamp, user, and anonymity status of the first revision. + """ + if not self.meta.create_data: + self.find_create_data() + return self.meta.create_data + + def get_timestamp(self): + """ + Returns the timestamp of the latest revision of the page. + + If the timestamp is not already loaded, retrieves the page content to obtain it. + """ + if not self.revisions_data.timestamp: + self.get_text() + return self.revisions_data.timestamp + + def get_newrevid(self): + # newrevid is only populated on successful edit/create responses. + if not self.revisions_data.newrevid: + # Ensure we at least have the current revid loaded. + if not self.revisions_data.revid: + self.get_text() + # Fallback to current revid if no newrevid exists. + return self.revisions_data.newrevid or self.revisions_data.revid + + def get_revid(self): + if not self.revisions_data.revid: + self.get_text() + return self.revisions_data.revid + + def exists(self): + if not self.meta.Exists: + self.get_text() + if not self.meta.Exists: + logger.info(f'page "{self.title}" not exists in {self.lang}:{self.family}') + return self.meta.Exists + + def namespace(self): + if self.ns is False: + self.get_text() + return self.ns + + def get_user(self): + if not self.user: + self.get_text() + return self.user + + def get_templates(self) -> dict: + if not self.text: + self.text = self.get_text() + self.template_data.templates = txtlib.extract_templates_and_params(self.text) + return self.template_data.templates + + def save( + self, + newtext="", + summary="", + nocreate=1, + minor="0", + tags="", + nodiff=False, + ask=False, + ) -> bool | str: + """ + Saves new text to the page, updating its content and metadata. + + Prompts for confirmation and checks for invalid edits before submitting the change. Updates instance attributes with the latest revision and timestamps on success. + + Args: + newtext: The new wikitext to save to the page. + summary: Edit summary for the change. + nocreate: If 1 (default), prevents creating the page if it does not exist. + minor: Indicates if the edit should be marked as minor. + tags: Optional tags to associate with the edit. + nodiff: If True, skips showing a diff before saving. + ask: If True, prompts the user for confirmation before saving. + + Returns: + True if the edit was successful, False otherwise. + """ + # --- + self.newtext = newtext + if summary: + self.content.summary = summary + # --- + if self.false_edit(): + return False + # --- + message = f"Do you want to save this page? ({self.lang}:{self.title})" + # --- + user = self.meta.username + # --- + if ( + self.ask_put( + nodiff=nodiff, + newtext=newtext, + text=self.text, + message=message, + job="save", + username=user, + summary=self.content.summary, + ) + is False + ): + return False + # --- + params = { + "action": "edit", + "title": self.title, + "text": newtext, + "summary": self.content.summary, + "minor": minor, + "nocreate": nocreate, + } + # --- + if nocreate != 1: + del params["nocreate"] + # --- + if self.revisions_data.revid: + params["baserevid"] = self.revisions_data.revid + # --- + if tags: + params["tags"] = tags + # --- + # params['basetimestamp'] = self.revisions_data.timestamp + # --- + pop = self.client_request(params) + # --- + if not pop: + return False + # --- + error = pop.get("error", {}) + edit = pop.get("edit", {}) + result = edit.get("result", "") + # --- + # {'edit': {'result': 'Success', 'pageid': 5013, 'title': 'User:Mr. Ibrahem/sandbox', 'contentmodel': 'wikitext', 'oldrevid': 1336986, 'newrevid': 1343447, 'newtimestamp': '2023-04-01T23:14:07Z', 'watched': ''}} + # --- + if result.lower() == "success": + self.text = newtext + self.user = "" + logger.info(f"<> ** true .. [[{self.lang}:{self.family}:{self.title}]] ") + # logger.info('Done True...') + # --- + self.revisions_data.pageid = edit.get("pageid") or self.revisions_data.pageid + self.revisions_data.revid = edit.get("newrevid") or self.revisions_data.revid + self.revisions_data.newrevid = edit.get("newrevid") or self.revisions_data.newrevid + self.revisions_data.touched = edit.get("touched") or self.revisions_data.touched + self.revisions_data.timestamp = edit.get("newtimestamp") or self.revisions_data.timestamp + # --- + return True + # --- + if error != {}: + print(pop) + er = self.handle_err(error, function="Save", params=params) + # --- + return er + # --- + return False + + def purge(self): + # --- + params = { + "action": "purge", + "forcelinkupdate": 1, + "forcerecursivelinkupdate": 1, + "titles": self.title, + } + # --- + data = self.client_request(params) + # --- + if not data: + logger.info("<> ** purge error. ") + return False + # --- + title2 = self.title + # --- + # 'normalized': [{'from': 'وب:ملعب', 'to': 'ويكيبيديا:ملعب'}]} + # --- + for x in data.get("normalized", []): + # logger.info(f"normalized from {x['from']} to {x['to']}") + if x["from"] == self.title: + title2 = x["to"] + break + # --- + for t in data.get("purge", []): + # t = [{'ns': 4, 'title': 'ويكيبيديا:ملعب', 'purged': '', 'linkupdate': ''}] + ti = t["title"] + if title2 == ti and "purged" in t: + return True + if "missing" in t: + logger.info(f'page "{ti}" missing') + return "missing" + return False + + def create( + self, + text="", + summary="", + nodiff="", + noask=False, + ) -> bool: + # --- + """ + Creates a new page with the specified text and summary. + + If user confirmation is not suppressed, prompts the user before creating the page. Only succeeds if the page does not already exist. Updates instance attributes with the new page state on success. + + Args: + text: The wikitext content to use for the new page. + summary: Edit summary for the page creation. + nodiff: If set, disables showing a diff before confirmation. + noask: If True, skips the user confirmation prompt. + + Returns: + True if the page was created successfully, False otherwise or if the user aborts. + """ + self.newtext = text + # --- + if not noask: + # --- + message = f"Do you want to create this page? ({self.lang}:{self.title})" + # --- + user = self.meta.username + # --- + if ( + self.ask_put( + nodiff=nodiff, + newtext=text, + message=message, + job="create", + username=user, + summary=summary, + ) + is False + ): + return False + # --- + params = { + "action": "edit", + "title": self.title, + "text": text, + "summary": summary, + "notminor": 1, + "createonly": 1, + } + # --- + pop = self.client_request(params) + # --- + if not pop: + return False + # --- + error = pop.get("error", {}) + edit = pop.get("edit", {}) + result = edit.get("result", "") + # --- + if result.lower() == "success": + # --- + # {'edit': {'new': '', 'result': 'Success', 'pageid': 9090918, 'title': 'مستخدم:Mr. Ibrahem/test2024', 'contentmodel': 'wikitext', 'oldrevid': 0, 'newrevid': 61016221, 'newtimestamp': '2023-02-01T21:52:42Z'}} + # --- + self.text = text + # --- + logger.info(f"<> ** true .. [[{self.lang}:{self.family}:{self.title}]] ") + # logger.info('Done True... time.sleep() ') + # --- + self.revisions_data.pageid = edit.get("pageid") or self.revisions_data.pageid + self.revisions_data.revid = edit.get("newrevid") or self.revisions_data.revid + self.revisions_data.touched = edit.get("touched") or self.revisions_data.touched + self.revisions_data.newrevid = edit.get("newrevid") or self.revisions_data.newrevid + self.revisions_data.timestamp = edit.get("newtimestamp") or self.revisions_data.timestamp + # --- + return True + # --- + if error != {}: + print(pop) + er = self.handle_err(error, function="Create", params=params) + # --- + return er + # --- + return False + + def Create( + self, + text="", + summary="", + nodiff="", + noask=False, + ) -> bool: + return self.create(text=text, summary=summary, nodiff=nodiff, noask=noask) + + def page_backlinks(self, ns=0): + params = { + "action": "query", + "maxlag": "3", + # "prop": "info", + "generator": "backlinks", + # "redirects": 1, + # 'gblfilterredir': 'redirects', + "gbltitle": self.title, + "gblnamespace": ns, + "gbllimit": "max", + "formatversion": "2", + "gblredirect": 1, + } + # --- + # x = { 'batchcomplete': True, 'limits': { 'backlinks': 2500 }, 'query': { 'redirects': [{ 'from': 'فريدريش زيمرمان', 'to': 'فريدريش تسيمرمان' }], 'pages': [{ 'pageid': 2941285, 'ns': 0, 'title': 'فولفغانغ شويبله' }, { 'pageid': 4783977, 'ns': 0, 'title': 'وزارة الشؤون الرقمية والنقل' }, { 'pageid': 5218323, 'ns': 0, 'title': 'فريدريش تسيمرمان' }, { 'pageid': 6662649, 'ns': 0, 'title': 'غونتر كراوزه' }] } } + # --- + # data = self.client_request(params) + # pages = data.get("query", {}).get("pages", []) + # --- + pages = self.post_continue(params, "query", _p_="pages", p_empty=[]) + # --- + back_links = [x for x in pages if x["title"] != self.title] + # --- + self.links_data.back_links = back_links + # --- + return self.links_data.back_links + + def page_links(self) -> list: + """ + Get the links on the page. + Return: + list: A list of links on the page, where each link is represented as a dictionary containing its namespace, title, and existence status. + Example of returned data: + [ + {'ns': 14, 'title': 'تصنيف:مقالات بحاجة لشريط بوابات', 'exists': True}, + {'ns': 14, 'title': 'تصنيف:مقالات بحاجة لصندوق معلومات', 'exists': False} + ] + """ + params = { + "action": "parse", + "prop": "links", + "formatversion": "2", + "page": self.title, + } + # data = self.client_request(params) + # data = data.get('parse', {}).get('links', []) + # --- + data: list = self.post_continue(params, "parse", _p_="links", p_empty=[]) + # --- + # [{'ns': 14, 'title': 'تصنيف:مقالات بحاجة لشريط بوابات', 'exists': True}, {'ns': 14, 'title': 'تصنيف:مقالات بحاجة لصندوق معلومات', 'exists': False}] + # --- + self.links_data.links2 = data + # --- + return self.links_data.links2 + + def page_links_query(self, plnamespace="*"): + params = { + "action": "query", + "prop": "links", + "formatversion": "2", + "titles": self.title, + "plnamespace": plnamespace, + "pllimit": "max", + "converttitles": 1, + } + # data = self.client_request(params) + # data = data.get('query', {}).get('links', []) + # --- + data = self.post_continue(params, "query", _p_="links", p_empty=[]) + # --- + # [{'ns': 14, 'title': 'تصنيف:مقالات بحاجة لشريط بوابات', 'exists': True}, {'ns': 14, 'title': 'تصنيف:مقالات بحاجة لصندوق معلومات', 'exists': False}] + # --- + self.links_data.links = data + # --- + return self.links_data.links + + def get_revisions(self, rvprops=None) -> list: + # --- + rvprop = [ + "comment", + "timestamp", + "user", + # "content", + "ids", + ] + # --- + if rvprops: + for x in rvprops: + if x not in rvprop: + rvprop.append(x) + # --- + params = { + "action": "query", + "format": "json", + "prop": "revisions", + "titles": self.title, + "utf8": 1, + "formatversion": "2", + "rvdir": "newer", + "rvslots": "*", + "rvlimit": "max", + # "rvprop": "comment|timestamp|user|content|ids", + "rvprop": "|".join(rvprop), + } + # --- + _revisions = self.post_continue(params, "query", _p_="pages", p_empty=[]) + # --- + revisions = [] + # --- + for x in _revisions: + revisions.extend(x["revisions"]) + # --- + self.revisions_data.revisions = revisions + # --- + return revisions + + def post_continue( + self, + params, + action, + _p_="pages", + p_empty=None, + max=500000, + first=False, + _p_2="", + _p_2_empty=None, + **kwargs, + ): + return self.login_bot.post_continue( + params, + action, + _p_=_p_, + p_empty=p_empty, + max=max, + first=first, + _p_2=_p_2, + _p_2_empty=_p_2_empty, + **kwargs, + ) + + def __getitem__(self, key): + if key == "q": + return self.get_qid() + else: + raise # noqa: PLE0704 From 3dc0bf2a0964d88c8efb0586b2438ca479cb823f Mon Sep 17 00:00:00 2001 From: Ibrahem Date: Tue, 5 May 2026 02:56:03 +0300 Subject: [PATCH 09/16] . --- newapi/client_wiki/api_utils/botEdit.py | 26 +++++++------------ .../api_utils/bot_edit/bot_edit_by_time.py | 14 +++++----- 2 files changed, 16 insertions(+), 24 deletions(-) diff --git a/newapi/client_wiki/api_utils/botEdit.py b/newapi/client_wiki/api_utils/botEdit.py index 9fc1915..f67a9da 100644 --- a/newapi/client_wiki/api_utils/botEdit.py +++ b/newapi/client_wiki/api_utils/botEdit.py @@ -1,12 +1,13 @@ -""" -from newapi import botEdit -bot_edit! -""" +""" """ + +import logging from .bot_edit.bot_edit_by_templates import is_bot_edit_allowed from .bot_edit.bot_edit_by_time import check_create_time, check_last_edit_time -Created_Cache = {} +_created_cache = {} + +logger = logging.getLogger(__name__) def bot_May_Edit( @@ -16,7 +17,6 @@ def bot_May_Edit( page=False, delay: int = 0, ) -> bool: - # --- """ Determines whether a bot is permitted to edit a page based on templates, last edit time, and creation time. @@ -33,29 +33,19 @@ def bot_May_Edit( True if the bot is allowed to edit the page; False otherwise. """ check_it = is_bot_edit_allowed(text=text, title_page=title_page, botjob=botjob) - # --- if page and check_it: - # --- if delay and isinstance(delay, int): - # --- ns = page.namespace() lang = page.lang - # --- if ns != 0 or lang != "ar": return check_it - # --- check_time = check_last_edit_time(page, title_page, delay) - # --- if not check_time: return False - # --- check_create = check_create_time(page, title_page) - # --- - Created_Cache[title_page] = check_create - # --- + _created_cache[title_page] = check_create if not check_create: return False - # --- return check_it @@ -65,4 +55,6 @@ def botMayEdit(**kwargs): __all__ = [ "bot_May_Edit", + "check_create_time", + "check_last_edit_time", ] diff --git a/newapi/client_wiki/api_utils/bot_edit/bot_edit_by_time.py b/newapi/client_wiki/api_utils/bot_edit/bot_edit_by_time.py index d8a613e..4d4949b 100644 --- a/newapi/client_wiki/api_utils/bot_edit/bot_edit_by_time.py +++ b/newapi/client_wiki/api_utils/bot_edit/bot_edit_by_time.py @@ -3,7 +3,7 @@ import datetime import logging -Created_Cache = {} +_created_cache = {} logger = logging.getLogger(__name__) @@ -16,8 +16,8 @@ def check_create_time(page, title_page): Returns True if the page is not in the Arabic main namespace or if the creation timestamp is missing. Returns False if the page was created less than three hours ago, caching the result for future checks. """ # --- - if title_page in Created_Cache: - return Created_Cache[title_page] + if title_page in _created_cache: + return _created_cache[title_page] # --- ns = page.namespace() lang = page.lang @@ -25,7 +25,7 @@ def check_create_time(page, title_page): if ns != 0 or lang != "ar": return True # --- - now = datetime.datetime.now(datetime.timezone.utc) + now = datetime.datetime.now(datetime.UTC) # --- create_data = page.get_create_data() # { "timestamp" : "2025-05-07T12:00:17Z", "user" : "", "anon" : "" } # --- @@ -34,7 +34,7 @@ def check_create_time(page, title_page): if create_data.get("timestamp"): # --- create_time = create_data["timestamp"] - ts_c_time = datetime.datetime.strptime(create_time, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.timezone.utc) + ts_c_time = datetime.datetime.strptime(create_time, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.UTC) # --- diff = (now - ts_c_time).total_seconds() / (60 * 60) # --- @@ -70,10 +70,10 @@ def check_last_edit_time(page, title_page, delay): # example: 2025-05-07T12:00:17Z timestamp = page.get_timestamp() # --- - now = datetime.datetime.now(datetime.timezone.utc) + now = datetime.datetime.now(datetime.UTC) # --- if timestamp: - ts_time = datetime.datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.timezone.utc) + ts_time = datetime.datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.UTC) # --- diff_minutes = (now - ts_time).total_seconds() / 60 # --- From 34c35289c1e76514fdd4c400c8e5a7fb0df6d406 Mon Sep 17 00:00:00 2001 From: Ibrahem Date: Tue, 5 May 2026 02:59:00 +0300 Subject: [PATCH 10/16] Update botEdit.py --- newapi/client_wiki/api_utils/botEdit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newapi/client_wiki/api_utils/botEdit.py b/newapi/client_wiki/api_utils/botEdit.py index f67a9da..332000a 100644 --- a/newapi/client_wiki/api_utils/botEdit.py +++ b/newapi/client_wiki/api_utils/botEdit.py @@ -1,7 +1,7 @@ """ """ - import logging + from .bot_edit.bot_edit_by_templates import is_bot_edit_allowed from .bot_edit.bot_edit_by_time import check_create_time, check_last_edit_time From 23ed5d08c804e672fb0d849abbc6dfb91da5eb6e Mon Sep 17 00:00:00 2001 From: Ibrahem Date: Tue, 5 May 2026 03:04:08 +0300 Subject: [PATCH 11/16] . --- newapi/client_wiki/api_utils/botEdit.py | 9 +- newapi/client_wiki/pages/ar_err.py | 35 -- newapi/client_wiki/pages/super_page.py | 416 ++++++++++++------------ 3 files changed, 216 insertions(+), 244 deletions(-) diff --git a/newapi/client_wiki/api_utils/botEdit.py b/newapi/client_wiki/api_utils/botEdit.py index 332000a..16ab34d 100644 --- a/newapi/client_wiki/api_utils/botEdit.py +++ b/newapi/client_wiki/api_utils/botEdit.py @@ -5,8 +5,6 @@ from .bot_edit.bot_edit_by_templates import is_bot_edit_allowed from .bot_edit.bot_edit_by_time import check_create_time, check_last_edit_time -_created_cache = {} - logger = logging.getLogger(__name__) @@ -33,19 +31,24 @@ def bot_May_Edit( True if the bot is allowed to edit the page; False otherwise. """ check_it = is_bot_edit_allowed(text=text, title_page=title_page, botjob=botjob) + if page and check_it: if delay and isinstance(delay, int): ns = page.namespace() lang = page.lang + if ns != 0 or lang != "ar": return check_it + check_time = check_last_edit_time(page, title_page, delay) if not check_time: return False + check_create = check_create_time(page, title_page) - _created_cache[title_page] = check_create + if not check_create: return False + return check_it diff --git a/newapi/client_wiki/pages/ar_err.py b/newapi/client_wiki/pages/ar_err.py index 9b34575..bc68a39 100644 --- a/newapi/client_wiki/pages/ar_err.py +++ b/newapi/client_wiki/pages/ar_err.py @@ -1,36 +1 @@ """ """ - - -def find_edit_error(old, new): - # Define the dictionary of conversion phrases - conversion_phrases = { - "#تحويل [[", - } - for phrase in conversion_phrases: - if phrase in old and phrase not in new: - print(f"ar_err.py found ({phrase}) in old but not in new. return True") - return True - - return False - - -def test_find_edit_error(): - # Test case 1: Phrase in old but not in new - old_text = "#تحويل [[قاعدة قانونية]]" - new_text = "[[تصنيف:أخلاقيات قانونية]]" - result = find_edit_error(old_text, new_text) - print(f"Test case 1: Result = {result}") - - # Test case 2: Phrase in both old and new - old_text = "This is an #تحويل [[ example." - new_text = "This is an #تحويل [[ example." - result = find_edit_error(old_text, new_text) - print(f"Test case 2: Result = {result}") - - # Test case 3: Phrase not in old or new - old_text = "This is an example." - new_text = "This is another example." - result = find_edit_error(old_text, new_text) - print(f"Test case 3: Result = {result}") - - print("All test cases pass!") diff --git a/newapi/client_wiki/pages/super_page.py b/newapi/client_wiki/pages/super_page.py index b51a314..df825f8 100644 --- a/newapi/client_wiki/pages/super_page.py +++ b/newapi/client_wiki/pages/super_page.py @@ -11,12 +11,24 @@ from ..api_utils import botEdit, txtlib from ..api_utils.ask_bot import AskBot from ..api_utils.lang_codes import change_codes -from .ar_err import find_edit_error from .data import CategoriesData, Content, LinksData, Meta, RevisionsData, TemplateData logger = logging.getLogger(__name__) +def find_edit_error(old, new): + # Define the dictionary of conversion phrases + conversion_phrases = { + "#تحويل [[", + } + for phrase in conversion_phrases: + if phrase in old and phrase not in new: + logger.info(f"ar_err.py found ({phrase}) in old but not in new. return True") + return True + + return False + + class MainPage(HandleErrors, AskBot): """ Main page class for interacting with MediaWiki pages. @@ -32,35 +44,33 @@ def __init__( family: str = "wikipedia", ) -> None: # print(f"class MainPage: {lang=}") - # --- """ Initializes a MainPage instance for interacting with a MediaWiki page. Sets up page attributes including title, language, family, API endpoint, and metadata fields. Normalizes the language code, loads user tables if available, and logs into the wiki if required. """ - # --- + self.login_bot = login_bot - # --- - # --- + self.title: str = title self.lang: str = change_codes.get(lang) or lang self.family: str = family self.endpoint: str = f"https://{self.lang}.{self.family}.org/w/api.php" - # --- + self.text: str = "" self.newtext: str = "" self.ns: Union[bool, int] = False self.langlinks: Dict[str, str] = {} - # --- + self.meta = Meta() self.content = Content() self.revisions_data = RevisionsData() self.links_data = LinksData() self.categories_data = CategoriesData() self.template_data = TemplateData() - # --- + self.user: str = "" - # --- + super().__init__() def client_request( @@ -68,18 +78,19 @@ def client_request( params: Dict[str, Any], method: str = "get", files: Optional[Dict[str, Any]] = None, + **kwargs, ) -> Dict[str, Any]: - # --- + return self.login_bot.client_request( params, method=method, files=files, + **kwargs, ) def false_edit(self) -> bool: # self.newtext # self.text - # --- """ Determines if a proposed edit should be considered erroneous and aborted. @@ -87,24 +98,24 @@ def false_edit(self) -> bool: """ if self.ns is False or self.ns != 0: return False - # --- + if settings.bot.no_fa: return False - # --- + if not self.text: self.text = self.get_text() - # --- + # If the new edit will remove 90% of the text, return False if len(self.newtext) < 0.1 * len(self.text): text_err = f"Edit will remove 90% of the text. {len(self.newtext)} < 0.1 * {len(self.text)}" text_err += f"title: {self.title}, summary: {self.content.summary}" logger.exception(text_err) return True - # --- + if self.lang == "ar" and self.ns == 0: if find_edit_error(self.text, self.newtext): return True - # --- + return False def import_page(self, family="wikipedia"): @@ -125,13 +136,13 @@ def import_page(self, family="wikipedia"): "fullhistory": 1, "assignknownusers": 1, } - # --- + data = self.client_request(params) - # --- + done = data.get("import", [{}])[0].get("revisions", 0) - # --- + logger.info(f"<> imported {done} revisions") - # --- + return data def find_create_data(self): @@ -153,24 +164,23 @@ def find_create_data(self): "rvlimit": "1", "rvdir": "newer", } - # --- - data = self.client_request(params) - # --- + + data = self.login_bot.client_request(params, method="get") + pages = data.get("query", {}).get("pages", {}) - # --- + for _, v in pages.items(): - # --- page_data = v.get("revisions", [{}])[0] - # --- + if "parentid" in page_data and page_data["parentid"] == 0: self.meta.create_data = { "timestamp": page_data["timestamp"], "user": page_data.get("user", ""), "anon": page_data.get("anon", False), } - # --- + break - # --- + return self.meta.create_data def get_text(self, redirects=False): @@ -193,53 +203,50 @@ def get_text(self, redirects=False): "rvprop": "timestamp|content|user|ids", "rvslots": "*", } # pageprops # revisions # revisions - # --- + if redirects: params["redirects"] = 1 - data = self.client_request(params) - # --- - # _dat_ = { "batchcomplete": "", "query": { "normalized": [{ "from": "وب:ملعب", "to": "ويكيبيديا:ملعب" }], "pages": { "361534": { "pageid": 361534, "ns": 4, "title": "ويكيبيديا:ملعب", "revisions": [{ "revid": 61421668, "parentid": 61421528, "user": "Al-shazali Sabeel", "timestamp": "2023-03-07T13:50:29Z", "slots": { "main": { "contentmodel": "wikitext", "contentformat": "text/x-wiki", "*": "{{عنوان الملعب}}" } } }], "pageprops": { "wikibase_item": "Q3938" } } } }, } - # --- + data = self.login_bot.client_request(params, method="get") + pages = data.get("query", {}).get("pages", {}) - # --- + for k, v in pages.items(): - # --- if "ns" in v: self.ns = v["ns"] # ns = 0 ! - # --- + if "missing" in v or k == "-1": self.meta.Exists = False # break else: self.meta.Exists = True - # --- + # title = v["title"] - # --- + pageprops = v.get("pageprops", {}) self.meta.wikibase_item = pageprops.get("wikibase_item") or self.meta.wikibase_item - # --- + # "flagged": { "stable_revid": 61366100, "level": 0, "level_text": "stable"} self.meta.flagged = v.get("flagged", False) is not False - # --- + self.revisions_data.pageid = v.get("pageid") or self.revisions_data.pageid - # --- + page_data = v.get("revisions", [{}])[0] - # --- + self.text = page_data.get("slots", {}).get("main", {}).get("*", "") self.user = page_data.get("user") or self.user self.revisions_data.revid = page_data.get("revid") or self.revisions_data.revid - # --- + self.revisions_data.timestamp = page_data.get("timestamp") or self.revisions_data.timestamp - # --- + if "parentid" in page_data and page_data["parentid"] == 0: self.meta.create_data = { "timestamp": page_data["timestamp"], "user": page_data.get("user", ""), "anon": page_data.get("anon", False), } - # --- + break - # --- + return self.text def get_qid(self): @@ -258,7 +265,6 @@ def get_qid(self): return self.meta.wikibase_item def get_infos(self): - # --- """ Fetches and updates comprehensive metadata for the current page from the MediaWiki API. @@ -278,62 +284,62 @@ def get_infos(self): # "normalize": 1, "tlnamespace": "10", } - # --- + # _data_ = { "continue": {}, "query": { "pages": { "9124097": { "pageid": 9124097, "ns": 0, "title": "طواف العالم للدراجات 2023", "categories": [], "langlinks": [], "templates": [{ "ns": 10, "title": "قالب:-" }], "linkshere": [{ "pageid": 189150, "ns": 0, "title": "طواف فرنسا" }], "iwlinks": [{ "prefix": "commons", "*": "Category:2023_UCI_World_Tour" }], "contentmodel": "wikitext", "pagelanguage": "ar", "pagelanguagehtmlcode": "ar", "pagelanguagedir": "rtl", "touched": "2023-03-07T11:53:53Z", "lastrevid": 61366100, "length": 985, } } }, } - # --- - data = self.client_request(params) - # --- + + data = self.login_bot.client_request(params, method="get") + # xs = { 'batchcomplete': True, 'query': { 'pages': [{ 'pageid': 151314, 'ns': 10, 'title': 'قالب:أوب', 'categories': [{ 'ns': 14, 'title': 'تصنيف:قوالب تستخدم أنماط القوالب', 'sortkey': '', 'sortkeyprefix': '', 'hidden': False }, { 'ns': 14, 'title': 'تصنيف:cc', 'sortkey': 'v', 'sortkeyprefix': 'أوب', 'hidden': True }], 'langlinks': [{ 'lang': 'bh', 'title': 'टेम्पलेट:AWB' }], 'templates': [{ 'ns': 10, 'title': 'قالب:No redirect' }], 'linkshere': [{ 'pageid': 308641, 'ns': 10, 'title': 'قالب:AWB', 'redirect': True }], 'iwlinks': [{ 'prefix': 'd', 'title': 'Q4063270' }], 'contentmodel': 'wikitext', 'pagelanguage': 'ar', 'pagelanguagehtmlcode': 'ar', 'pagelanguagedir': 'rtl', 'touched': '2023-03-05T22:10:23Z', 'lastrevid': 61388266, 'length': 3477, }] }, } - # --- + ta = data.get("query", {}).get("pages", [{}])[0] - # --- + # for _, ta in pages.items(): - # --- + # self.ns = ta.get("ns") or self.ns if "ns" in ta: self.ns = ta["ns"] # ns = 0 ! - # --- + self.revisions_data.pageid = ta.get("pageid") or self.revisions_data.pageid self.content.length = ta.get("length") or self.content.length self.revisions_data.revid = ta.get("lastrevid") or self.revisions_data.revid self.revisions_data.touched = ta.get("touched") or self.revisions_data.touched - # --- + self.meta.is_redirect = True if "redirect" in ta else False - # --- + for cat in ta.get("categories", []): - # --- + # _cat_ = { "ns": 14, "title": "تصنيف:بوابة سباق الدراجات الهوائية/مقالات متعلقة", "sortkey": "d8b7", "sortkeyprefix": "", "hidden": True } - # --- + if "sortkey" in cat: del cat["sortkey"] - # --- + category_title = cat["title"] - # --- + self.categories_data.all_categories_with_hidden[category_title] = cat - # --- + if cat.get("hidden") is True: self.categories_data.hidden_categories[category_title] = cat else: del cat["hidden"] self.categories_data.categories[category_title] = cat - # --- + if ta.get("langlinks", []) != []: - # --- + # {"lang": "ca", "*": "UCI World Tour 2023"} or {'lang': 'bh', 'title': 'टेम्पलेट:AWB'} - # --- + self.langlinks = {ta["lang"]: ta.get("*") or ta.get("title") for ta in ta.get("langlinks", [])} - # --- + if ta.get("templates", []) != []: - # --- + # 'templates': [{'ns': 10, 'title': 'قالب:No redirect'}], - # --- + self.template_data.templates_API = [ta["title"] for ta in ta.get("templates", [])] - # --- + # "linkshere": [{"pageid": 189150,"ns": 0,"title": "طواف فرنسا"}, {"pageid": 308641,"ns": 10,"title": "قالب:AWB","redirect": ""}] self.links_data.links_here = ta.get("linkshere", []) - # --- + self.links_data.iwlinks = ta.get("iwlinks", []) - # --- + self.meta.info["done"] = True def get_text_html(self): @@ -343,37 +349,37 @@ def get_text_html(self): "formatversion": "2", "prop": "text", } - # --- + data = self.client_request(params) - # --- + # _data_ = { 'warnings': { 'main': { 'warnings': 'Unrecognized parameter: bot.' } }, 'parse': { 'title': 'ويكيبيديا:ملعب', 'pageid': 361534, 'text': '' } } - # --- + self.content.text_html = data.get("parse", {}).get("text", "") - # --- + return self.content.text_html def get_redirect_target(self): - # --- + params = { "action": "query", "titles": self.title, "prop": "info", "redirects": 1, } - # --- + data = self.client_request(params) - # --- + # _pages_ = { 'batchcomplete': '', 'query': { 'redirects': [{ 'from': 'Yemen', 'to': 'اليمن' }], 'pages': {}, 'normalized': [{ 'from': 'yemen', 'to': 'Yemen' }] } } - # --- + _redirects = {"from": "Yemen", "to": "اليمن"} - # --- + redirects = data.get("query", {}).get("redirects", [{}])[0] - # --- + to = redirects.get("to", "") - # --- + if to: logger.info(f"<>Page:({self.title}) redirect to ({to})") - # --- + return to def get_words(self): @@ -385,19 +391,19 @@ def get_words(self): "srlimit": srlimit, } data = self.client_request(params) - # --- + if not data: return 0 - # --- + search = data.get("query", {}).get("search", []) - # --- + for pag in search: tit = pag["title"] if tit == self.title: count = pag["wordcount"] self.content.words = count break - # --- + return self.content.words def get_extlinks(self): @@ -410,34 +416,34 @@ def get_extlinks(self): "utf8": 1, "ellimit": "max", } - # --- + links = [] - # --- + continue_params = {} - # --- + d = 0 - # --- + while continue_params != {} or d == 0: - # --- + d += 1 - # --- + if continue_params: # params = {**params, **continue_params} params.update(continue_params) - # --- + json1 = self.client_request(params) - # --- + continue_params = json1.get("continue", {}) - # --- + linkso = json1.get("query", {}).get("pages", [{}])[0].get("extlinks", []) - # --- + links.extend(linkso) - # --- + links = [x["url"] for x in links] - # --- + # remove duplicates liste1 = sorted(set(links)) - # --- + self.links_data.extlinks = liste1 return liste1 @@ -451,128 +457,127 @@ def get_userinfo(self): "usprop": "groups", "ususers": self.user, } - # --- + data = self.client_request(params) - # --- + # _userinfo_ = { "id": 229481, "name": "Mr. Ibrahem", "groups": ["editor", "reviewer", "rollbacker", "*", "user", "autoconfirmed"] } - # --- + ff = data.get("query", {}).get("users", [{}]) - # --- + if ff: self.meta.userinfo = ff[0] - # --- + return self.meta.userinfo def isRedirect(self): - # --- + if not self.meta.is_redirect: self.get_infos() - # --- + return self.meta.is_redirect def isDisambiguation(self): - # --- + # if the title ends with '(توضيح)' or '(disambiguation)' self.meta.is_disambig = self.title.endswith("(توضيح)") or self.title.endswith("(disambiguation)") - # --- + if self.meta.is_disambig: logger.info(f'<> page "{self.title}" is Disambiguation / توضيح') - # --- + return self.meta.is_disambig def get_categories(self, with_hidden=False): - # --- + # if not self.categories_data.categories: self.get_infos() if not self.meta.info["done"]: self.get_infos() - # --- + if with_hidden: return self.categories_data.all_categories_with_hidden - # --- + return self.categories_data.categories def get_hidden_categories(self): - # --- + if self.categories_data.categories == {} and self.categories_data.hidden_categories == {}: self.get_infos() - # --- + return self.categories_data.hidden_categories def get_langlinks(self): - # --- + if not self.meta.info["done"]: self.get_infos() - # --- + return self.langlinks def get_templates_API(self): - # --- + if not self.meta.info["done"]: self.get_infos() - # --- + return self.template_data.templates_API def get_links_here(self): - # --- + if not self.meta.info["done"]: self.get_infos() - # --- + return self.links_data.links_here def get_wiki_links_from_text(self): if not self.text: self.text = self.get_text() - # --- + parsed = wtp.parse(self.text) wikilinks = parsed.wikilinks - # --- + # logger.info(f'wikilinks:{str(wikilinks)}') - # --- + # for x in wikilinks: # print(x.title) - # --- + return wikilinks def Get_tags(self, tag=""): if not self.text: self.text = self.get_text() - # --- + self.text = self.text.replace("", '', 1) - # --- + parsed = wtp.parse(self.text) tags = parsed.get_tags() - # --- + # logger.info(f'tags:{str(tags)}') - # --- + if not tag: return tags - # --- + new_tags = [] - # --- + for x in tags: if x.name == tag: new_tags.append(x) - # --- + # return tags if tag == '' else [x for x in tags if x.name == tag] - # --- + return new_tags def can_edit(self, script="", delay=0): - # --- + if self.family != "wikipedia": return True - # --- + if not self.text: self.text = self.get_text() - # --- + self.meta.can_be_edit = botEdit.bot_May_Edit( text=self.text, title_page=self.title, botjob=script, page=self, delay=delay ) - # --- + return self.meta.can_be_edit def is_flagged(self): - # --- """ Returns whether the page is flagged for review or quality control. @@ -583,7 +588,7 @@ def is_flagged(self): """ if not self.text: self.text = self.get_text() - # --- + return self.meta.flagged def get_create_data(self): @@ -670,18 +675,18 @@ def save( Returns: True if the edit was successful, False otherwise. """ - # --- + self.newtext = newtext if summary: self.content.summary = summary - # --- + if self.false_edit(): return False - # --- + message = f"Do you want to save this page? ({self.lang}:{self.title})" - # --- + user = self.meta.username - # --- + if ( self.ask_put( nodiff=nodiff, @@ -695,7 +700,7 @@ def save( is False ): return False - # --- + params = { "action": "edit", "title": self.title, @@ -704,76 +709,76 @@ def save( "minor": minor, "nocreate": nocreate, } - # --- + if nocreate != 1: del params["nocreate"] - # --- + if self.revisions_data.revid: params["baserevid"] = self.revisions_data.revid - # --- + if tags: params["tags"] = tags - # --- + # params['basetimestamp'] = self.revisions_data.timestamp - # --- + pop = self.client_request(params) - # --- + if not pop: return False - # --- + error = pop.get("error", {}) edit = pop.get("edit", {}) result = edit.get("result", "") - # --- + # {'edit': {'result': 'Success', 'pageid': 5013, 'title': 'User:Mr. Ibrahem/sandbox', 'contentmodel': 'wikitext', 'oldrevid': 1336986, 'newrevid': 1343447, 'newtimestamp': '2023-04-01T23:14:07Z', 'watched': ''}} - # --- + if result.lower() == "success": self.text = newtext self.user = "" logger.info(f"<> ** true .. [[{self.lang}:{self.family}:{self.title}]] ") # logger.info('Done True...') - # --- + self.revisions_data.pageid = edit.get("pageid") or self.revisions_data.pageid self.revisions_data.revid = edit.get("newrevid") or self.revisions_data.revid self.revisions_data.newrevid = edit.get("newrevid") or self.revisions_data.newrevid self.revisions_data.touched = edit.get("touched") or self.revisions_data.touched self.revisions_data.timestamp = edit.get("newtimestamp") or self.revisions_data.timestamp - # --- + return True - # --- + if error != {}: print(pop) er = self.handle_err(error, function="Save", params=params) - # --- + return er - # --- + return False def purge(self): - # --- + params = { "action": "purge", "forcelinkupdate": 1, "forcerecursivelinkupdate": 1, "titles": self.title, } - # --- + data = self.client_request(params) - # --- + if not data: logger.info("<> ** purge error. ") return False - # --- + title2 = self.title - # --- + # 'normalized': [{'from': 'وب:ملعب', 'to': 'ويكيبيديا:ملعب'}]} - # --- + for x in data.get("normalized", []): # logger.info(f"normalized from {x['from']} to {x['to']}") if x["from"] == self.title: title2 = x["to"] break - # --- + for t in data.get("purge", []): # t = [{'ns': 4, 'title': 'ويكيبيديا:ملعب', 'purged': '', 'linkupdate': ''}] ti = t["title"] @@ -791,7 +796,6 @@ def create( nodiff="", noask=False, ) -> bool: - # --- """ Creates a new page with the specified text and summary. @@ -807,13 +811,13 @@ def create( True if the page was created successfully, False otherwise or if the user aborts. """ self.newtext = text - # --- + if not noask: - # --- + message = f"Do you want to create this page? ({self.lang}:{self.title})" - # --- + user = self.meta.username - # --- + if ( self.ask_put( nodiff=nodiff, @@ -826,7 +830,7 @@ def create( is False ): return False - # --- + params = { "action": "edit", "title": self.title, @@ -835,39 +839,39 @@ def create( "notminor": 1, "createonly": 1, } - # --- + pop = self.client_request(params) - # --- + if not pop: return False - # --- + error = pop.get("error", {}) edit = pop.get("edit", {}) result = edit.get("result", "") - # --- + if result.lower() == "success": - # --- + # {'edit': {'new': '', 'result': 'Success', 'pageid': 9090918, 'title': 'مستخدم:Mr. Ibrahem/test2024', 'contentmodel': 'wikitext', 'oldrevid': 0, 'newrevid': 61016221, 'newtimestamp': '2023-02-01T21:52:42Z'}} - # --- + self.text = text - # --- + logger.info(f"<> ** true .. [[{self.lang}:{self.family}:{self.title}]] ") # logger.info('Done True... time.sleep() ') - # --- + self.revisions_data.pageid = edit.get("pageid") or self.revisions_data.pageid self.revisions_data.revid = edit.get("newrevid") or self.revisions_data.revid self.revisions_data.touched = edit.get("touched") or self.revisions_data.touched self.revisions_data.newrevid = edit.get("newrevid") or self.revisions_data.newrevid self.revisions_data.timestamp = edit.get("newtimestamp") or self.revisions_data.timestamp - # --- + return True - # --- + if error != {}: print(pop) er = self.handle_err(error, function="Create", params=params) - # --- + return er - # --- + return False def Create( @@ -893,18 +897,18 @@ def page_backlinks(self, ns=0): "formatversion": "2", "gblredirect": 1, } - # --- + # x = { 'batchcomplete': True, 'limits': { 'backlinks': 2500 }, 'query': { 'redirects': [{ 'from': 'فريدريش زيمرمان', 'to': 'فريدريش تسيمرمان' }], 'pages': [{ 'pageid': 2941285, 'ns': 0, 'title': 'فولفغانغ شويبله' }, { 'pageid': 4783977, 'ns': 0, 'title': 'وزارة الشؤون الرقمية والنقل' }, { 'pageid': 5218323, 'ns': 0, 'title': 'فريدريش تسيمرمان' }, { 'pageid': 6662649, 'ns': 0, 'title': 'غونتر كراوزه' }] } } - # --- + # data = self.client_request(params) # pages = data.get("query", {}).get("pages", []) - # --- + pages = self.post_continue(params, "query", _p_="pages", p_empty=[]) - # --- + back_links = [x for x in pages if x["title"] != self.title] - # --- + self.links_data.back_links = back_links - # --- + return self.links_data.back_links def page_links(self) -> list: @@ -926,13 +930,13 @@ def page_links(self) -> list: } # data = self.client_request(params) # data = data.get('parse', {}).get('links', []) - # --- + data: list = self.post_continue(params, "parse", _p_="links", p_empty=[]) - # --- + # [{'ns': 14, 'title': 'تصنيف:مقالات بحاجة لشريط بوابات', 'exists': True}, {'ns': 14, 'title': 'تصنيف:مقالات بحاجة لصندوق معلومات', 'exists': False}] - # --- + self.links_data.links2 = data - # --- + return self.links_data.links2 def page_links_query(self, plnamespace="*"): @@ -947,17 +951,17 @@ def page_links_query(self, plnamespace="*"): } # data = self.client_request(params) # data = data.get('query', {}).get('links', []) - # --- + data = self.post_continue(params, "query", _p_="links", p_empty=[]) - # --- + # [{'ns': 14, 'title': 'تصنيف:مقالات بحاجة لشريط بوابات', 'exists': True}, {'ns': 14, 'title': 'تصنيف:مقالات بحاجة لصندوق معلومات', 'exists': False}] - # --- + self.links_data.links = data - # --- + return self.links_data.links def get_revisions(self, rvprops=None) -> list: - # --- + rvprop = [ "comment", "timestamp", @@ -965,12 +969,12 @@ def get_revisions(self, rvprops=None) -> list: # "content", "ids", ] - # --- + if rvprops: for x in rvprops: if x not in rvprop: rvprop.append(x) - # --- + params = { "action": "query", "format": "json", @@ -984,16 +988,16 @@ def get_revisions(self, rvprops=None) -> list: # "rvprop": "comment|timestamp|user|content|ids", "rvprop": "|".join(rvprop), } - # --- + _revisions = self.post_continue(params, "query", _p_="pages", p_empty=[]) - # --- + revisions = [] - # --- + for x in _revisions: revisions.extend(x["revisions"]) - # --- + self.revisions_data.revisions = revisions - # --- + return revisions def post_continue( From 825d870990eacbe0b9c2f0bf0c6780e84bd5c8d8 Mon Sep 17 00:00:00 2001 From: Ibrahem Date: Tue, 5 May 2026 03:16:09 +0300 Subject: [PATCH 12/16] . --- .../bot_edit/bot_edit_by_templates.py | 91 ++++++++++--------- .../api_utils/bot_edit/bot_edit_by_time.py | 50 +++++----- newapi/client_wiki/pages/ar_err.py | 1 - newapi/client_wiki/pages/data.py | 6 +- newapi/client_wiki/pages/super_page.py | 47 ++++------ newapi/super/cookies_bot.py | 79 ---------------- 6 files changed, 95 insertions(+), 179 deletions(-) delete mode 100644 newapi/client_wiki/pages/ar_err.py delete mode 100644 newapi/super/cookies_bot.py diff --git a/newapi/client_wiki/api_utils/bot_edit/bot_edit_by_templates.py b/newapi/client_wiki/api_utils/bot_edit/bot_edit_by_templates.py index 9f503d7..156f50f 100644 --- a/newapi/client_wiki/api_utils/bot_edit/bot_edit_by_templates.py +++ b/newapi/client_wiki/api_utils/bot_edit/bot_edit_by_templates.py @@ -1,16 +1,14 @@ """ """ import logging -import sys import wikitextparser as wtp from ....config import settings logger = logging.getLogger(__name__) -edit_username = {1: "Mr.Ibrahembot"} -Bot_Cache = {} -stop_edit_temps = { + +STOP_EDIT_TEMPLATES: dict[str, list[str]] = { "all": ["تحرر", "قيد التطوير", "يحرر", "تطوير مقالة"], "تعريب": ["لا للتعريب"], "تقييم آلي": ["لا للتقييم الآلي"], @@ -22,70 +20,76 @@ "portal": ["لا لربط البوابات المعادل", "لا لصيانة البوابات"], } +BOT_USERNAME = "Mr.Ibrahembot" +Bot_Cache = {} + def _handle_nobots_template(params, title_page, botjob, _template): """Handle nobots template logic.""" - # --- # {{nobots}} منع جميع البوتات # منع جميع البوتات if not params: - logger.info(f"<> botEdit.py: the page has template:({_template}), botjob:{botjob} skipp.") + logger.debug(f"<> botEdit.py: the page has temp:({_template}), botjob:{botjob} skipp.") + logger.debug(f"nobots active - blocking bot {botjob} on {title_page}") Bot_Cache[botjob][title_page] = False return False elif params.get("1"): List = [x.strip() for x in params.get("1", "").split(",")] - # if 'all' in List or pywikibot.calledModuleName() in List or edit_username[1] in List: - if "all" in List or edit_username[1] in List: - logger.info(f"<> botEdit.py: the page has template:({_template}), botjob:{botjob} skipp.") + # if 'all' in List or pywikibot.calledModuleName() in List or BOT_USERNAME in List: + if "all" in List or BOT_USERNAME in List: + logger.debug(f"<> botEdit.py: the page has temp:({_template}), botjob:{botjob} skipp.") + logger.debug(f"bot {BOT_USERNAME} in nobots list - blocking {title_page}") + # Bot_Cache[title_page] = False Bot_Cache[botjob][title_page] = False return False - # --- + # no restricting template found Bot_Cache[botjob][title_page] = True - # --- return True def _handle_bots_template(params, title_page, botjob, title): """Handle bots template logic.""" - # --- + logger.debug(f"handling bots template for {title}") # {{bots}} السماح لجميع البوتات if not params: Bot_Cache[botjob][title_page] = False return False else: - logger.info(f"botEdit.py title:({title}), params:({str(params)}).") - # --- + logger.debug(f"botEdit.py title:({title}), params:({str(params)}).") + # for param in params: + # value = params[param] + # value = [ x.strip() for x in value.split(',') ] # {{bots|allow=all}} السماح لجميع البوتات # {{bots|allow=none}} منع جميع البوتات allow = params.get("allow") if allow: value = [x.strip() for x in allow.split(",")] - sd = "all" in value or edit_username[1] in value + # if param == 'allow': + # 'all' in value or BOT_USERNAME in value is True + sd = "all" in value or BOT_USERNAME in value if not sd: - logger.info(f"<>botEdit.py Template:({title}) has |allow={','.join(value)}.") + logger.debug(f"<>botEdit.py Template:({title}) has |allow={','.join(value)}.") else: - logger.info(f"<>botEdit.py Template:({title}) has |allow={','.join(value)}.") + logger.warning(f"<>botEdit.py Template:({title}) has |allow={','.join(value)}.") Bot_Cache[botjob][title_page] = sd return sd - # --- - # --- # {{bots|deny=all}} منع جميع البوتات deny = params.get("deny") if deny: value = [x.strip() for x in deny.split(",")] - sd = "all" not in value and edit_username[1] not in value + # {{bots|deny=all}} + # if param == 'deny': + sd = "all" not in value and BOT_USERNAME not in value if not sd: - logger.info(f"<>botEdit.py Template:({title}) has |deny={','.join(value)}.") + logger.debug(f"<>botEdit.py Template:({title}) has |deny={','.join(value)}.") Bot_Cache[botjob][title_page] = sd return sd - # --- # if param == 'allowscript': # return ('all' in value or pywikibot.calledModuleName() in value) # if param == 'denyscript': # return not ('all' in value or pywikibot.calledModuleName() in value) - # --- + # no restricting template found Bot_Cache[botjob][title_page] = True - # --- return True @@ -94,7 +98,6 @@ def is_bot_edit_allowed( title_page: str = "", botjob: str = "all", ) -> bool: - # --- """ Determines if a bot is permitted to edit a page based on templates in the page text. @@ -110,56 +113,56 @@ def is_bot_edit_allowed( """ if (settings.bot.force_edit) or settings.bot.workibrahem: return True - # --- + if botjob in ["", "fixref|cat|stub|tempcat|portal"]: botjob = "all" - # --- + if botjob not in Bot_Cache: Bot_Cache[botjob] = {} - # --- + if title_page in Bot_Cache[botjob]: return Bot_Cache[botjob][title_page] - # --- - all_stop = stop_edit_temps["all"] - # --- + + all_stop = STOP_EDIT_TEMPLATES["all"] + parser = wtp.parse(text) templates = parser.templates - # --- + for template in templates: title = str(template.normal_name()).strip() - # --- + params = { str(param.name).strip(): str(param.value).strip() for param in template.arguments if str(param.value).strip() } - # --- + _template = template.string - # --- - restrictions = stop_edit_temps.get(botjob, []) - # --- + + restrictions = STOP_EDIT_TEMPLATES.get(botjob, []) + if title in restrictions or title in all_stop: - logger.info(f"<> botEdit.py: the page has template:({title}), botjob:{botjob} skipp.") + logger.debug(f"<> botEdit.py: the page has temp:({title}), botjob:{botjob} skipp.") Bot_Cache[botjob][title_page] = False return False - # --- + # logger.debug("<>botEdit.py title:(%s), params:(%s)." % (title, str(params))) - # --- + if title.lower() == "nobots": return _handle_nobots_template(params, title_page, botjob, _template) - # --- + # {{bots|allow=}} منع جميع البوتات غير الموجودة في القائمة # {{bots|deny=}} منع جميع البوتات الموجودة في القائمة - # --- + elif title.lower() == "bots": return _handle_bots_template(params, title_page, botjob, title) - # --- + # no restricting template found Bot_Cache[botjob][title_page] = True - # --- return True __all__ = [ "is_bot_edit_allowed", + "BOT_USERNAME", ] diff --git a/newapi/client_wiki/api_utils/bot_edit/bot_edit_by_time.py b/newapi/client_wiki/api_utils/bot_edit/bot_edit_by_time.py index 4d4949b..170fc11 100644 --- a/newapi/client_wiki/api_utils/bot_edit/bot_edit_by_time.py +++ b/newapi/client_wiki/api_utils/bot_edit/bot_edit_by_time.py @@ -9,49 +9,47 @@ def check_create_time(page, title_page): - # --- """ Checks if a page was created at least three hours ago before allowing bot edits. Returns True if the page is not in the Arabic main namespace or if the creation timestamp is missing. Returns False if the page was created less than three hours ago, caching the result for future checks. """ - # --- + if title_page in _created_cache: return _created_cache[title_page] - # --- + ns = page.namespace() lang = page.lang - # --- + if ns != 0 or lang != "ar": return True - # --- + now = datetime.datetime.now(datetime.UTC) - # --- + create_data = page.get_create_data() # { "timestamp" : "2025-05-07T12:00:17Z", "user" : "", "anon" : "" } - # --- + delay_hours = 3 - # --- + if create_data.get("timestamp"): - # --- + create_time = create_data["timestamp"] ts_c_time = datetime.datetime.strptime(create_time, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.UTC) - # --- + diff = (now - ts_c_time).total_seconds() / (60 * 60) - # --- + user = create_data.get("user", "") - # --- + wait_time = delay_hours - diff - # --- + if diff < delay_hours: - logger.info(f"<>Page:{title_page} create at ({create_time}).") - logger.info(f"<>Page Created before {diff:.2f} hours by: {user}, wait {wait_time:.2f}H.") + logger.debug(f"<>Page:{title_page} create at ({create_time}).") + logger.debug(f"<>Page Created before {diff:.2f} hours by: {user}, wait {wait_time:.2f}H.") return False - # --- + return True def check_last_edit_time(page, title_page, delay): - # --- """ Checks if enough time has passed since the last non-bot edit before allowing a bot to edit. @@ -63,31 +61,31 @@ def check_last_edit_time(page, title_page, delay): delay: Minimum number of minutes that must have passed since the last edit. """ userinfo = page.get_userinfo() - # --- + if "bot" in userinfo.get("groups", []): return True - # --- + # example: 2025-05-07T12:00:17Z timestamp = page.get_timestamp() - # --- + now = datetime.datetime.now(datetime.UTC) - # --- + if timestamp: ts_time = datetime.datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=datetime.UTC) - # --- + diff_minutes = (now - ts_time).total_seconds() / 60 - # --- + # logger.info(f"<> last-edit Δ={diff_minutes:.2f} min for {title_page}") - # --- + wait_time = delay - diff_minutes - # --- + if diff_minutes < delay: logger.info(f"<>Page:{title_page} last edit ({timestamp}).") logger.info( f"<>Page Last edit before {delay} minutes, Wait {wait_time:.2f} minutes. title:{title_page}" ) return False - # --- + return True diff --git a/newapi/client_wiki/pages/ar_err.py b/newapi/client_wiki/pages/ar_err.py deleted file mode 100644 index bc68a39..0000000 --- a/newapi/client_wiki/pages/ar_err.py +++ /dev/null @@ -1 +0,0 @@ -""" """ diff --git a/newapi/client_wiki/pages/data.py b/newapi/client_wiki/pages/data.py index a0f33d9..6e7f932 100644 --- a/newapi/client_wiki/pages/data.py +++ b/newapi/client_wiki/pages/data.py @@ -2,7 +2,7 @@ from .data import Content, Meta, RevisionsData, LinksData, CategoriesData, TemplateData -(Exists|all_categories_with_hidden|back_links|can_be_edit|categories|create_data|extlinks|flagged|hidden_categories|info|is_disambig|is_redirect|iwlinks|length|links_here|links|newrevid|pageid|revid|revisions|summary|templates|templates_API|text_html|timestamp|touched|userinfo|username|wikibase_item|words) +(Exists|all_categories_with_hidden|back_links|can_be_edit|categories|create_data|extlinks|flagged|hidden_categories|info|is_disambig|is_redirect|iwlinks|length|links_here|links|newrevid|pageid|revid|revisions|summary|templates|templates_api|text_html|timestamp|touched|userinfo|username|wikibase_item|words) """ @@ -11,6 +11,8 @@ @dataclass class Content: + # text: str = "" + # newtext: str = "" text_html: str = "" summary: str = "" words: int = 0 @@ -62,4 +64,4 @@ class CategoriesData: @dataclass class TemplateData: templates: dict = field(default_factory=dict) - templates_API: dict = field(default_factory=dict) + templates_api: dict = field(default_factory=dict) diff --git a/newapi/client_wiki/pages/super_page.py b/newapi/client_wiki/pages/super_page.py index df825f8..f42f281 100644 --- a/newapi/client_wiki/pages/super_page.py +++ b/newapi/client_wiki/pages/super_page.py @@ -8,7 +8,8 @@ from ...api_client import WikiLoginClient from ...client_wiki.api_utils.handel_errors import HandleErrors from ...config import settings -from ..api_utils import botEdit, txtlib +from ..api_utils.botEdit import bot_May_Edit +from ..api_utils import txtlib from ..api_utils.ask_bot import AskBot from ..api_utils.lang_codes import change_codes from .data import CategoriesData, Content, LinksData, Meta, RevisionsData, TemplateData @@ -333,7 +334,7 @@ def get_infos(self): # 'templates': [{'ns': 10, 'title': 'قالب:No redirect'}], - self.template_data.templates_API = [ta["title"] for ta in ta.get("templates", [])] + self.template_data.templates_api = [ta["title"] for ta in ta.get("templates", [])] # "linkshere": [{"pageid": 189150,"ns": 0,"title": "طواف فرنسا"}, {"pageid": 308641,"ns": 10,"title": "قالب:AWB","redirect": ""}] self.links_data.links_here = ta.get("linkshere", []) @@ -359,7 +360,6 @@ def get_text_html(self): return self.content.text_html def get_redirect_target(self): - params = { "action": "query", "titles": self.title, @@ -367,7 +367,7 @@ def get_redirect_target(self): "redirects": 1, } - data = self.client_request(params) + data = self.login_bot.client_request(params, method="get") # _pages_ = { 'batchcomplete': '', 'query': { 'redirects': [{ 'from': 'Yemen', 'to': 'اليمن' }], 'pages': {}, 'normalized': [{ 'from': 'yemen', 'to': 'Yemen' }] } } @@ -378,7 +378,7 @@ def get_redirect_target(self): to = redirects.get("to", "") if to: - logger.info(f"<>Page:({self.title}) redirect to ({to})") + logger.debug(f"<>Page:({self.title}) redirect to ({to})") return to @@ -431,7 +431,7 @@ def get_extlinks(self): # params = {**params, **continue_params} params.update(continue_params) - json1 = self.client_request(params) + json1 = self.login_bot.client_request(params, method="get") continue_params = json1.get("continue", {}) @@ -458,7 +458,7 @@ def get_userinfo(self): "ususers": self.user, } - data = self.client_request(params) + data = self.login_bot.client_request(params, method="get") # _userinfo_ = { "id": 229481, "name": "Mr. Ibrahem", "groups": ["editor", "reviewer", "rollbacker", "*", "user", "autoconfirmed"] } @@ -470,24 +470,21 @@ def get_userinfo(self): return self.meta.userinfo def isRedirect(self): - if not self.meta.is_redirect: self.get_infos() return self.meta.is_redirect def isDisambiguation(self): - # if the title ends with '(توضيح)' or '(disambiguation)' self.meta.is_disambig = self.title.endswith("(توضيح)") or self.title.endswith("(disambiguation)") if self.meta.is_disambig: - logger.info(f'<> page "{self.title}" is Disambiguation / توضيح') + logger.debug(f'<> page "{self.title}" is Disambiguation / توضيح') return self.meta.is_disambig def get_categories(self, with_hidden=False): - # if not self.categories_data.categories: self.get_infos() if not self.meta.info["done"]: self.get_infos() @@ -498,14 +495,12 @@ def get_categories(self, with_hidden=False): return self.categories_data.categories def get_hidden_categories(self): - if self.categories_data.categories == {} and self.categories_data.hidden_categories == {}: self.get_infos() return self.categories_data.hidden_categories def get_langlinks(self): - if not self.meta.info["done"]: self.get_infos() @@ -516,7 +511,7 @@ def get_templates_API(self): if not self.meta.info["done"]: self.get_infos() - return self.template_data.templates_API + return self.template_data.templates_api def get_links_here(self): @@ -564,14 +559,13 @@ def Get_tags(self, tag=""): return new_tags def can_edit(self, script="", delay=0): - if self.family != "wikipedia": return True if not self.text: self.text = self.get_text() - self.meta.can_be_edit = botEdit.bot_May_Edit( + self.meta.can_be_edit = bot_May_Edit( text=self.text, title_page=self.title, botjob=script, page=self, delay=delay ) @@ -628,6 +622,7 @@ def get_revid(self): def exists(self): if not self.meta.Exists: self.get_text() + if not self.meta.Exists: logger.info(f'page "{self.title}" not exists in {self.lang}:{self.family}') return self.meta.Exists @@ -635,6 +630,7 @@ def exists(self): def namespace(self): if self.ns is False: self.get_text() + logger.debug(f"namespace: {self.ns}") return self.ns def get_user(self): @@ -721,7 +717,7 @@ def save( # params['basetimestamp'] = self.revisions_data.timestamp - pop = self.client_request(params) + pop = self.login_bot.client_request(params) if not pop: return False @@ -735,8 +731,8 @@ def save( if result.lower() == "success": self.text = newtext self.user = "" - logger.info(f"<> ** true .. [[{self.lang}:{self.family}:{self.title}]] ") - # logger.info('Done True...') + logger.warning(f"<> ** true .. [[{self.lang}:{self.family}:{self.title}]] ") + logger.debug(f"save success for {self.title}") self.revisions_data.pageid = edit.get("pageid") or self.revisions_data.pageid self.revisions_data.revid = edit.get("newrevid") or self.revisions_data.revid @@ -747,7 +743,7 @@ def save( return True if error != {}: - print(pop) + logger.debug(pop) er = self.handle_err(error, function="Save", params=params) return er @@ -813,7 +809,6 @@ def create( self.newtext = text if not noask: - message = f"Do you want to create this page? ({self.lang}:{self.title})" user = self.meta.username @@ -840,7 +835,7 @@ def create( "createonly": 1, } - pop = self.client_request(params) + pop = self.login_bot.client_request(params) if not pop: return False @@ -850,13 +845,12 @@ def create( result = edit.get("result", "") if result.lower() == "success": - # {'edit': {'new': '', 'result': 'Success', 'pageid': 9090918, 'title': 'مستخدم:Mr. Ibrahem/test2024', 'contentmodel': 'wikitext', 'oldrevid': 0, 'newrevid': 61016221, 'newtimestamp': '2023-02-01T21:52:42Z'}} self.text = text - logger.info(f"<> ** true .. [[{self.lang}:{self.family}:{self.title}]] ") - # logger.info('Done True... time.sleep() ') + logger.warning(f"<> ** true .. [[{self.lang}:{self.family}:{self.title}]] ") + logger.debug(f"create success for {self.title}") self.revisions_data.pageid = edit.get("pageid") or self.revisions_data.pageid self.revisions_data.revid = edit.get("newrevid") or self.revisions_data.revid @@ -867,9 +861,8 @@ def create( return True if error != {}: - print(pop) + logger.debug(pop) er = self.handle_err(error, function="Create", params=params) - return er return False diff --git a/newapi/super/cookies_bot.py b/newapi/super/cookies_bot.py deleted file mode 100644 index a48ff84..0000000 --- a/newapi/super/cookies_bot.py +++ /dev/null @@ -1,79 +0,0 @@ -""" - -# cookies = get_cookies(lang, family, username) - -""" - -import functools -import logging -import os -import stat -from datetime import datetime, timedelta -from pathlib import Path - -from ..config import settings - -logger = logging.getLogger(__name__) -statgroup = stat.S_IRWXU | stat.S_IRWXG - - -@functools.lru_cache(maxsize=1) -def get_ta_dir() -> Path: - tool = os.getenv("HOME") - - if not tool: - tool = Path(__file__).parent - else: - tool = Path(tool) - - ta_dir = tool / "cookies" - - if not ta_dir.exists(): - ta_dir.mkdir() - logger.info("<> mkdir:") - logger.info(f"ta_dir:{ta_dir}") - logger.info("<> mkdir:") - os.chmod(ta_dir, statgroup) - - return ta_dir - - -def del_cookies_file(file_path): - # --- - file = Path(str(file_path)) - # --- - if file.exists(): - try: - file.unlink(missing_ok=True) - logger.info(f"<> unlink: file:{file}") - except Exception as e: - logger.error(f"<> unlink: Exception:{e}") - - -def get_file_name(lang, family, username) -> Path: - - ta_dir = get_ta_dir() - - if settings.bot.no_cookies: - randome = os.urandom(8).hex() - return ta_dir / f"{randome}.txt" - # --- - lang = lang.lower() - family = family.lower() - # --- - username = username.lower().replace(" ", "_").split("@")[0] - # --- - file = ta_dir / f"{family}_{lang}_{username}.txt" - # --- - if file.exists(): - # --- - # check if file old is > 3 days - # --- - file_time = datetime.fromtimestamp(file.stat().st_mtime) - # --- - if not file.stat().st_size: - del_cookies_file(file) - elif datetime.now() - file_time > timedelta(days=3): - del_cookies_file(file) - # --- - return file From 723eab38682bf5a624464f5df5716cee56ca924e Mon Sep 17 00:00:00 2001 From: Ibrahem Date: Tue, 5 May 2026 03:20:32 +0300 Subject: [PATCH 13/16] . --- newapi/client_wiki/pages/super_page.py | 2 +- newapi/super/S_API/bot.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/newapi/client_wiki/pages/super_page.py b/newapi/client_wiki/pages/super_page.py index f42f281..31ea505 100644 --- a/newapi/client_wiki/pages/super_page.py +++ b/newapi/client_wiki/pages/super_page.py @@ -8,9 +8,9 @@ from ...api_client import WikiLoginClient from ...client_wiki.api_utils.handel_errors import HandleErrors from ...config import settings -from ..api_utils.botEdit import bot_May_Edit from ..api_utils import txtlib from ..api_utils.ask_bot import AskBot +from ..api_utils.botEdit import bot_May_Edit from ..api_utils.lang_codes import change_codes from .data import CategoriesData, Content, LinksData, Meta, RevisionsData, TemplateData diff --git a/newapi/super/S_API/bot.py b/newapi/super/S_API/bot.py index 610587e..e826f6f 100644 --- a/newapi/super/S_API/bot.py +++ b/newapi/super/S_API/bot.py @@ -5,7 +5,6 @@ """ import logging -import sys from ...client_wiki.api_utils.ask_bot import AskBot from ...client_wiki.api_utils.handel_errors import HandleErrors From 2136cea43e173f41364fd2bac4e2882f13d9ede9 Mon Sep 17 00:00:00 2001 From: Ibrahem Date: Tue, 5 May 2026 03:21:50 +0300 Subject: [PATCH 14/16] . --- .../test_bot_edit_by_templates.py | 12 ++++++------ .../test_bot_edit_by_templates2.py | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates.py b/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates.py index 1dfe843..c641706 100644 --- a/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates.py +++ b/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates.py @@ -10,9 +10,9 @@ import pytest from newapi.client_wiki.api_utils.bot_edit.bot_edit_by_templates import ( Bot_Cache, - edit_username, + BOT_USERNAME, is_bot_edit_allowed, - stop_edit_temps, + STOP_EDIT_TEMPLATES, ) @@ -36,7 +36,7 @@ def original_argv(): @pytest.fixture def bot_username(): """Return the bot username for testing.""" - return edit_username.get(1, "Mr.Ibrahembot") + return BOT_USERNAME # Test bot job normalization @@ -101,12 +101,12 @@ def test_cache_key_includes_botjob(self, original_argv): # Test stop templates class TestStopTemplates: - """Test cases for stop_edit_temps restrictions.""" + """Test cases for STOP_EDIT_TEMPLATES restrictions.""" def test_all_stop_templates_block_edit(self, original_argv): """Templates in 'all' stop list should block editing.""" sys.argv = ["script"] - for template in stop_edit_temps["all"]: + for template in STOP_EDIT_TEMPLATES["all"]: text = f"{{{{{template}}}}}" assert not is_bot_edit_allowed( text=text, title_page=f"Test_{template}", botjob="all" @@ -115,7 +115,7 @@ def test_all_stop_templates_block_edit(self, original_argv): def test_botjob_specific_stop_templates(self, original_argv): """Templates in botjob-specific stop list should block editing.""" sys.argv = ["script"] - for botjob, templates in stop_edit_temps.items(): + for botjob, templates in STOP_EDIT_TEMPLATES.items(): if botjob == "all": continue for template in templates: diff --git a/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates2.py b/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates2.py index 159fd40..04a81ef 100644 --- a/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates2.py +++ b/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates2.py @@ -15,7 +15,7 @@ from newapi.client_wiki.api_utils.bot_edit.bot_edit_by_templates import ( Bot_Cache, is_bot_edit_allowed, - stop_edit_temps, + STOP_EDIT_TEMPLATES, ) # ==================== Fixtures ==================== @@ -343,7 +343,7 @@ def test_bots_case_insensitive(self, setup_parser): class TestStopEditTemplates: """Test stop edit templates handling.""" - @pytest.mark.parametrize("template_name", stop_edit_temps["all"]) + @pytest.mark.parametrize("template_name", STOP_EDIT_TEMPLATES["all"]) def test_global_stop_templates_deny_edit(self, template_name, setup_parser): """Test that global stop templates deny editing.""" setup_parser([{"name": template_name, "arguments": None}]) @@ -372,7 +372,7 @@ def test_stop_template_for_different_botjob_allows_edit(self, setup_parser): @pytest.mark.parametrize( "botjob,template_list", - [(job, templates) for job, templates in stop_edit_temps.items() if job != "all"], + [(job, templates) for job, templates in STOP_EDIT_TEMPLATES.items() if job != "all"], ) def test_all_stop_templates_for_each_botjob(self, botjob, template_list, setup_parser): """Test all stop templates for each specific bot job.""" From 5aa2efc3b93e1c945795e6b51e07336f7eec47cf Mon Sep 17 00:00:00 2001 From: Ibrahem Date: Tue, 5 May 2026 03:22:27 +0300 Subject: [PATCH 15/16] Update TestMainPage.py --- tests/TestMainPage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestMainPage.py b/tests/TestMainPage.py index 1e250e0..f06e3c6 100644 --- a/tests/TestMainPage.py +++ b/tests/TestMainPage.py @@ -85,7 +85,7 @@ def test_page_without_edit_permission(self, mock_login_bot): } } page = MainPage(mock_login_bot, "الصفحة الرئيسة", "ar") - with patch("newapi.client_wiki.pages.super_page.botEdit.bot_May_Edit", return_value=False): + with patch("newapi.client_wiki.pages.super_page.bot_May_Edit", return_value=False): assert page.can_edit() is False def test_page_title_validation(self): From 25f4a11114c09d541340a6e674936b736ab1d46a Mon Sep 17 00:00:00 2001 From: Ibrahem Date: Tue, 5 May 2026 03:22:41 +0300 Subject: [PATCH 16/16] . --- .../bot_edit_by_templates/test_bot_edit_by_templates.py | 4 ++-- .../bot_edit_by_templates/test_bot_edit_by_templates2.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates.py b/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates.py index c641706..53641cd 100644 --- a/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates.py +++ b/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates.py @@ -9,10 +9,10 @@ import pytest from newapi.client_wiki.api_utils.bot_edit.bot_edit_by_templates import ( - Bot_Cache, BOT_USERNAME, - is_bot_edit_allowed, STOP_EDIT_TEMPLATES, + Bot_Cache, + is_bot_edit_allowed, ) diff --git a/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates2.py b/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates2.py index 04a81ef..23c7361 100644 --- a/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates2.py +++ b/tests/unit/api_utils/bot_edit/bot_edit_by_templates/test_bot_edit_by_templates2.py @@ -13,9 +13,9 @@ import pytest from newapi.client_wiki.api_utils.bot_edit.bot_edit_by_templates import ( + STOP_EDIT_TEMPLATES, Bot_Cache, is_bot_edit_allowed, - STOP_EDIT_TEMPLATES, ) # ==================== Fixtures ====================