jenkins-bot has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/836251 )
Change subject: [IMPR] Add APISite.ratelimit() method
......................................................................
[IMPR] Add APISite.ratelimit() method
- add a RateLimit namedtuple to tools/collections.py
- add APISite.ratelimit() method
- use it in api.Request instead of private _ratelimited() method
to handle ratelimited warning
- add some tests
Bug: T304808
Change-Id: I5db248dc4ef4bea03ad3ac4589830c209b961a04
---
M pywikibot/site/_apisite.py
M pywikibot/data/api/_requests.py
M pywikibot/tools/collections.py
M tests/site_tests.py
4 files changed, 204 insertions(+), 16 deletions(-)
Approvals:
JJMC89: Looks good to me, approved
jenkins-bot: Verified
diff --git a/pywikibot/data/api/_requests.py b/pywikibot/data/api/_requests.py
index 3b63cb2..916fc41 100644
--- a/pywikibot/data/api/_requests.py
+++ b/pywikibot/data/api/_requests.py
@@ -918,21 +918,11 @@
return True
def _ratelimited(self) -> None:
- """Handle ratelimited warning."""
- ratelimits = self.site.userinfo['ratelimits']
- delay = None
+ """Handle ratelimited warning.
- ratelimit = ratelimits.get(self.action, {})
- # find the lowest wait time for the given action
- for limit in ratelimit.values():
- seconds = limit['seconds']
- hits = limit['hits']
- delay = min(delay or seconds, seconds / hits)
-
- if not delay:
- pywikibot.warning(
- f'No rate limit found for action {self.action}')
- self.wait(delay)
+ This is also called from :meth:`_default_warning_handler`.
+ """
+ self.wait(self.site.ratelimit(self.action).delay)
def _bad_token(self, code) -> bool:
"""Check for bad token.
diff --git a/pywikibot/site/_apisite.py b/pywikibot/site/_apisite.py
index 1b4b1e6..2135184 100644
--- a/pywikibot/site/_apisite.py
+++ b/pywikibot/site/_apisite.py
@@ -78,6 +78,7 @@
merge_unique_dicts,
normalize_username,
)
+from pywikibot.tools.collections import RateLimit
if TYPE_CHECKING:
@@ -85,8 +86,8 @@
__all__ = ('APISite', )
-_mw_msg_cache: DefaultDict[str, dict[str, str]] = defaultdict(dict)
+_mw_msg_cache: DefaultDict[str, dict[str, str]] = defaultdict(dict)
_CompType = Union[int, str, 'pywikibot.page.Page', 'pywikibot.page.Revision']
_RequestWrapperT = TypeVar('_RequestWrapperT', bound='api._RequestWrapper')
@@ -498,6 +499,108 @@
return int(parameter['limit']) # T78333, T161783
+ def ratelimit(self, action: str) -> RateLimit:
+ """Get the rate limit for a given action.
+
+ This method get the ratelimit for a given action and returns a
+ :class:`tools.collections.RateLimit` namedtuple which has the
+ following fields and properties:
+
+ - ``group`` --- The current user group returned by the API. If
+ the user is not logged in, the group will be 'ip'.
+ - ``hits`` --- rate limit hits; API requests should not exceed
+ this limit value for the given action.
+ - ``seconds`` --- time base in seconds for the maximum hits
+ - ``delay`` --- *(property)* calculated as seconds per hits
+ which may be used for wait cycles.
+ - ``ratio`` --- *(property)* inverse of delay, calculated as
+ hits per seconds. The result may be Infinite.
+
+ If the user has 'noratelimit' rights, :meth:`maxlimit` is used
+ for ``hits`` and ``seconds`` will be 0. 'noratelimit' is
+ returned as group parameter in that case.
+
+ If no rate limit is found for the given action, :meth:`maxlimit`
+ is used for ``hits`` and ``seconds`` will be
+ :ref:`config.put_throttle<Settings to Avoid Server Overload>`.
+ As group parameter 'unknown' is returned in that case.
+
+ **Examples:**
+
+ This is an example for a bot user which is not logged in. The
+ rate limit user group is 'ip'
+
+ >>> site = pywikibot.Site()
+ >>> limit = site.ratelimit('edit') # get rate limit for 'edit' action
+ >>> limit
+ RateLimit(group='ip', hits=8, seconds=60)
+ >>> limit.delay # delay and ratio must be get as attributes
+ 7.5
+ >>> site.ratelimit('purge').hits # get purge hits
+ 30
+ >>> group, *limit = site.ratelimit('urlshortcode')
+ >>> group # the user is not logged in, we get 'ip' as group
+ 'ip'
+ >>> limit # starred assignment is allowed for the fields
+ [10, 120]
+
+ After login to the site and the rate limit will change. The
+ limit user group might be 'user':
+
+ >>> limit = site.ratelimit('edit') # doctest: +SKIP
+ >>> limit # doctest: +SKIP
+ RateLimit(group='user', hits=90, seconds=60)
+ >>> limit.ratio # doctest: +SKIP
+ 1.5
+ >>> limit = site.ratelimit('urlshortcode') # no action limit found
+ >>> group, *limits = limit
+ >>> group # doctest: +SKIP
+ 'unknown' # the group is 'unknown' because action was not found
+ >>> limits # doctest: +SKIP
+ (50, 10) # hits is maxlimit and seconds is config.put_throttle
+ >>> site.maxlimit, pywikibot.config.put_throttle
+ (50, 10)
+
+ If a user is logged in and has no rate limit, e.g bot accounts,
+ we always get a default RateLimit namedtuple like this:
+
+ >>> site.has_right['noratelimit'] # doctest: +SKIP
+ True
+ >>> limit = site.ratelimit('any_action') # maxlimit is used
+ >>> limit # doctest: +SKIP
+ RateLimit(group='noratelimit', hits=500, seconds=0)
+ >>> limit.delay, limit.ratio # doctest: +SKIP
+ (0.0, inf)
+
+ .. seealso:: :class:`tools.collections.RateLimit` for RateLimit
+ examples.
+ .. note:: It is not verified whether ``action`` parameter has a
+ valid value.
+ .. seealso:: :api:`Ratelimit`
+ .. versionadded:: 9.0
+
+ :param action: action which might be limited
+ :return: RateLimit tuple with ``group``, ``hits`` and ``seconds``
+ fields and properties for ``delay`` and ``ratio``.
+ """
+ ratio = 0
+ for key, value in self.userinfo['ratelimits'].get(action, {}).items():
+ h, s = value['hits'], value['seconds']
+ # find the highest ratio hits per seconds
+ if h / s > ratio:
+ limit = value
+ limit['group'] = key
+ ratio = h / s
+
+ if not ratio: # no limits found
+ limit = {'hits': self.maxlimit}
+ if self.has_right('noratelimit'):
+ limit['group'] = 'noratelimit'
+ else:
+ limit['seconds'] = pywikibot.config.put_throttle
+
+ return RateLimit(**limit)
+
@property
def userinfo(self) -> dict[str, Any]:
"""Retrieve userinfo from site and store in _userinfo attribute.
diff --git a/pywikibot/tools/collections.py b/pywikibot/tools/collections.py
index 63fbe43..38e330a 100644
--- a/pywikibot/tools/collections.py
+++ b/pywikibot/tools/collections.py
@@ -11,7 +11,7 @@
from collections.abc import Collection, Generator, Iterator, Mapping
from contextlib import suppress
from itertools import chain
-from typing import Any
+from typing import Any, NamedTuple
from pywikibot.backports import Generator as GeneratorType
@@ -21,6 +21,7 @@
'DequeGenerator',
'EmptyDefault',
'GeneratorWrapper',
+ 'RateLimit',
'SizedKeyCollection',
'EMPTY_DEFAULT',
)
@@ -294,3 +295,55 @@
"""Restart the generator."""
with suppress(AttributeError):
del self._started_gen
+
+
+class RateLimit(NamedTuple):
+
+ """A namedtuple which can hold rate limit content.
+
+ This class is used by :meth:`APISite.ratelimit()
+ <pywikibot.site._apisite.APISite.ratelimit>`.
+
+ .. note:: :meth:`delay` and :meth:`ratio` properties cannot be
+ sliced or used with tuple indices. The must be used as attributes.
+
+ >>> limit = RateLimit('user', 500, 10)
+ >>> limit
+ RateLimit(group='user', hits=500, seconds=10)
+ >>> limit.delay
+ 0.02
+ >>> limit.ratio
+ 50.0
+ >>> limit._fields
+ ('group', 'hits', 'seconds')
+ >>> limit._asdict() # doctest: +SKIP
+ {'group': 'user', 'hits': 500, 'seconds': 10}
+ >>> limit[0]
+ 'user'
+ >>> limit[-1]
+ 10
+ >>> user, hits, seconds = limit
+ >>> hits, seconds
+ (500, 10)
+ >>> newlimit = limit._replace(seconds=0)
+ >>> newlimit.delay
+ 0.0
+ >>> newlimit.ratio
+ inf
+
+ .. versionadded:: 9.0
+ """
+
+ group: str = 'unknown'
+ hits: int = 50
+ seconds: int = 0
+
+ @property
+ def delay(self) -> float:
+ """Calculate a delay value which is the inverse of :meth:`ratio`."""
+ return self.seconds / self.hits
+
+ @property
+ def ratio(self) -> float:
+ """Calculate a ratio how many hits can be done within one second."""
+ return self.hits / self.seconds if self.seconds != 0 else float('inf')
diff --git a/tests/site_tests.py b/tests/site_tests.py
index d2a51cf..840b5b2 100755
--- a/tests/site_tests.py
+++ b/tests/site_tests.py
@@ -277,6 +277,32 @@
if a:
self.assertEqual(a[0], mainpage)
+ def test_maxlimit(self):
+ """Test maxlimit property."""
+ limit = self.site.maxlimit
+ self.assertIsInstance(limit, int)
+ self.assertIn(limit, [10, 50, 500, 5000])
+
+ def test_ratelimit(self):
+ """Test ratelimit method."""
+ actions = ('edit', 'move', 'purge', 'invalid')
+ if self.site.logged_in():
+ groups = ['user', 'unknown', 'noratelimit']
+ else:
+ groups = ['ip', 'unknown']
+ self.assertFalse(self.site.has_right('noratelimit'))
+ for action in actions:
+ with self.subTest(action=action):
+ limit = self.site.ratelimit(action)
+ self.assertIn(limit.group, groups)
+ self.assertEqual(limit.seconds / limit.hits, limit.delay)
+ self.assertEqual(
+ 1 / limit.delay if limit.seconds else float('inf'),
+ limit.ratio)
+ if limit.group == 'unknown':
+ self.assertEqual(limit.hits, self.site.maxlimit)
+ self.assertEqual(limit.seconds, config.put_throttle)
+
class TestLockingPage(DefaultSiteTestCase):
"""Test cases for lock/unlock a page within threads."""
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/836251
To unsubscribe, or for help writing mail filters, visit https://gerrit.wikimedia.org/r/settings
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: I5db248dc4ef4bea03ad3ac4589830c209b961a04
Gerrit-Change-Number: 836251
Gerrit-PatchSet: 19
Gerrit-Owner: Xqt <info(a)gno.de>
Gerrit-Reviewer: JJMC89 <JJMC89.Wikimedia(a)gmail.com>
Gerrit-Reviewer: jenkins-bot
Gerrit-CC: Mpaa <mpaa.wiki(a)gmail.com>
Gerrit-MessageType: merged
jenkins-bot has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/983522 )
Change subject: [bugfix] Allow to run tests with oauth
......................................................................
[bugfix] Allow to run tests with oauth
Use api.php instead of index.php to check if site in alive.
Bug: T352606
Change-Id: I2a436d0f1cd2c06201688d4ebe211bf97c787640
---
M tests/aspects.py
1 file changed, 14 insertions(+), 1 deletion(-)
Approvals:
JJMC89: Looks good to me, approved
jenkins-bot: Verified
diff --git a/tests/aspects.py b/tests/aspects.py
index ff191ee..f6dc453 100644
--- a/tests/aspects.py
+++ b/tests/aspects.py
@@ -940,7 +940,7 @@
# obsolete without a mapping to a hostname.
with suppress(KeyError):
data['hostname'] = (
- data['site'].base_url(data['site'].path()))
+ data['site'].base_url(data['site'].apipath()))
cm.__exit__(None, None, None)
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/983522
To unsubscribe, or for help writing mail filters, visit https://gerrit.wikimedia.org/r/settings
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: I2a436d0f1cd2c06201688d4ebe211bf97c787640
Gerrit-Change-Number: 983522
Gerrit-PatchSet: 2
Gerrit-Owner: Mpaa <mpaa.wiki(a)gmail.com>
Gerrit-Reviewer: JJMC89 <JJMC89.Wikimedia(a)gmail.com>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged
Xqt has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/981376 )
Change subject: [cleanup] deprecate SequenceOutputter.output() method
......................................................................
[cleanup] deprecate SequenceOutputter.output() method
SequenceOutputter acts like a bot_choice Option and
output() methods were derecated in Pywikibot 6.2 in favour
of out property due to cache_output implementation
Change-Id: I0dd4fdc006d7adde16f1aa22ff3fa0eee4c66831
---
M scripts/solve_disambiguation.py
M ROADMAP.rst
M pywikibot/tools/formatter.py
3 files changed, 21 insertions(+), 2 deletions(-)
Approvals:
Xqt: Verified; Looks good to me, approved
diff --git a/ROADMAP.rst b/ROADMAP.rst
index c95ddc6..a0e7f09 100644
--- a/ROADMAP.rst
+++ b/ROADMAP.rst
@@ -29,6 +29,7 @@
Deprecations
------------
+* 9.0.0: ``SequenceOutputter.output()`` is deprecated in favour of :attr:`tools.formatter.SequenceOutputter.out` property
* 9.0.0: *nullcontext* context manager and *SimpleQueue* queue of :mod:`backports` are derecated
* 8.4.0: *modules_only_mode* parameter of :class:`data.api.ParamInfo`, its *paraminfo_keys* class attribute
and its preloaded_modules property will be removed
diff --git a/pywikibot/tools/formatter.py b/pywikibot/tools/formatter.py
index 7008347..6ad82f5 100644
--- a/pywikibot/tools/formatter.py
+++ b/pywikibot/tools/formatter.py
@@ -55,8 +55,13 @@
content = ''
return self.prefix + content + self.suffix
+ @deprecated('pywikibot.info(SequenceOutputter.out)', since='9.0.0')
def output(self) -> None:
- """Output the text of the current sequence."""
+ """Output the text of the current sequence.
+
+ .. deprecated:: 9.0
+ Use :func:`pywikibot.info` with *out* property
+ """
info(self.out)
diff --git a/scripts/solve_disambiguation.py b/scripts/solve_disambiguation.py
index 45e5956..ac7f4ad 100755
--- a/scripts/solve_disambiguation.py
+++ b/scripts/solve_disambiguation.py
@@ -1275,7 +1275,7 @@
self.opt.pos.sort(key=lambda x: x.lower())
else:
self.opt.pos.sort()
- SequenceOutputter(self.opt.pos).output()
+ pywikibot.info(SequenceOutputter(self.opt.pos).out)
gen = ReferringPageGeneratorWithIgnore(
page,
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/981376
To unsubscribe, or for help writing mail filters, visit https://gerrit.wikimedia.org/r/settings
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: I0dd4fdc006d7adde16f1aa22ff3fa0eee4c66831
Gerrit-Change-Number: 981376
Gerrit-PatchSet: 1
Gerrit-Owner: Xqt <info(a)gno.de>
Gerrit-Reviewer: D3r1ck01 <dalangi-ctr(a)wikimedia.org>
Gerrit-Reviewer: Xqt <info(a)gno.de>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged
jenkins-bot has submitted this change. ( https://gerrit.wikimedia.org/r/c/pywikibot/core/+/921582 )
Change subject: [bugfix] Suppress error in CosmeticChangesToolkit.cleanUpLinks()
......................................................................
[bugfix] Suppress error in CosmeticChangesToolkit.cleanUpLinks()
- no longer retry api request on ServerError which includes
FatalServerError. ServerError is raised when a requests.ReadTimeout
or requests.ConnectTimeout occurs.
- suppress ServerError in CosmeticChangesToolkit.cleanUpLinks()
when calling site.isInterwikiLink() because the corresponding
site may be down.
Bug: T337045
Change-Id: I499bc1d7cc2bbd98cac20548a5e40c9dbf314ec2
---
M pywikibot/cosmetic_changes.py
M pywikibot/data/api/_requests.py
2 files changed, 26 insertions(+), 9 deletions(-)
Approvals:
Xqt: Looks good to me, approved
jenkins-bot: Verified
diff --git a/pywikibot/cosmetic_changes.py b/pywikibot/cosmetic_changes.py
index 1e13373..845d6ce 100644
--- a/pywikibot/cosmetic_changes.py
+++ b/pywikibot/cosmetic_changes.py
@@ -64,9 +64,8 @@
from urllib.parse import urlparse, urlunparse
import pywikibot
-from pywikibot import textlib
+from pywikibot import exceptions, textlib
from pywikibot.backports import Callable, Match, Pattern
-from pywikibot.exceptions import InvalidTitleError
from pywikibot.textlib import (
FILE_LINK_REGEX,
MultiTemplateMatchBuilder,
@@ -532,8 +531,10 @@
oldlink = url2string(match.group(),
encodings=self.site.encodings())
- is_interwiki = self.site.isInterwikiLink(titleWithSection)
- if is_interwiki:
+ is_interwiki = None
+ with suppress(exceptions.ServerError):
+ is_interwiki = self.site.isInterwikiLink(titleWithSection)
+ if is_interwiki is not False:
return oldlink
# The link looks like this:
@@ -541,10 +542,9 @@
# We only work on namespace 0 because pipes and linktrails work
# differently for images and categories.
page = pywikibot.Page(pywikibot.Link(titleWithSection, self.site))
- try:
+ in_main_namespace = None
+ with suppress(exceptions.InvalidTitleError):
in_main_namespace = page.namespace() == 0
- except InvalidTitleError:
- in_main_namespace = False
if not in_main_namespace:
return oldlink
diff --git a/pywikibot/data/api/_requests.py b/pywikibot/data/api/_requests.py
index 3b63cb2..61dad40 100644
--- a/pywikibot/data/api/_requests.py
+++ b/pywikibot/data/api/_requests.py
@@ -30,10 +30,10 @@
from pywikibot.exceptions import (
Client414Error,
Error,
- FatalServerError,
MaxlagTimeoutError,
NoUsernameError,
Server504Error,
+ ServerError,
SiteDefinitionError,
)
from pywikibot.login import LoginStatus
@@ -694,7 +694,7 @@
pywikibot.warning(
'Caught HTTP 414 error, although not using GET.')
raise
- except (ConnectionError, FatalServerError):
+ except (ConnectionError, ServerError):
# This error is not going to be fixed by just waiting
pywikibot.error(traceback.format_exc())
raise
--
To view, visit https://gerrit.wikimedia.org/r/c/pywikibot/core/+/921582
To unsubscribe, or for help writing mail filters, visit https://gerrit.wikimedia.org/r/settings
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: I499bc1d7cc2bbd98cac20548a5e40c9dbf314ec2
Gerrit-Change-Number: 921582
Gerrit-PatchSet: 5
Gerrit-Owner: Xqt <info(a)gno.de>
Gerrit-Reviewer: Meno25 <meno25mail(a)gmail.com>
Gerrit-Reviewer: Xqt <info(a)gno.de>
Gerrit-Reviewer: jenkins-bot
Gerrit-MessageType: merged