From b60fff31727d5783b19dc5f254f2c214547f35f7 Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Fri, 21 Jul 2023 15:38:02 +0200 Subject: [PATCH 01/14] docs: upgrade to sphinx 7.1+ and furo 2023.9.10 Sphinx 7.2+ for Python 3.9+, 7.1 only for Python 3.8. --- dev-requirements.txt | 5 +++-- docs/source/conf.py | 6 ++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 7831466753..f37c2b9ab6 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -6,11 +6,12 @@ flake8-future-import flake8-import-order flake8-type-checking; python_version >= '3.8' # Sphinx theme -furo==2022.4.7 +furo==2023.9.10 pytest~=7.1.0 pytest-vcr~=1.0.2 requests-mock~=1.9.3 -sphinx>=4,<5 +sphinx>=7.1.0,<8; python_version <= '3.8' +sphinx>=7.2.0,<8; python_version > '3.8' # specify exact autoprogram version because the new (in 2021) maintainer # showed that they will indeed make major changes in patch versions sphinxcontrib-autoprogram==0.1.8 diff --git a/docs/source/conf.py b/docs/source/conf.py index e8b31c7689..695b0c86bf 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -11,9 +11,7 @@ # serve to show the default. from datetime import date -import sys, os -parentdir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) -os.sys.path.insert(0,parentdir) + from sopel import __version__ # If extensions (or modules to document with autodoc) are in another directory, @@ -24,7 +22,7 @@ # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -needs_sphinx = '4.0' +needs_sphinx = '7.1' # todo: upgrade when Py3.8 reaches EOL # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. From abf29ab1587d38dd3b1292d2e8a73b91bb81ab04 Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Fri, 21 Jul 2023 15:38:41 +0200 Subject: [PATCH 02/14] docs: add general and module index --- docs/source/conf.py | 2 +- docs/source/index.rst | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 695b0c86bf..fe0a9733d9 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -109,7 +109,7 @@ pygments_dark_style = 'monokai' # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +modindex_common_prefix = ['sopel.'] # -- Options for HTML output --------------------------------------------------- diff --git a/docs/source/index.rst b/docs/source/index.rst index 2726ad3a4d..08952a9805 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -25,6 +25,8 @@ Documentation plugin package tests + genindex + modindex .. toctree:: :caption: Donate From 72a071d390a9fbe92427456a1ce25da1f17c71f4 Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Fri, 21 Jul 2023 17:12:16 +0200 Subject: [PATCH 03/14] docs, plugin: better handling of type checking features --- docs/source/package/plugins/rules.rst | 18 +++---- docs/source/plugin/advanced.rst | 4 ++ sopel/plugin.py | 73 ++++++++++++++++++--------- sopel/plugins/rules.py | 11 ++++ 4 files changed, 70 insertions(+), 36 deletions(-) diff --git a/docs/source/package/plugins/rules.rst b/docs/source/package/plugins/rules.rst index a3ffea267c..196d4eef94 100644 --- a/docs/source/package/plugins/rules.rst +++ b/docs/source/package/plugins/rules.rst @@ -14,16 +14,10 @@ sopel.plugins.rules :members: :undoc-members: - .. class:: TypedRule - - A :class:`~typing.TypeVar` bound to :class:`AbstractRule`. When used in - the :meth:`AbstractRule.from_callable` class method, it means the return - value must be an instance of the class used to call that method and not a - different subclass of ``AbstractRule``. - - .. versionadded:: 8.0 - - This ``TypeVar`` was added as part of a goal to start type-checking - Sopel and is not used at runtime. + .. autoclass:: TypedRule + :members: + :undoc-members: - .. TODO remove when sphinx-autodoc can manage TypeVar properly. + .. autoclass:: RuleMetrics + :members: + :undoc-members: diff --git a/docs/source/plugin/advanced.rst b/docs/source/plugin/advanced.rst index f7f5b7f9b8..d9a50cbc13 100644 --- a/docs/source/plugin/advanced.rst +++ b/docs/source/plugin/advanced.rst @@ -206,6 +206,10 @@ handler to run after the capability is acknowledged or denied by the server:: .. autoclass:: sopel.plugin.CapabilityNegotiation :members: +.. autoclass:: sopel.plugin.CapabilityHandler + :members: + :special-members: __call__ + Working with capabilities ------------------------- diff --git a/sopel/plugin.py b/sopel/plugin.py index 3554e74ace..068a91eb4f 100644 --- a/sopel/plugin.py +++ b/sopel/plugin.py @@ -18,7 +18,7 @@ Callable, Optional, Pattern, - Tuple, + Protocol, TYPE_CHECKING, Union, ) @@ -121,30 +121,15 @@ class CapabilityNegotiation(enum.Enum): """ -if TYPE_CHECKING: - CapabilityHandler = Callable[ - [Tuple[str, ...], SopelWrapper, bool], - CapabilityNegotiation, - ] - - -class capability: - """Decorate a function to request a capability and handle the result. +class CapabilityHandler(Protocol): + """:class:`~typing.Protocol` definition for capability handler. - :param name: name of the capability to negotiate with the server; this - positional argument can be used multiple times to form a - single ``CAP REQ`` - :param handler: optional keyword argument, acknowledgement handler + When a plugin requests a capability using the :class:`capability`, it can + define a callback handler for that request, that will be called upon + Sopel receiving either an ``ACK`` (capability enabled) or a ``NAK`` + (capability denied) CAP message. - The Client Capability Negotiation is a feature of IRCv3 that exposes a - mechanism for a server to advertise a list of features and for clients to - request them when they are available. - - This decorator will register a capability request, allowing the bot to - request capabilities if they are available. You can request more than one - at a time, which will make for one single request. - - The handler must follow this interface:: + Example:: from sopel import plugin from sopel.bot import SopelWrapper @@ -168,7 +153,47 @@ def capability_handler( # always return if Sopel can send "CAP END" (DONE) # or if the plugin must notify the bot for that later (CONTINUE) - return CapabilityNegotiation.DONE + return plugin.CapabilityNegotiation.DONE + + .. note:: + + This protocol class is used for type checking and documentation purpose + only. It should not be used for other purposes. + """ + def __call__( + self, + cap_req: tuple[str, ...], + bot: SopelWrapper, + acknowledged: bool, + ) -> CapabilityNegotiation: + """A capability handler must be a callable with this signature. + + :param cap_req: the capability request, as a tuple of string + :param bot: the bot instance + :param acknowledged: that flag that tells if the capability is enabled + or denied + :return: the return value indicates if the capability negotiation is + complete for this request or not + """ + + +class capability: + """Decorate a function to request a capability and handle the result. + + :param name: name of the capability to negotiate with the server; this + positional argument can be used multiple times to form a + single ``CAP REQ`` + :param handler: optional keyword argument, acknowledgement handler + + The Client Capability Negotiation is a feature of IRCv3 that exposes a + mechanism for a server to advertise a list of features and for clients to + request them when they are available. + + This decorator will register a capability request, allowing the bot to + request capabilities if they are available. You can request more than one + at a time, which will make for one single request. + + The handler must follow the :class:`CapabilityHandler` protocol. .. note:: diff --git a/sopel/plugins/rules.py b/sopel/plugins/rules.py index a40b41ae66..dcb91e8af1 100644 --- a/sopel/plugins/rules.py +++ b/sopel/plugins/rules.py @@ -54,6 +54,17 @@ ] TypedRule = TypeVar('TypedRule', bound='AbstractRule') +"""A :class:`~typing.TypeVar` bound to :class:`AbstractRule`. + +When used in the :meth:`AbstractRule.from_callable` class method, it means the +return value must be an instance of the class used to call that method and not +a different subclass of ``AbstractRule``. + +.. versionadded:: 8.0 + + This ``TypeVar`` was added as part of a goal to start type-checking + Sopel and is not used at runtime. +""" LOGGER = logging.getLogger(__name__) From 09ee6538f513b929f0e93b1d1f67ee15c3a57f99 Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Fri, 21 Jul 2023 18:39:49 +0200 Subject: [PATCH 04/14] docs, db, tools, trigger: another attempt at better types for documentation Co-authored-by: dgw --- docs/source/conf.py | 11 ++++++++++ sopel/db.py | 5 ++--- sopel/plugin.py | 13 ++++++------ sopel/plugins/rules.py | 1 + sopel/tools/identifiers.py | 5 +++++ sopel/tools/memories.py | 11 +++------- sopel/tools/target.py | 43 +++++++++++++++++--------------------- sopel/trigger.py | 18 +++++++--------- 8 files changed, 55 insertions(+), 52 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index fe0a9733d9..2f8c882f5a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -112,6 +112,17 @@ modindex_common_prefix = ['sopel.'] +# -- Options for autodoc ------------------------------------------------------- + +autodoc_type_aliases = { + 'Casemapping': 'sopel.tools.identifiers.Casemapping', + 'IdentifierFactory': 'sopel.tools.identifiers.IdentifierFactory', + 'ModeTuple': 'sopel.irc.modes.ModeTuple', + 'ModeDetails': 'sopel.irc.modes.ModeDetails', + 'PrivilegeDetails': 'sopel.irc.modes.PrivilegeDetails', +} + + # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for diff --git a/sopel/db.py b/sopel/db.py index 8c39ce918d..8deaa8035c 100644 --- a/sopel/db.py +++ b/sopel/db.py @@ -29,14 +29,13 @@ from sqlalchemy.sql import delete, func, select, update from sopel.lifecycle import deprecated -from sopel.tools.identifiers import Identifier +from sopel.tools.identifiers import Identifier, IdentifierFactory if typing.TYPE_CHECKING: from collections.abc import Iterable LOGGER = logging.getLogger(__name__) -IdentifierFactory = typing.Callable[[str], Identifier] def _deserialize(value): @@ -146,7 +145,7 @@ def __init__( config, identifier_factory: IdentifierFactory = Identifier, ) -> None: - self.make_identifier = identifier_factory + self.make_identifier: IdentifierFactory = identifier_factory if config.core.db_url is not None: self.url = make_url(config.core.db_url) diff --git a/sopel/plugin.py b/sopel/plugin.py index 068a91eb4f..37f06fa572 100644 --- a/sopel/plugin.py +++ b/sopel/plugin.py @@ -124,10 +124,10 @@ class CapabilityNegotiation(enum.Enum): class CapabilityHandler(Protocol): """:class:`~typing.Protocol` definition for capability handler. - When a plugin requests a capability using the :class:`capability`, it can - define a callback handler for that request, that will be called upon - Sopel receiving either an ``ACK`` (capability enabled) or a ``NAK`` - (capability denied) CAP message. + When a plugin requests a capability, it can define a callback handler for + that request using :class:`capability` as a decorator. That handler will be + called upon Sopel receiving either an ``ACK`` (capability enabled) or a + ``NAK`` (capability denied) CAP message. Example:: @@ -157,8 +157,9 @@ def capability_handler( .. note:: - This protocol class is used for type checking and documentation purpose - only. It should not be used for other purposes. + This protocol class should be used for type checking and documentation + purposes only. + """ def __call__( self, diff --git a/sopel/plugins/rules.py b/sopel/plugins/rules.py index dcb91e8af1..fa2ad833d7 100644 --- a/sopel/plugins/rules.py +++ b/sopel/plugins/rules.py @@ -64,6 +64,7 @@ This ``TypeVar`` was added as part of a goal to start type-checking Sopel and is not used at runtime. + """ LOGGER = logging.getLogger(__name__) diff --git a/sopel/tools/identifiers.py b/sopel/tools/identifiers.py index 017aa0fd06..6b0937a995 100644 --- a/sopel/tools/identifiers.py +++ b/sopel/tools/identifiers.py @@ -38,6 +38,7 @@ from typing import Callable Casemapping = Callable[[str], str] +"""Type definition of a casemapping callable.""" ASCII_TABLE = str.maketrans(string.ascii_uppercase, string.ascii_lowercase) RFC1459_TABLE = str.maketrans( @@ -278,3 +279,7 @@ def is_nick(self) -> bool: """ return bool(self) and not self.startswith(self.chantypes) + + +IdentifierFactory = Callable[[str], Identifier] +"""Type definition of an identifier factory.""" diff --git a/sopel/tools/memories.py b/sopel/tools/memories.py index aa21af7dbc..69ac99a663 100644 --- a/sopel/tools/memories.py +++ b/sopel/tools/memories.py @@ -8,14 +8,9 @@ from collections import defaultdict import threading -from typing import TYPE_CHECKING +from typing import Optional -from .identifiers import Identifier - -if TYPE_CHECKING: - from typing import Callable, Optional - - IdentifierFactory = Callable[[str], Identifier] +from .identifiers import Identifier, IdentifierFactory class SopelMemory(dict): @@ -156,7 +151,7 @@ def __init__( identifier_factory: IdentifierFactory = Identifier, ) -> None: super().__init__(*args) - self.make_identifier = identifier_factory + self.make_identifier: IdentifierFactory = identifier_factory """A factory to transform keys into identifiers.""" def _make_key(self, key: Optional[str]) -> Optional[Identifier]: diff --git a/sopel/tools/target.py b/sopel/tools/target.py index 8c4e6d1349..f7f34d0574 100644 --- a/sopel/tools/target.py +++ b/sopel/tools/target.py @@ -1,16 +1,15 @@ +"""User and channel objects used in state tracking.""" from __future__ import annotations import functools -from typing import Any, Callable, Optional, TYPE_CHECKING, Union +from typing import Any, Optional, TYPE_CHECKING, Union from sopel import privileges -from sopel.tools import identifiers, memories +from sopel.tools import memories +from sopel.tools.identifiers import Identifier, IdentifierFactory if TYPE_CHECKING: - from datetime import datetime - - -IdentifierFactory = Callable[[str], identifiers.Identifier] + import datetime @functools.total_ordering @@ -28,12 +27,12 @@ class User: def __init__( self, - nick: identifiers.Identifier, + nick: Identifier, user: Optional[str], host: Optional[str], ) -> None: - assert isinstance(nick, identifiers.Identifier) - self.nick: identifiers.Identifier = nick + assert isinstance(nick, Identifier) + self.nick: Identifier = nick """The user's nickname.""" self.user: Optional[str] = user """The user's local username. @@ -53,7 +52,7 @@ def __init__( Will be ``None`` if Sopel has not yet received complete user information from the IRC server. """ - self.channels: dict[identifiers.Identifier, 'Channel'] = {} + self.channels: dict[Identifier, 'Channel'] = {} """The channels the user is in. This maps channel name :class:`~sopel.tools.identifiers.Identifier`\\s @@ -123,11 +122,11 @@ class Channel: def __init__( self, - name: identifiers.Identifier, - identifier_factory: IdentifierFactory = identifiers.Identifier, + name: Identifier, + identifier_factory: IdentifierFactory = Identifier, ) -> None: - assert isinstance(name, identifiers.Identifier) - self.name: identifiers.Identifier = name + assert isinstance(name, Identifier) + self.name: Identifier = name """The name of the channel.""" self.make_identifier: IdentifierFactory = identifier_factory @@ -139,7 +138,7 @@ def __init__( """ self.users: dict[ - identifiers.Identifier, + Identifier, User, ] = memories.SopelIdentifierMemory( identifier_factory=self.make_identifier, @@ -150,7 +149,7 @@ def __init__( :class:`User` objects. """ self.privileges: dict[ - identifiers.Identifier, + Identifier, int, ] = memories.SopelIdentifierMemory( identifier_factory=self.make_identifier, @@ -177,17 +176,17 @@ def __init__( does not automatically populate all modes and lists. """ - self.last_who: Optional[datetime] = None + self.last_who: Optional[datetime.datetime] = None """The last time a WHO was requested for the channel.""" - self.join_time: Optional[datetime] = None + self.join_time: Optional[datetime.datetime] = None """The time the server acknowledged our JOIN message. Based on server-reported time if the ``server-time`` IRCv3 capability is available, otherwise the time Sopel received it. """ - def clear_user(self, nick: identifiers.Identifier) -> None: + def clear_user(self, nick: Identifier) -> None: """Remove ``nick`` from this channel. :param nick: the nickname of the user to remove @@ -426,11 +425,7 @@ def is_voiced(self, nick: str) -> bool: identifier = self.make_identifier(nick) return bool(self.privileges.get(identifier, 0) & privileges.VOICE) - def rename_user( - self, - old: identifiers.Identifier, - new: identifiers.Identifier, - ) -> None: + def rename_user(self, old: Identifier, new: Identifier) -> None: """Rename a user. :param old: the user's old nickname diff --git a/sopel/trigger.py b/sopel/trigger.py index 97fb09c7a3..3f04040daa 100644 --- a/sopel/trigger.py +++ b/sopel/trigger.py @@ -12,7 +12,6 @@ from datetime import datetime, timezone import re from typing import ( - Callable, cast, Match, Optional, @@ -21,7 +20,8 @@ ) from sopel import formatting, tools -from sopel.tools import identifiers, web +from sopel.tools import web +from sopel.tools.identifiers import Identifier, IdentifierFactory if TYPE_CHECKING: from sopel import config @@ -32,10 +32,6 @@ 'Trigger', ] - -IdentifierFactory = Callable[[str], identifiers.Identifier] - - COMMANDS_WITH_CONTEXT = frozenset({ 'INVITE', 'JOIN', @@ -163,13 +159,13 @@ class PreTrigger: def __init__( self, - own_nick: identifiers.Identifier, + own_nick: Identifier, line: str, url_schemes: Optional[Sequence] = None, - identifier_factory: IdentifierFactory = identifiers.Identifier, + identifier_factory: IdentifierFactory = Identifier, statusmsg_prefixes: tuple[str, ...] = tuple(), ): - self.make_identifier = identifier_factory + self.make_identifier: IdentifierFactory = identifier_factory line = line.strip('\r\n') self.line: str = line self.urls: tuple[str, ...] = tuple() @@ -235,11 +231,11 @@ def __init__( components_match = cast( Match, PreTrigger.component_regex.match(self.hostmask or '')) nick, self.user, self.host = components_match.groups() - self.nick: identifiers.Identifier = self.make_identifier(nick) + self.nick: Identifier = self.make_identifier(nick) # If we have arguments, the first one is *usually* the sender, # most numerics and certain general events (e.g. QUIT) excepted - target: Optional[identifiers.Identifier] = None + target: Optional[Identifier] = None status_prefix: Optional[str] = None if self.args and self.event in COMMANDS_WITH_CONTEXT: From 73cf3a3d3c3f65f60711c7924c8aec2ae44c9a1a Mon Sep 17 00:00:00 2001 From: Florian Strzelecki Date: Fri, 25 Aug 2023 22:54:51 +0200 Subject: [PATCH 05/14] docs: set maximum signature line length --- docs/source/conf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/source/conf.py b/docs/source/conf.py index 2f8c882f5a..fc70cb3c8d 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -111,6 +111,10 @@ # A list of ignored prefixes for module index sorting. modindex_common_prefix = ['sopel.'] +# If a signature’s length in characters exceeds the number set, each parameter +# within the signature will be displayed on an individual logical line. +maximum_signature_line_length = 80 + # -- Options for autodoc ------------------------------------------------------- From d4686ff7d56e01ffea961e4c293aa658367ae4b4 Mon Sep 17 00:00:00 2001 From: dgw Date: Sun, 1 Oct 2023 15:16:30 -0500 Subject: [PATCH 06/14] currency: replace `exchangerate.host` -> `open.er-api.com` exchangerate.host was bought and changed to require an API key. The new owner also allegedly broke compatibility with the original endpoints, based on GitHub comments from other developers, so I didn't even bother signing up for a key and testing it. I just found a new keyless API. ExchangeRate-API.com also provides key-based services, but there's an open endpoint that doesn't require any key we can use for now. VCR file for `currency` example test has been updated, as well. Note that this will be a "breaking" change for users who specify the `currency.fiat_provider` option in their settings as `exchangerate.host` since that's no longer a valid value. (Ask me how I know... I forgot to update the default at first and the test suite failed because of it. :D) --- sopel/modules/currency.py | 4 +- .../currency/test_example_exchange_cmd_0.yaml | 394 +++++++++++++----- .../currency/test_example_exchange_cmd_1.yaml | 378 +++++++++++++++++ 3 files changed, 661 insertions(+), 115 deletions(-) create mode 100644 test/vcr/modules/currency/test_example_exchange_cmd_1.yaml diff --git a/sopel/modules/currency.py b/sopel/modules/currency.py index c2a6387d8d..a6c9773edf 100644 --- a/sopel/modules/currency.py +++ b/sopel/modules/currency.py @@ -21,7 +21,7 @@ PLUGIN_OUTPUT_PREFIX = '[currency] ' FIAT_PROVIDERS = { - 'exchangerate.host': 'https://api.exchangerate.host/latest?base=EUR', + 'open.er-api.com': 'https://open.er-api.com/v6/latest/EUR', 'fixer.io': '//data.fixer.io/api/latest?base=EUR&access_key={}', } CRYPTO_URL = 'https://api.coingecko.com/api/v3/exchange_rates' @@ -43,7 +43,7 @@ class CurrencySection(types.StaticSection): fiat_provider = types.ChoiceAttribute( 'fiat_provider', list(FIAT_PROVIDERS.keys()), - default='exchangerate.host', + default='open.er-api.com', ) """Which data provider to use (some of which require no API key)""" fixer_io_key = types.ValidatedAttribute('fixer_io_key', default=None) diff --git a/test/vcr/modules/currency/test_example_exchange_cmd_0.yaml b/test/vcr/modules/currency/test_example_exchange_cmd_0.yaml index c445ed22e9..3fa2103fb3 100644 --- a/test/vcr/modules/currency/test_example_exchange_cmd_0.yaml +++ b/test/vcr/modules/currency/test_example_exchange_cmd_0.yaml @@ -5,45 +5,65 @@ interactions: Accept: - '*/*' Accept-Encoding: - - gzip, deflate + - gzip, deflate, br Connection: - keep-alive User-Agent: - - python-requests/2.25.1 + - python-requests/2.31.0 method: GET uri: https://api.coingecko.com/api/v3/exchange_rates response: body: - string: !!binary | - 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 - /wMAWDH8i3QSAAA= + string: "{\"rates\":{\"btc\":{\"name\":\"Bitcoin\",\"unit\":\"BTC\",\"value\":1.0,\"type\":\"crypto\"},\"eth\":{\"name\":\"Ether\",\"unit\":\"ETH\",\"value\":16.192,\"type\":\"crypto\"},\"ltc\":{\"name\":\"Litecoin\",\"unit\":\"LTC\",\"value\":408.142,\"type\":\"crypto\"},\"bch\":{\"name\":\"Bitcoin + Cash\",\"unit\":\"BCH\",\"value\":115.368,\"type\":\"crypto\"},\"bnb\":{\"name\":\"Binance + Coin\",\"unit\":\"BNB\",\"value\":126.626,\"type\":\"crypto\"},\"eos\":{\"name\":\"EOS\",\"unit\":\"EOS\",\"value\":45745.839,\"type\":\"crypto\"},\"xrp\":{\"name\":\"XRP\",\"unit\":\"XRP\",\"value\":52367.203,\"type\":\"crypto\"},\"xlm\":{\"name\":\"Lumens\",\"unit\":\"XLM\",\"value\":241520.224,\"type\":\"crypto\"},\"link\":{\"name\":\"Chainlink\",\"unit\":\"LINK\",\"value\":3476.143,\"type\":\"crypto\"},\"dot\":{\"name\":\"Polkadot\",\"unit\":\"DOT\",\"value\":6445.577,\"type\":\"crypto\"},\"yfi\":{\"name\":\"Yearn.finance\",\"unit\":\"YFI\",\"value\":5.003,\"type\":\"crypto\"},\"usd\":{\"name\":\"US + Dollar\",\"unit\":\"$\",\"value\":27116.789,\"type\":\"fiat\"},\"aed\":{\"name\":\"United + Arab Emirates Dirham\",\"unit\":\"DH\",\"value\":99601.052,\"type\":\"fiat\"},\"ars\":{\"name\":\"Argentine + Peso\",\"unit\":\"$\",\"value\":9526798.84,\"type\":\"fiat\"},\"aud\":{\"name\":\"Australian + Dollar\",\"unit\":\"A$\",\"value\":42213.058,\"type\":\"fiat\"},\"bdt\":{\"name\":\"Bangladeshi + Taka\",\"unit\":\"\u09F3\",\"value\":2996127.929,\"type\":\"fiat\"},\"bhd\":{\"name\":\"Bahraini + Dinar\",\"unit\":\"BD\",\"value\":10245.807,\"type\":\"fiat\"},\"bmd\":{\"name\":\"Bermudian + Dollar\",\"unit\":\"$\",\"value\":27116.789,\"type\":\"fiat\"},\"brl\":{\"name\":\"Brazil + Real\",\"unit\":\"R$\",\"value\":136783.973,\"type\":\"fiat\"},\"cad\":{\"name\":\"Canadian + Dollar\",\"unit\":\"CA$\",\"value\":36785.145,\"type\":\"fiat\"},\"chf\":{\"name\":\"Swiss + Franc\",\"unit\":\"Fr.\",\"value\":24789.68,\"type\":\"fiat\"},\"clp\":{\"name\":\"Chilean + Peso\",\"unit\":\"CLP$\",\"value\":24445453.168,\"type\":\"fiat\"},\"cny\":{\"name\":\"Chinese + Yuan\",\"unit\":\"\xA5\",\"value\":194522.288,\"type\":\"fiat\"},\"czk\":{\"name\":\"Czech + Koruna\",\"unit\":\"K\u010D\",\"value\":625648.055,\"type\":\"fiat\"},\"dkk\":{\"name\":\"Danish + Krone\",\"unit\":\"kr.\",\"value\":191315.023,\"type\":\"fiat\"},\"eur\":{\"name\":\"Euro\",\"unit\":\"\u20AC\",\"value\":25652.184,\"type\":\"fiat\"},\"gbp\":{\"name\":\"British + Pound Sterling\",\"unit\":\"\xA3\",\"value\":22217.761,\"type\":\"fiat\"},\"hkd\":{\"name\":\"Hong + Kong Dollar\",\"unit\":\"HK$\",\"value\":212349.543,\"type\":\"fiat\"},\"huf\":{\"name\":\"Hungarian + Forint\",\"unit\":\"Ft\",\"value\":9997133.179,\"type\":\"fiat\"},\"idr\":{\"name\":\"Indonesian + Rupiah\",\"unit\":\"Rp\",\"value\":420129908.636,\"type\":\"fiat\"},\"ils\":{\"name\":\"Israeli + New Shekel\",\"unit\":\"\u20AA\",\"value\":103334.871,\"type\":\"fiat\"},\"inr\":{\"name\":\"Indian + Rupee\",\"unit\":\"\u20B9\",\"value\":2255399.636,\"type\":\"fiat\"},\"jpy\":{\"name\":\"Japanese + Yen\",\"unit\":\"\xA5\",\"value\":4057214.026,\"type\":\"fiat\"},\"krw\":{\"name\":\"South + Korean Won\",\"unit\":\"\u20A9\",\"value\":36687117.846,\"type\":\"fiat\"},\"kwd\":{\"name\":\"Kuwaiti + Dinar\",\"unit\":\"KD\",\"value\":8393.866,\"type\":\"fiat\"},\"lkr\":{\"name\":\"Sri + Lankan Rupee\",\"unit\":\"Rs\",\"value\":8811643.795,\"type\":\"fiat\"},\"mmk\":{\"name\":\"Burmese + Kyat\",\"unit\":\"K\",\"value\":57066530.853,\"type\":\"fiat\"},\"mxn\":{\"name\":\"Mexican + Peso\",\"unit\":\"MX$\",\"value\":472305.106,\"type\":\"fiat\"},\"myr\":{\"name\":\"Malaysian + Ringgit\",\"unit\":\"RM\",\"value\":127320.105,\"type\":\"fiat\"},\"ngn\":{\"name\":\"Nigerian + Naira\",\"unit\":\"\u20A6\",\"value\":21144788.942,\"type\":\"fiat\"},\"nok\":{\"name\":\"Norwegian + Krone\",\"unit\":\"kr\",\"value\":290437.084,\"type\":\"fiat\"},\"nzd\":{\"name\":\"New + Zealand Dollar\",\"unit\":\"NZ$\",\"value\":45132.696,\"type\":\"fiat\"},\"php\":{\"name\":\"Philippine + Peso\",\"unit\":\"\u20B1\",\"value\":1536179.676,\"type\":\"fiat\"},\"pkr\":{\"name\":\"Pakistani + Rupee\",\"unit\":\"\u20A8\",\"value\":7846800.933,\"type\":\"fiat\"},\"pln\":{\"name\":\"Polish + Zloty\",\"unit\":\"z\u0142\",\"value\":118499.556,\"type\":\"fiat\"},\"rub\":{\"name\":\"Russian + Ruble\",\"unit\":\"\u20BD\",\"value\":2657445.358,\"type\":\"fiat\"},\"sar\":{\"name\":\"Saudi + Riyal\",\"unit\":\"SR\",\"value\":101701.518,\"type\":\"fiat\"},\"sek\":{\"name\":\"Swedish + Krona\",\"unit\":\"kr\",\"value\":296334.036,\"type\":\"fiat\"},\"sgd\":{\"name\":\"Singapore + Dollar\",\"unit\":\"S$\",\"value\":37001.401,\"type\":\"fiat\"},\"thb\":{\"name\":\"Thai + Baht\",\"unit\":\"\u0E3F\",\"value\":991135.328,\"type\":\"fiat\"},\"try\":{\"name\":\"Turkish + Lira\",\"unit\":\"\u20BA\",\"value\":743047.456,\"type\":\"fiat\"},\"twd\":{\"name\":\"New + Taiwan Dollar\",\"unit\":\"NT$\",\"value\":874413.413,\"type\":\"fiat\"},\"uah\":{\"name\":\"Ukrainian + hryvnia\",\"unit\":\"\u20B4\",\"value\":1003617.023,\"type\":\"fiat\"},\"vef\":{\"name\":\"Venezuelan + bol\xEDvar fuerte\",\"unit\":\"Bs.F\",\"value\":2715.204,\"type\":\"fiat\"},\"vnd\":{\"name\":\"Vietnamese + \u0111\u1ED3ng\",\"unit\":\"\u20AB\",\"value\":659055093.19,\"type\":\"fiat\"},\"zar\":{\"name\":\"South + African Rand\",\"unit\":\"R\",\"value\":512101.489,\"type\":\"fiat\"},\"xdr\":{\"name\":\"IMF + Special Drawing Rights\",\"unit\":\"XDR\",\"value\":20724.684,\"type\":\"fiat\"},\"xag\":{\"name\":\"Silver + - Troy Ounce\",\"unit\":\"XAG\",\"value\":1222.524,\"type\":\"commodity\"},\"xau\":{\"name\":\"Gold + - Troy Ounce\",\"unit\":\"XAU\",\"value\":14.673,\"type\":\"commodity\"},\"bits\":{\"name\":\"Bits\",\"unit\":\"\u03BCBTC\",\"value\":1000000.0,\"type\":\"crypto\"},\"sats\":{\"name\":\"Satoshi\",\"unit\":\"sats\",\"value\":100000000.0,\"type\":\"crypto\"}}}" headers: Access-Control-Allow-Headers: - Origin, X-Requested-With, Content-Type, Accept, Authorization @@ -55,44 +75,110 @@ interactions: - link, per-page, total Access-Control-Request-Method: - '*' - Age: - - '9' Alternate-Protocol: - 443:npn-spdy/2 CF-Cache-Status: - HIT CF-RAY: - - 65557e62db73882f-ORD + - 80f7953daa694c94-MSP Cache-Control: - - public, max-age=60 + - public, max-age=120 Connection: - keep-alive Content-Encoding: - - gzip + - br Content-Type: - application/json; charset=utf-8 Date: - - Wed, 26 May 2021 08:04:36 GMT + - Sun, 01 Oct 2023 20:51:19 GMT ETag: - - W/"2eeb08bb94c466379194ab1f4529841d" - Expect-CT: - - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" + - W/"730c99d03289ea4b2a5aa0bfebcdb8cb" Expires: - - Wed, 26 May 2021 08:05:36 GMT + - Sun, 01 Oct 2023 20:53:19 GMT + Referrer-Policy: + - strict-origin-when-cross-origin Server: - cloudflare + Set-Cookie: + - __cf_bm=hjvLAJlfDxTF66qjE81JNmCAeEFTg7kP8RhJVWzhhSY-1696193479-0-AXcmiCQ/V6/Hrb6y5qcs0Tq6pqaTXKvsxKE7XjtG88pMm/g3KAZWbKd+wUFpt+wKM89ewdWaWw69JiLHo77D9XY=; + path=/; expires=Sun, 01-Oct-23 21:21:19 GMT; domain=.api.coingecko.com; HttpOnly; + Secure; SameSite=None + Strict-Transport-Security: + - max-age=15724800; includeSubdomains Transfer-Encoding: - chunked Vary: - Accept-Encoding, Origin + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none X-Request-Id: - - fecbeaf4-c15e-4147-afe9-239787fc1a2b + - 868bf71e-512c-49d8-9985-261b15289653 X-Runtime: - - '0.015122' - alt-svc: - - h3-27=":443"; ma=86400, h3-28=":443"; ma=86400, h3-29=":443"; ma=86400 - cf-request-id: - - 0a494d51ca0000882f6685d000000001 + - '0.084620' + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 + method: GET + uri: https://open.er-api.com/v6/latest/EUR + response: + body: + string: '{"result":"success","provider":"https://www.exchangerate-api.com","documentation":"https://www.exchangerate-api.com/docs/free","terms_of_use":"https://www.exchangerate-api.com/terms","time_last_update_unix":1696118552,"time_last_update_utc":"Sun, + 01 Oct 2023 00:02:32 +0000","time_next_update_unix":1696206702,"time_next_update_utc":"Mon, + 02 Oct 2023 00:31:42 +0000","time_eol_unix":0,"base_code":"EUR","rates":{"EUR":1,"AED":3.884474,"AFN":82.54531,"ALL":106.503046,"AMD":418.721148,"ANG":1.893317,"AOA":883.989055,"ARS":370.212307,"AUD":1.640007,"AWG":1.893317,"AZN":1.802558,"BAM":1.95583,"BBD":2.115438,"BDT":116.561496,"BGN":1.95583,"BHD":0.397702,"BIF":2985.034948,"BMD":1.057719,"BND":1.444977,"BOB":7.307976,"BRL":5.308575,"BSD":1.057719,"BTN":87.983906,"BWP":14.494392,"BYN":3.047928,"BZD":2.115438,"CAD":1.430381,"CDF":2631.233871,"CHF":0.966998,"CLP":959.961674,"CNY":7.718692,"COP":4313.318245,"CRC":565.3924,"CUP":25.385261,"CVE":110.265,"CZK":24.386644,"DJF":187.978919,"DKK":7.460234,"DOP":59.93039,"DZD":145.150187,"EGP":32.700872,"ERN":15.865788,"ETB":58.978847,"FJD":2.426578,"FKP":0.866315,"FOK":7.46038,"GBP":0.866317,"GEL":2.838726,"GGP":0.866315,"GHS":12.44916,"GIP":0.866315,"GMD":69.052487,"GNF":9063.138889,"GTQ":8.298895,"GYD":220.902505,"HKD":8.280852,"HNL":26.054506,"HRK":7.5345,"HTG":143.102193,"HUF":389.735142,"IDR":16412.454774,"ILS":4.044046,"IMP":0.866315,"INR":87.984057,"IQD":1382.512712,"IRR":44632.813246,"ISK":144.900843,"JEP":0.866315,"JMD":164.858458,"JOD":0.749923,"JPY":157.965623,"KES":156.021652,"KGS":93.614746,"KHR":4350.306667,"KID":1.639995,"KMF":491.96775,"KRW":1426.682028,"KWD":0.326957,"KYD":0.881432,"KZT":506.925969,"LAK":21516.339237,"LBP":15865.78842,"LKR":342.37238,"LRD":199.407927,"LSL":19.98806,"LYD":5.161322,"MAD":10.877776,"MDL":19.301859,"MGA":4798.132353,"MKD":61.5016,"MMK":2661.95976,"MNT":3665.016674,"MOP":8.529255,"MRU":40.410329,"MUR":46.821469,"MVR":16.290227,"MWK":1186.843188,"MXN":18.452127,"MYR":4.974924,"MZN":67.580168,"NAD":19.98806,"NGN":907.79099,"NIO":38.662306,"NOK":11.293032,"NPR":140.77425,"NZD":1.761544,"OMR":0.40669,"PAB":1.057719,"PEN":4.007614,"PGK":3.843705,"PHP":59.996589,"PKR":302.72906,"PLN":4.626396,"PYG":7727.646261,"QAR":3.850098,"RON":4.973917,"RSD":117.203457,"RUB":103.138172,"RWF":1335.603898,"SAR":3.966447,"SBD":8.951671,"SCR":14.294945,"SDG":472.175109,"SEK":11.534893,"SGD":1.444979,"SHP":0.866315,"SLE":21.775238,"SLL":21782.609565,"SOS":603.092421,"SRD":40.692567,"SSP":1073.96241,"STN":24.5,"SYP":13649.896587,"SZL":19.98806,"THB":38.710832,"TJS":11.592476,"TMT":3.705225,"TND":3.349414,"TOP":2.49893,"TRY":29.021196,"TTD":7.491725,"TVD":1.639995,"TWD":34.045153,"TZS":2657.375386,"UAH":39.181942,"UGX":3975.922323,"USD":1.057361,"UYU":40.469499,"UZS":12985.975003,"VES":36.407959,"VND":25710.272165,"VUV":127.746609,"WST":2.927129,"XAF":655.957,"XCD":2.855842,"XDR":0.805514,"XOF":655.957,"XPF":119.332,"YER":264.02934,"ZAR":19.98818,"ZMW":22.203222,"ZWL":5782.7245}}' + headers: + Age: + - '61' + CF-Cache-Status: + - HIT + CF-RAY: + - 80f7953e7889e1db-ORD + Cache-Control: + - public, max-age=3600 + Connection: + - keep-alive + Content-Encoding: + - br + Content-Type: + - application/json + Date: + - Sun, 01 Oct 2023 20:51:19 GMT + Last-Modified: + - Sun, 01 Oct 2023 20:26:45 GMT + NEL: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=VaR0%2FFYZ8I4iCQD0Q8lLiVvgLTsdcJa6Tg7RX%2B8kp5zUKytrAqiRjKUNvUI%2B4WFGjRDYWIx7nPKCSNLVMycaX%2Fwr9%2FzaTEtGlnnDfMfTShS9Fev%2FVfjcqc5BxUUEXwzEWg%3D%3D"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + access-control-allow-origin: + - '*' + x-content-type-options: + - NOSNIFF + x-frame-options: + - SAMEORIGIN status: code: 200 message: OK @@ -102,108 +188,190 @@ interactions: Accept: - '*/*' Accept-Encoding: - - gzip, deflate + - gzip, deflate, br Connection: - keep-alive User-Agent: - - python-requests/2.25.1 + - python-requests/2.31.0 method: GET - uri: https://api.exchangerate.host/latest?base=EUR + uri: https://api.coingecko.com/api/v3/exchange_rates response: body: string: !!binary | - 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 - CwAA + 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 + nR5ykP55HawB66YlNFWde04tt0mJln7e+/zz headers: - Access-Control-Allow-Credentials: - - 'false' Access-Control-Allow-Headers: - - Origin, X-Requested-With, Content-Type, Accept, Methods + - Origin, X-Requested-With, Content-Type, Accept, Authorization Access-Control-Allow-Methods: - - GET + - POST, PUT, DELETE, GET, OPTIONS Access-Control-Allow-Origin: - '*' + Access-Control-Expose-Headers: + - link, per-page, total + Access-Control-Request-Method: + - '*' Age: - - '5972' + - '37' + Alternate-Protocol: + - 443:npn-spdy/2 CF-Cache-Status: - HIT CF-RAY: - - 65557e6a5c09110e-ORD + - 80f7a377ee244c94-MSP Cache-Control: - - max-age=31536000 + - public, max-age=120 Connection: - keep-alive Content-Encoding: - - gzip + - br Content-Type: - application/json; charset=utf-8 Date: - - Wed, 26 May 2021 08:04:38 GMT + - Sun, 01 Oct 2023 21:01:02 GMT ETag: - - W/"bac-xOQMnXJ7P8JGpGDZL8dHgRtLzMI" - Expect-CT: - - max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct" - NEL: - - '{"report_to":"cf-nel","max_age":604800}' - Report-To: - - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v2?s=FzL%2FKnFfJ30tU3IwnPqUc%2F601fwneecHTTO7qlfOu0IL2IcPb1Vc3qVTO%2F0N%2FBgJmNvDef7hHUT2Kbv8HAT3B0VFJtkTFUeuOiXgafgss5YwCE4YH%2Bj7t4migAmj0ylqEoud"}],"group":"cf-nel","max_age":604800}' + - W/"4cf0fd708b1693db791a0b37d93997ee" + Expires: + - Sun, 01 Oct 2023 21:03:02 GMT + Referrer-Policy: + - strict-origin-when-cross-origin Server: - cloudflare + Set-Cookie: + - __cf_bm=5UxxXqU.f3qSFhDvN3DCv3aLFd4UDhDW7gmM6ksvY2A-1696194062-0-AQ9tlsT69DycH8roZ+KZQt3jORqH5nUflkJkiZWZR4KkdFarvOKVS87bG3WewA2/IqbaET5fwj9cShbzBPWSW28=; + path=/; expires=Sun, 01-Oct-23 21:31:02 GMT; domain=.api.coingecko.com; HttpOnly; + Secure; SameSite=None Strict-Transport-Security: - - max-age=15552000; includeSubDomains + - max-age=15724800; includeSubdomains Transfer-Encoding: - chunked Vary: - - Accept-Encoding + - Accept-Encoding, Origin X-Content-Type-Options: - nosniff - X-DNS-Prefetch-Control: - - 'off' X-Download-Options: - noopen - X-Forwarded-For: - - api.exchangerate.host X-Frame-Options: - SAMEORIGIN - X-RateLimit-Limit: - - '2000' - X-RateLimit-Remaining: - - '1999' + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - 88109ab3-e11d-4952-87bf-94309a30c25e + X-Runtime: + - '0.126824' X-XSS-Protection: - - 1; mode=block - alt-svc: - - h3-27=":443"; ma=86400, h3-28=":443"; ma=86400, h3-29=":443"; ma=86400 - cf-request-id: - - 0a494d567b0000110e39081000000001 + - '0' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 + method: GET + uri: https://open.er-api.com/v6/latest/EUR + response: + body: + string: !!binary | + E6QLAMS3L9U6XV3vulj4I/wA3Ei1RMmM003Fi9NoQtLUSN7Nf38r8NQCziINw5/9CSiO3gRegtkN + cmK3de8FcnK6Fu5xA+Eu39rW1E+E0Cvc5aHbepbsv/hM1mAkXr99/fr8+grCAoQh8dPb25+v+f79 + P//5T0Yf1b8+vz3//fOfP/PXl99B+P7l67ffn/94+/z288sfsH7vv3/5+vr+h7+en0H4HwW7P778 + 8PHb6zOeue4iCG8///788bfPr28fv/35/ee354/f/vj5X0hp0URGrTpsJm9fkVi+/UHvirw7fn17 + p0XtXSlZNE3f/a2UUuJT/fH8L5NaWlOtTCX2L2Sls5LKP7/8FrdYIXz5/Pr88evL989IXN7OIPz1 + +e35FfnfWw8qhbC6nJDGY7h3J6yuDsihXL2aEFa7HVJK41qseCOs9hPSZXBXER+E1WGDFB5hJp2w + Oq6QYxjHiFIrYTUvSOuFVdRKJ6xuJ6Rw81JKJ6zuN0jhEWbSCaunQ3EoWusgrFf7ckVR6zDCej0h + lUWq2yCspzNSpHFt4tEI680BMYbrCVnYoveihPXNFVJjVC7m4YOw3s/GpdTeJQjrQ7sZ7h69E9bH + NbKzlR69EdbzDlnZyqi9EtYL/urPB+ToHMOiNML6/oQUZw+3UML68YA0Lt5DB2H9lAx3sXr6q2zF + hhAupiukNhNWs9GFcHF9hSwcrUUMwsXuhIwaHE1ad8LF4RHZuctooYSL4wnpJsYmQ70SLuYLZG2V + LdQJF7cnpFa2UbUJ4eLuEilSWFslXDxtkepsozV3wvThCimjc/QREoRpu0V29lbUnDAdT8gaHFYs + CNPThBSvLLXI6ITLzQlpyr2U0ZVwOR+QUnm02scgXJ7XyDo4+hjeCVcfJqSya6t9EK62J2Th0ZpJ + JVwdTw0bhM16wKCdsLncIZWHja6NsNnAN7O5XpCi7B7SCJubPhf7CdmCS1UfnbA5XCGjNGOxMUYQ + NufvkIM1xohK2DxOSNXCUbSWSrjeTkf2UUZVwvVhh9TGpXotjXA9z34+qnklXJ83SHFjKSphhOvb + K6SN4G5VXAk304yU5qLs1Xt3ws1uQToXd0Iqb/ZJ8DeHeSOOy0vthJvvJqTYUK6iXZRwM89I92bK + Q0zjDLhskeLOUcpwI3y4DMx+IIp7cx51eB2ED0dqzVn3CDXCh9MjUmrnaLWpEbaXC1Jq46LSqhK2 + mwUZxk28eyNsr2ekWy1spbXWCdsbMk60WURUwnZ/hfQQjtZ7JWzne6S4Nm5Diw7C9p7eUFvUTtg+ + sv3vNoa4KWH7dEbW0ji0RgvCbrVFqlRpbBZqnbBbn5BSR6vcx3Al7LYz0lzZutog7OYJKRHspYd2 + wm7ZISU4xiiNsHuckJWliakS9qsJKYVH7703wn7aISXYiowahP1mhfQeg8XUqhH22wnZhGuRRtjv + t0htTThq9EbYH85Ia61ykda6E/bHE3Jw1dBaCfv5FumFXYppEPa3M9IbDxVvQdjfzUhprFFUO2F/ + v0WKjMbDTcYg7B8OSBnsVUU7Yf84I52je6gT9k8HZOtcR5E2CIfVhJTgGKM0wmFzQEbp3KNEEA43 + R6QNbk2tNMLhuEWKsIYVU8LhNCPFC/fuWgkHrdgC9ybVnXDcz8jCXloLwmm11md8ujxsfpfSmzjh + tNkal9Stl0o4XYsQiFZHEE7LKRTlroqMTrsI2Zo2i0Y4PW6QvWvn5k0D8Xer2QhhLSUGYT5iYHQL + 6YRZ7zNIZy3mtRPm2zVSijjklq6E+f4KKWaVW7ERg7BEdYnW3DthWXe3qNK6EJaL2SIuScPDK2GZ + NkjvytKrlCAsl1ukCFfzEUZYNv6kDsJyTf7ntOwukSrce1UbhGW3Q6r0odxK1FYJy3FBtmJcQl2F + sMwT0gu30No6YVlOSCndOJq6EJbzAanOlbA8npBizYNHtDo6YXnaISU4xiiNcL5eI21wlzJMCecP + Sw6ZQ703wnl/Rhr3UlUr4Xx48XAzDxcnnI8n0Wcj3yzn+RGpwUVFohHO56nikI5X3E2CsR7n+wlp + zsWrVCOcnxakttrZerXRCLera6QFy5BwJdxuHpAWvXKomhrh1tDF1oRw+0gu2MIjCLdP1jH3MSpH + r6UY4e5yQVpjLz1qEO4OE1Jrl8LaVVol3N3eIUU7d2+tBOF+OSOVQ7toEB5WV8hWK0fthIeLybxQ + rYKEHiZC9BylVnHCw3F87dMVUiTYTAmPlzNSm3PRMCc8rebVGYkMwtP+HqnKWkxVCU/3O2TtQ7mr + 1///Hw== + headers: + Age: + - '3241' + CF-Cache-Status: + - HIT + CF-RAY: + - 80f7a378cbc98117-ORD + Cache-Control: + - public, max-age=3600 + Connection: + - keep-alive + Content-Encoding: + - br + Content-Type: + - application/json + Date: + - Sun, 01 Oct 2023 21:01:02 GMT + Last-Modified: + - Sun, 01 Oct 2023 19:26:09 GMT + NEL: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=RXf8uRjL1%2FXEULJmIVRhNNDHJUPpkNRSO2KA1G3g9g71zizQLVFQTlKAMPqZnUEcvwmXnYn0r8RchUhnZ7E8l38WDBJcD%2Fykkkl3nBzdtJBxhdunZSUFvt%2FRtMCZzAC23g%3D%3D"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + access-control-allow-origin: + - '*' + x-content-type-options: + - NOSNIFF + x-frame-options: + - SAMEORIGIN status: code: 200 message: OK diff --git a/test/vcr/modules/currency/test_example_exchange_cmd_1.yaml b/test/vcr/modules/currency/test_example_exchange_cmd_1.yaml new file mode 100644 index 0000000000..3fa2103fb3 --- /dev/null +++ b/test/vcr/modules/currency/test_example_exchange_cmd_1.yaml @@ -0,0 +1,378 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 + method: GET + uri: https://api.coingecko.com/api/v3/exchange_rates + response: + body: + string: "{\"rates\":{\"btc\":{\"name\":\"Bitcoin\",\"unit\":\"BTC\",\"value\":1.0,\"type\":\"crypto\"},\"eth\":{\"name\":\"Ether\",\"unit\":\"ETH\",\"value\":16.192,\"type\":\"crypto\"},\"ltc\":{\"name\":\"Litecoin\",\"unit\":\"LTC\",\"value\":408.142,\"type\":\"crypto\"},\"bch\":{\"name\":\"Bitcoin + Cash\",\"unit\":\"BCH\",\"value\":115.368,\"type\":\"crypto\"},\"bnb\":{\"name\":\"Binance + Coin\",\"unit\":\"BNB\",\"value\":126.626,\"type\":\"crypto\"},\"eos\":{\"name\":\"EOS\",\"unit\":\"EOS\",\"value\":45745.839,\"type\":\"crypto\"},\"xrp\":{\"name\":\"XRP\",\"unit\":\"XRP\",\"value\":52367.203,\"type\":\"crypto\"},\"xlm\":{\"name\":\"Lumens\",\"unit\":\"XLM\",\"value\":241520.224,\"type\":\"crypto\"},\"link\":{\"name\":\"Chainlink\",\"unit\":\"LINK\",\"value\":3476.143,\"type\":\"crypto\"},\"dot\":{\"name\":\"Polkadot\",\"unit\":\"DOT\",\"value\":6445.577,\"type\":\"crypto\"},\"yfi\":{\"name\":\"Yearn.finance\",\"unit\":\"YFI\",\"value\":5.003,\"type\":\"crypto\"},\"usd\":{\"name\":\"US + Dollar\",\"unit\":\"$\",\"value\":27116.789,\"type\":\"fiat\"},\"aed\":{\"name\":\"United + Arab Emirates Dirham\",\"unit\":\"DH\",\"value\":99601.052,\"type\":\"fiat\"},\"ars\":{\"name\":\"Argentine + Peso\",\"unit\":\"$\",\"value\":9526798.84,\"type\":\"fiat\"},\"aud\":{\"name\":\"Australian + Dollar\",\"unit\":\"A$\",\"value\":42213.058,\"type\":\"fiat\"},\"bdt\":{\"name\":\"Bangladeshi + Taka\",\"unit\":\"\u09F3\",\"value\":2996127.929,\"type\":\"fiat\"},\"bhd\":{\"name\":\"Bahraini + Dinar\",\"unit\":\"BD\",\"value\":10245.807,\"type\":\"fiat\"},\"bmd\":{\"name\":\"Bermudian + Dollar\",\"unit\":\"$\",\"value\":27116.789,\"type\":\"fiat\"},\"brl\":{\"name\":\"Brazil + Real\",\"unit\":\"R$\",\"value\":136783.973,\"type\":\"fiat\"},\"cad\":{\"name\":\"Canadian + Dollar\",\"unit\":\"CA$\",\"value\":36785.145,\"type\":\"fiat\"},\"chf\":{\"name\":\"Swiss + Franc\",\"unit\":\"Fr.\",\"value\":24789.68,\"type\":\"fiat\"},\"clp\":{\"name\":\"Chilean + Peso\",\"unit\":\"CLP$\",\"value\":24445453.168,\"type\":\"fiat\"},\"cny\":{\"name\":\"Chinese + Yuan\",\"unit\":\"\xA5\",\"value\":194522.288,\"type\":\"fiat\"},\"czk\":{\"name\":\"Czech + Koruna\",\"unit\":\"K\u010D\",\"value\":625648.055,\"type\":\"fiat\"},\"dkk\":{\"name\":\"Danish + Krone\",\"unit\":\"kr.\",\"value\":191315.023,\"type\":\"fiat\"},\"eur\":{\"name\":\"Euro\",\"unit\":\"\u20AC\",\"value\":25652.184,\"type\":\"fiat\"},\"gbp\":{\"name\":\"British + Pound Sterling\",\"unit\":\"\xA3\",\"value\":22217.761,\"type\":\"fiat\"},\"hkd\":{\"name\":\"Hong + Kong Dollar\",\"unit\":\"HK$\",\"value\":212349.543,\"type\":\"fiat\"},\"huf\":{\"name\":\"Hungarian + Forint\",\"unit\":\"Ft\",\"value\":9997133.179,\"type\":\"fiat\"},\"idr\":{\"name\":\"Indonesian + Rupiah\",\"unit\":\"Rp\",\"value\":420129908.636,\"type\":\"fiat\"},\"ils\":{\"name\":\"Israeli + New Shekel\",\"unit\":\"\u20AA\",\"value\":103334.871,\"type\":\"fiat\"},\"inr\":{\"name\":\"Indian + Rupee\",\"unit\":\"\u20B9\",\"value\":2255399.636,\"type\":\"fiat\"},\"jpy\":{\"name\":\"Japanese + Yen\",\"unit\":\"\xA5\",\"value\":4057214.026,\"type\":\"fiat\"},\"krw\":{\"name\":\"South + Korean Won\",\"unit\":\"\u20A9\",\"value\":36687117.846,\"type\":\"fiat\"},\"kwd\":{\"name\":\"Kuwaiti + Dinar\",\"unit\":\"KD\",\"value\":8393.866,\"type\":\"fiat\"},\"lkr\":{\"name\":\"Sri + Lankan Rupee\",\"unit\":\"Rs\",\"value\":8811643.795,\"type\":\"fiat\"},\"mmk\":{\"name\":\"Burmese + Kyat\",\"unit\":\"K\",\"value\":57066530.853,\"type\":\"fiat\"},\"mxn\":{\"name\":\"Mexican + Peso\",\"unit\":\"MX$\",\"value\":472305.106,\"type\":\"fiat\"},\"myr\":{\"name\":\"Malaysian + Ringgit\",\"unit\":\"RM\",\"value\":127320.105,\"type\":\"fiat\"},\"ngn\":{\"name\":\"Nigerian + Naira\",\"unit\":\"\u20A6\",\"value\":21144788.942,\"type\":\"fiat\"},\"nok\":{\"name\":\"Norwegian + Krone\",\"unit\":\"kr\",\"value\":290437.084,\"type\":\"fiat\"},\"nzd\":{\"name\":\"New + Zealand Dollar\",\"unit\":\"NZ$\",\"value\":45132.696,\"type\":\"fiat\"},\"php\":{\"name\":\"Philippine + Peso\",\"unit\":\"\u20B1\",\"value\":1536179.676,\"type\":\"fiat\"},\"pkr\":{\"name\":\"Pakistani + Rupee\",\"unit\":\"\u20A8\",\"value\":7846800.933,\"type\":\"fiat\"},\"pln\":{\"name\":\"Polish + Zloty\",\"unit\":\"z\u0142\",\"value\":118499.556,\"type\":\"fiat\"},\"rub\":{\"name\":\"Russian + Ruble\",\"unit\":\"\u20BD\",\"value\":2657445.358,\"type\":\"fiat\"},\"sar\":{\"name\":\"Saudi + Riyal\",\"unit\":\"SR\",\"value\":101701.518,\"type\":\"fiat\"},\"sek\":{\"name\":\"Swedish + Krona\",\"unit\":\"kr\",\"value\":296334.036,\"type\":\"fiat\"},\"sgd\":{\"name\":\"Singapore + Dollar\",\"unit\":\"S$\",\"value\":37001.401,\"type\":\"fiat\"},\"thb\":{\"name\":\"Thai + Baht\",\"unit\":\"\u0E3F\",\"value\":991135.328,\"type\":\"fiat\"},\"try\":{\"name\":\"Turkish + Lira\",\"unit\":\"\u20BA\",\"value\":743047.456,\"type\":\"fiat\"},\"twd\":{\"name\":\"New + Taiwan Dollar\",\"unit\":\"NT$\",\"value\":874413.413,\"type\":\"fiat\"},\"uah\":{\"name\":\"Ukrainian + hryvnia\",\"unit\":\"\u20B4\",\"value\":1003617.023,\"type\":\"fiat\"},\"vef\":{\"name\":\"Venezuelan + bol\xEDvar fuerte\",\"unit\":\"Bs.F\",\"value\":2715.204,\"type\":\"fiat\"},\"vnd\":{\"name\":\"Vietnamese + \u0111\u1ED3ng\",\"unit\":\"\u20AB\",\"value\":659055093.19,\"type\":\"fiat\"},\"zar\":{\"name\":\"South + African Rand\",\"unit\":\"R\",\"value\":512101.489,\"type\":\"fiat\"},\"xdr\":{\"name\":\"IMF + Special Drawing Rights\",\"unit\":\"XDR\",\"value\":20724.684,\"type\":\"fiat\"},\"xag\":{\"name\":\"Silver + - Troy Ounce\",\"unit\":\"XAG\",\"value\":1222.524,\"type\":\"commodity\"},\"xau\":{\"name\":\"Gold + - Troy Ounce\",\"unit\":\"XAU\",\"value\":14.673,\"type\":\"commodity\"},\"bits\":{\"name\":\"Bits\",\"unit\":\"\u03BCBTC\",\"value\":1000000.0,\"type\":\"crypto\"},\"sats\":{\"name\":\"Satoshi\",\"unit\":\"sats\",\"value\":100000000.0,\"type\":\"crypto\"}}}" + headers: + Access-Control-Allow-Headers: + - Origin, X-Requested-With, Content-Type, Accept, Authorization + Access-Control-Allow-Methods: + - POST, PUT, DELETE, GET, OPTIONS + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - link, per-page, total + Access-Control-Request-Method: + - '*' + Alternate-Protocol: + - 443:npn-spdy/2 + CF-Cache-Status: + - HIT + CF-RAY: + - 80f7953daa694c94-MSP + Cache-Control: + - public, max-age=120 + Connection: + - keep-alive + Content-Encoding: + - br + Content-Type: + - application/json; charset=utf-8 + Date: + - Sun, 01 Oct 2023 20:51:19 GMT + ETag: + - W/"730c99d03289ea4b2a5aa0bfebcdb8cb" + Expires: + - Sun, 01 Oct 2023 20:53:19 GMT + Referrer-Policy: + - strict-origin-when-cross-origin + Server: + - cloudflare + Set-Cookie: + - __cf_bm=hjvLAJlfDxTF66qjE81JNmCAeEFTg7kP8RhJVWzhhSY-1696193479-0-AXcmiCQ/V6/Hrb6y5qcs0Tq6pqaTXKvsxKE7XjtG88pMm/g3KAZWbKd+wUFpt+wKM89ewdWaWw69JiLHo77D9XY=; + path=/; expires=Sun, 01-Oct-23 21:21:19 GMT; domain=.api.coingecko.com; HttpOnly; + Secure; SameSite=None + Strict-Transport-Security: + - max-age=15724800; includeSubdomains + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Origin + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - 868bf71e-512c-49d8-9985-261b15289653 + X-Runtime: + - '0.084620' + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 + method: GET + uri: https://open.er-api.com/v6/latest/EUR + response: + body: + string: '{"result":"success","provider":"https://www.exchangerate-api.com","documentation":"https://www.exchangerate-api.com/docs/free","terms_of_use":"https://www.exchangerate-api.com/terms","time_last_update_unix":1696118552,"time_last_update_utc":"Sun, + 01 Oct 2023 00:02:32 +0000","time_next_update_unix":1696206702,"time_next_update_utc":"Mon, + 02 Oct 2023 00:31:42 +0000","time_eol_unix":0,"base_code":"EUR","rates":{"EUR":1,"AED":3.884474,"AFN":82.54531,"ALL":106.503046,"AMD":418.721148,"ANG":1.893317,"AOA":883.989055,"ARS":370.212307,"AUD":1.640007,"AWG":1.893317,"AZN":1.802558,"BAM":1.95583,"BBD":2.115438,"BDT":116.561496,"BGN":1.95583,"BHD":0.397702,"BIF":2985.034948,"BMD":1.057719,"BND":1.444977,"BOB":7.307976,"BRL":5.308575,"BSD":1.057719,"BTN":87.983906,"BWP":14.494392,"BYN":3.047928,"BZD":2.115438,"CAD":1.430381,"CDF":2631.233871,"CHF":0.966998,"CLP":959.961674,"CNY":7.718692,"COP":4313.318245,"CRC":565.3924,"CUP":25.385261,"CVE":110.265,"CZK":24.386644,"DJF":187.978919,"DKK":7.460234,"DOP":59.93039,"DZD":145.150187,"EGP":32.700872,"ERN":15.865788,"ETB":58.978847,"FJD":2.426578,"FKP":0.866315,"FOK":7.46038,"GBP":0.866317,"GEL":2.838726,"GGP":0.866315,"GHS":12.44916,"GIP":0.866315,"GMD":69.052487,"GNF":9063.138889,"GTQ":8.298895,"GYD":220.902505,"HKD":8.280852,"HNL":26.054506,"HRK":7.5345,"HTG":143.102193,"HUF":389.735142,"IDR":16412.454774,"ILS":4.044046,"IMP":0.866315,"INR":87.984057,"IQD":1382.512712,"IRR":44632.813246,"ISK":144.900843,"JEP":0.866315,"JMD":164.858458,"JOD":0.749923,"JPY":157.965623,"KES":156.021652,"KGS":93.614746,"KHR":4350.306667,"KID":1.639995,"KMF":491.96775,"KRW":1426.682028,"KWD":0.326957,"KYD":0.881432,"KZT":506.925969,"LAK":21516.339237,"LBP":15865.78842,"LKR":342.37238,"LRD":199.407927,"LSL":19.98806,"LYD":5.161322,"MAD":10.877776,"MDL":19.301859,"MGA":4798.132353,"MKD":61.5016,"MMK":2661.95976,"MNT":3665.016674,"MOP":8.529255,"MRU":40.410329,"MUR":46.821469,"MVR":16.290227,"MWK":1186.843188,"MXN":18.452127,"MYR":4.974924,"MZN":67.580168,"NAD":19.98806,"NGN":907.79099,"NIO":38.662306,"NOK":11.293032,"NPR":140.77425,"NZD":1.761544,"OMR":0.40669,"PAB":1.057719,"PEN":4.007614,"PGK":3.843705,"PHP":59.996589,"PKR":302.72906,"PLN":4.626396,"PYG":7727.646261,"QAR":3.850098,"RON":4.973917,"RSD":117.203457,"RUB":103.138172,"RWF":1335.603898,"SAR":3.966447,"SBD":8.951671,"SCR":14.294945,"SDG":472.175109,"SEK":11.534893,"SGD":1.444979,"SHP":0.866315,"SLE":21.775238,"SLL":21782.609565,"SOS":603.092421,"SRD":40.692567,"SSP":1073.96241,"STN":24.5,"SYP":13649.896587,"SZL":19.98806,"THB":38.710832,"TJS":11.592476,"TMT":3.705225,"TND":3.349414,"TOP":2.49893,"TRY":29.021196,"TTD":7.491725,"TVD":1.639995,"TWD":34.045153,"TZS":2657.375386,"UAH":39.181942,"UGX":3975.922323,"USD":1.057361,"UYU":40.469499,"UZS":12985.975003,"VES":36.407959,"VND":25710.272165,"VUV":127.746609,"WST":2.927129,"XAF":655.957,"XCD":2.855842,"XDR":0.805514,"XOF":655.957,"XPF":119.332,"YER":264.02934,"ZAR":19.98818,"ZMW":22.203222,"ZWL":5782.7245}}' + headers: + Age: + - '61' + CF-Cache-Status: + - HIT + CF-RAY: + - 80f7953e7889e1db-ORD + Cache-Control: + - public, max-age=3600 + Connection: + - keep-alive + Content-Encoding: + - br + Content-Type: + - application/json + Date: + - Sun, 01 Oct 2023 20:51:19 GMT + Last-Modified: + - Sun, 01 Oct 2023 20:26:45 GMT + NEL: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=VaR0%2FFYZ8I4iCQD0Q8lLiVvgLTsdcJa6Tg7RX%2B8kp5zUKytrAqiRjKUNvUI%2B4WFGjRDYWIx7nPKCSNLVMycaX%2Fwr9%2FzaTEtGlnnDfMfTShS9Fev%2FVfjcqc5BxUUEXwzEWg%3D%3D"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + access-control-allow-origin: + - '*' + x-content-type-options: + - NOSNIFF + x-frame-options: + - SAMEORIGIN + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 + method: GET + uri: https://api.coingecko.com/api/v3/exchange_rates + response: + body: + string: !!binary | + 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 + nR5ykP55HawB66YlNFWde04tt0mJln7e+/zz + headers: + Access-Control-Allow-Headers: + - Origin, X-Requested-With, Content-Type, Accept, Authorization + Access-Control-Allow-Methods: + - POST, PUT, DELETE, GET, OPTIONS + Access-Control-Allow-Origin: + - '*' + Access-Control-Expose-Headers: + - link, per-page, total + Access-Control-Request-Method: + - '*' + Age: + - '37' + Alternate-Protocol: + - 443:npn-spdy/2 + CF-Cache-Status: + - HIT + CF-RAY: + - 80f7a377ee244c94-MSP + Cache-Control: + - public, max-age=120 + Connection: + - keep-alive + Content-Encoding: + - br + Content-Type: + - application/json; charset=utf-8 + Date: + - Sun, 01 Oct 2023 21:01:02 GMT + ETag: + - W/"4cf0fd708b1693db791a0b37d93997ee" + Expires: + - Sun, 01 Oct 2023 21:03:02 GMT + Referrer-Policy: + - strict-origin-when-cross-origin + Server: + - cloudflare + Set-Cookie: + - __cf_bm=5UxxXqU.f3qSFhDvN3DCv3aLFd4UDhDW7gmM6ksvY2A-1696194062-0-AQ9tlsT69DycH8roZ+KZQt3jORqH5nUflkJkiZWZR4KkdFarvOKVS87bG3WewA2/IqbaET5fwj9cShbzBPWSW28=; + path=/; expires=Sun, 01-Oct-23 21:31:02 GMT; domain=.api.coingecko.com; HttpOnly; + Secure; SameSite=None + Strict-Transport-Security: + - max-age=15724800; includeSubdomains + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding, Origin + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - 88109ab3-e11d-4952-87bf-94309a30c25e + X-Runtime: + - '0.126824' + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 + method: GET + uri: https://open.er-api.com/v6/latest/EUR + response: + body: + string: !!binary | + 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 + 1///Hw== + headers: + Age: + - '3241' + CF-Cache-Status: + - HIT + CF-RAY: + - 80f7a378cbc98117-ORD + Cache-Control: + - public, max-age=3600 + Connection: + - keep-alive + Content-Encoding: + - br + Content-Type: + - application/json + Date: + - Sun, 01 Oct 2023 21:01:02 GMT + Last-Modified: + - Sun, 01 Oct 2023 19:26:09 GMT + NEL: + - '{"success_fraction":0,"report_to":"cf-nel","max_age":604800}' + Report-To: + - '{"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v3?s=RXf8uRjL1%2FXEULJmIVRhNNDHJUPpkNRSO2KA1G3g9g71zizQLVFQTlKAMPqZnUEcvwmXnYn0r8RchUhnZ7E8l38WDBJcD%2Fykkkl3nBzdtJBxhdunZSUFvt%2FRtMCZzAC23g%3D%3D"}],"group":"cf-nel","max_age":604800}' + Server: + - cloudflare + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + access-control-allow-origin: + - '*' + x-content-type-options: + - NOSNIFF + x-frame-options: + - SAMEORIGIN + status: + code: 200 + message: OK +version: 1 From 9bd18339022571aadd88ab5b85b4bb65b78403ad Mon Sep 17 00:00:00 2001 From: dgw Date: Sun, 1 Oct 2023 17:03:56 -0500 Subject: [PATCH 07/14] search: fix cases where `.suggest` gets only one item back `xmltodict` presents a response with only one `` element as just a plain OrderedDict, without any list wrapping it. --- sopel/modules/search.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/sopel/modules/search.py b/sopel/modules/search.py index 04fb39d562..f97efb8f05 100644 --- a/sopel/modules/search.py +++ b/sopel/modules/search.py @@ -213,9 +213,18 @@ def suggest(bot, trigger): answer = xmltodict.parse(response.text)['toplevel'] try: - answer = answer['CompleteSuggestion'][0]['suggestion']['@data'] + answer = answer['CompleteSuggestion'] + + try: + answer = answer[0] + except KeyError: + # only one suggestion; don't need to extract first item + pass + + answer = answer['suggestion']['@data'] except TypeError: answer = None + if answer: bot.say(answer) else: From 08238524e7a7f31bf15076403d22c148dddea093 Mon Sep 17 00:00:00 2001 From: dgw Date: Sun, 1 Oct 2023 17:08:58 -0500 Subject: [PATCH 08/14] search, test: update `.suggest` tests & help Tests now cover a case where the suggestion endpoint returns only one value (`.suggest sopel irc`). The responses for preexisting cases have been updated, too, since I shuffled the order of the examples. Which example to show in `.help suggest` is now explicitly specified using the `user_help` kwarg. --- sopel/modules/search.py | 3 +- .../search/test_example_suggest_0.yaml | 63 +++++++++++------ .../search/test_example_suggest_1.yaml | 56 +++++++++++++++ .../search/test_example_suggest_2.yaml | 70 ++++++++++++------- 4 files changed, 145 insertions(+), 47 deletions(-) create mode 100644 test/vcr/modules/search/test_example_suggest_1.yaml diff --git a/sopel/modules/search.py b/sopel/modules/search.py index f97efb8f05..a777481e44 100644 --- a/sopel/modules/search.py +++ b/sopel/modules/search.py @@ -186,8 +186,9 @@ def search(bot, trigger): @plugin.command('suggest') -@plugin.example('.suggest wikip', 'wikipedia', online=True, vcr=True) @plugin.example('.suggest', '.suggest what?') +@plugin.example('.suggest sopel irc', 'sopel irc', online=True, vcr=True) +@plugin.example('.suggest wikip', 'wikipedia', online=True, vcr=True, user_help=True) @plugin.example('.suggest lkashdfiauwgaef', 'Sorry, no result.', online=True, vcr=True) @plugin.output_prefix(PLUGIN_OUTPUT_PREFIX) def suggest(bot, trigger): diff --git a/test/vcr/modules/search/test_example_suggest_0.yaml b/test/vcr/modules/search/test_example_suggest_0.yaml index 9f3406a4ff..ec9fa24f7d 100644 --- a/test/vcr/modules/search/test_example_suggest_0.yaml +++ b/test/vcr/modules/search/test_example_suggest_0.yaml @@ -2,10 +2,14 @@ interactions: - request: body: null headers: - Accept: ['*/*'] - Accept-Encoding: ['gzip, deflate'] - Connection: [keep-alive] - User-Agent: [python-requests/2.24.0] + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 method: GET uri: https://suggestqueries.google.com/complete/search?output=toolbar&hl=en&q=lkashdfiauwgaef response: @@ -13,21 +17,38 @@ interactions: string: !!binary | H4sIAAAAAAAC/7Oxr8jNUShLLSrOzM+zVTLUM1Cyt7MpyS/ISS1LzbGz0YczAYEc0EoqAAAA headers: - Alt-Svc: ['h3-Q050=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-27=":443"; - ma=2592000,h3-T051=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"'] - Cache-Control: ['private, max-age=3600'] - Content-Encoding: [gzip] - Content-Type: [text/xml; charset=ISO-8859-1] - Date: ['Fri, 09 Oct 2020 04:40:44 GMT'] - Expires: ['Fri, 09 Oct 2020 04:40:44 GMT'] - P3P: [CP="This is not a P3P policy! See g.co/p3phelp for more info."] - Server: [gws] - Set-Cookie: ['1P_JAR=2020-10-09-04; expires=Sun, 08-Nov-2020 04:40:44 GMT; path=/; - domain=.google.com; Secure', 'NID=204=Pj4KwB7kHso7RFBcgEZMNDzLIhhuv8pFbIEWxp58KBEvUO9jquasGnvLCLcy0EfiJpPNvRfn_BIff0nkUzyqRJm4wSlrZ9tvmo7ohYEmAZerMGePW9wVWHCgMhCxArooUvVQ9W804M3QGhRlQ4ZZxwXnqGnfcSAA9h7DDDuRKLg; - expires=Sat, 10-Apr-2021 04:40:44 GMT; path=/; domain=.google.com; HttpOnly'] - Transfer-Encoding: [chunked] - X-Frame-Options: [SAMEORIGIN] - X-XSS-Protection: ['0'] - status: {code: 200, message: OK} + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Cache-Control: + - private, max-age=3600 + Content-Encoding: + - gzip + Content-Security-Policy: + - 'object-src ''none'';base-uri ''self'';script-src ''nonce-bqn-bde_sURx7gg6smeyow'' + ''strict-dynamic'' ''report-sample'' ''unsafe-eval'' ''unsafe-inline'' https: + http:;report-uri https://csp.withgoogle.com/csp/gws/xsrp' + Content-Type: + - text/xml; charset=ISO-8859-1 + Date: + - Sun, 01 Oct 2023 22:08:12 GMT + Expires: + - Sun, 01 Oct 2023 22:08:12 GMT + P3P: + - CP="This is not a P3P policy! See g.co/p3phelp for more info." + Server: + - gws + Set-Cookie: + - 1P_JAR=2023-10-01-22; expires=Tue, 31-Oct-2023 22:08:12 GMT; path=/; domain=.google.com; + Secure + - NID=511=dO-EpOs4FOklsnc4Pp3npRl1SVjfpgKMbw8to-Rx9usNNf_zbI02DRK-uIdRBYYvAVDOdu_zksAU1aRpNDsefpbsuAf54C2fz4RMgIrUH5i-MEnJ5kFBbrq-d8Of2g-Cp2BZDB0-_XyI1WT5VWdwpNprOyrNJVs4wtfLPxPe_DQ; + expires=Mon, 01-Apr-2024 22:08:12 GMT; path=/; domain=.google.com; HttpOnly + Transfer-Encoding: + - chunked + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK version: 1 diff --git a/test/vcr/modules/search/test_example_suggest_1.yaml b/test/vcr/modules/search/test_example_suggest_1.yaml new file mode 100644 index 0000000000..bf2fa11f28 --- /dev/null +++ b/test/vcr/modules/search/test_example_suggest_1.yaml @@ -0,0 +1,56 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 + method: GET + uri: https://suggestqueries.google.com/complete/search?output=toolbar&hl=en&q=wikip + response: + body: + string: !!binary | + H4sIAAAAAAAC/63TPQrDMAwF4KsYH6Bpd8cZeoSeQMQPR8R/2E6a4zdTpnYoaHuD9METyExHDGpH + bZzTqB+3u56s6bkE7AjWPHM8Y8dr8x6tn0PWtCsrR51G/eaVCxyTHqwZvq38x6gGqvMipSH5wE2M + 8xQhVrQArm5J9nCKk3TpXArSAo6oUqSjuqqYndgxKyWXo6LaeQ7C6k9tuH7lA1+7OeZLAwAA + headers: + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Cache-Control: + - private, max-age=3600 + Content-Encoding: + - gzip + Content-Security-Policy: + - 'object-src ''none'';base-uri ''self'';script-src ''nonce-tweQn7Ss8hox9zQe-xYVHA'' + ''strict-dynamic'' ''report-sample'' ''unsafe-eval'' ''unsafe-inline'' https: + http:;report-uri https://csp.withgoogle.com/csp/gws/xsrp' + Content-Type: + - text/xml; charset=ISO-8859-1 + Date: + - Sun, 01 Oct 2023 22:08:12 GMT + Expires: + - Sun, 01 Oct 2023 22:08:12 GMT + P3P: + - CP="This is not a P3P policy! See g.co/p3phelp for more info." + Server: + - gws + Set-Cookie: + - 1P_JAR=2023-10-01-22; expires=Tue, 31-Oct-2023 22:08:12 GMT; path=/; domain=.google.com; + Secure + - NID=511=GKP1S5d2fEPy67A69aYS0ZwYbNd_e7leYIAx6IRB4HxNWKE6YG-6ydKKf73w7khkbQ_LvVsrCodezyg_WiNpixCKw22_O0v3YtaiBlEGJ5tjKg7xQgW2wdGhVXo_svbIoz-SXgCyIsf-iMIGrKeUYQzTybvG5Jpc3PmvYcn62VQ; + expires=Mon, 01-Apr-2024 22:08:12 GMT; path=/; domain=.google.com; HttpOnly + Transfer-Encoding: + - chunked + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/test/vcr/modules/search/test_example_suggest_2.yaml b/test/vcr/modules/search/test_example_suggest_2.yaml index 4c8fbbef7c..490f472709 100644 --- a/test/vcr/modules/search/test_example_suggest_2.yaml +++ b/test/vcr/modules/search/test_example_suggest_2.yaml @@ -2,34 +2,54 @@ interactions: - request: body: null headers: - Accept: ['*/*'] - Accept-Encoding: ['gzip, deflate'] - Connection: [keep-alive] - User-Agent: [python-requests/2.24.0] + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Connection: + - keep-alive + User-Agent: + - python-requests/2.31.0 method: GET - uri: https://suggestqueries.google.com/complete/search?output=toolbar&hl=en&q=wikip + uri: https://suggestqueries.google.com/complete/search?output=toolbar&hl=en&q=sopel+irc response: body: string: !!binary | - H4sIAAAAAAAC/62TMQrDMAxFr2J8gNrN7DhDj9ATiFo4prYVbDXp8ZtSyFQoBW0fJD3e8OWmZ8lq - xdYT1VGfT1ZP3jEtGVfM3l2o7JHx+ogRO+9L3vUjqwAMo97SPS0YEmjjnfl28h9GYY059VkKF6jC - eyDFi1BQzA2B564GO1gpZIMaqChonG5ZTPRDFaBJCVVktVFjsZr8rrE5HuMFb2WKaDgDAAA= + H4sIAAAAAAAC/7Oxr8jNUShLLSrOzM+zVTLUM1Cyt7MpyS/ISS1LzbGzcc7PBTJLUoNL09NTi0uA + iuxsiuFshZTEkkRbpeL8gtQchcyiZCV9Oxt9bFr04SYCAKTyHI5xAAAA headers: - Alt-Svc: ['h3-Q050=":443"; ma=2592000,h3-29=":443"; ma=2592000,h3-27=":443"; - ma=2592000,h3-T051=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q046=":443"; - ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"'] - Cache-Control: ['private, max-age=3600'] - Content-Encoding: [gzip] - Content-Type: [text/xml; charset=ISO-8859-1] - Date: ['Fri, 09 Oct 2020 04:40:44 GMT'] - Expires: ['Fri, 09 Oct 2020 04:40:44 GMT'] - P3P: [CP="This is not a P3P policy! See g.co/p3phelp for more info."] - Server: [gws] - Set-Cookie: ['1P_JAR=2020-10-09-04; expires=Sun, 08-Nov-2020 04:40:44 GMT; path=/; - domain=.google.com; Secure', 'NID=204=xXUg2Mca4vSebU-u9utspHIfO-BbrJxaBASr5MUl5QdWU2MrASkdcw-iv_JpqhKoUDtgtJOnEIzx285qNJFJ0JNdXFvW63UXtfchTOsypgHlvQzF2W0VkkC9CvLQoo0TKj6NdY0weKL0TEF-LIqv-FsTEYyE_btmw0v9bLmyFY8; - expires=Sat, 10-Apr-2021 04:40:44 GMT; path=/; domain=.google.com; HttpOnly'] - Transfer-Encoding: [chunked] - X-Frame-Options: [SAMEORIGIN] - X-XSS-Protection: ['0'] - status: {code: 200, message: OK} + Alt-Svc: + - h3=":443"; ma=2592000,h3-29=":443"; ma=2592000 + Cache-Control: + - private, max-age=3600 + Content-Encoding: + - gzip + Content-Security-Policy: + - 'object-src ''none'';base-uri ''self'';script-src ''nonce-0QjS0PS1bkaLNV-ZwUrXtw'' + ''strict-dynamic'' ''report-sample'' ''unsafe-eval'' ''unsafe-inline'' https: + http:;report-uri https://csp.withgoogle.com/csp/gws/xsrp' + Content-Type: + - text/xml; charset=ISO-8859-1 + Date: + - Sun, 01 Oct 2023 22:08:13 GMT + Expires: + - Sun, 01 Oct 2023 22:08:13 GMT + P3P: + - CP="This is not a P3P policy! See g.co/p3phelp for more info." + Server: + - gws + Set-Cookie: + - 1P_JAR=2023-10-01-22; expires=Tue, 31-Oct-2023 22:08:13 GMT; path=/; domain=.google.com; + Secure + - NID=511=R9yxS4Vqfw-TpuqZFOmLmSnTAI9dE_3a_3kI-t4dINKpdeJb12NLhoIAA9S5O8uE1Lz1Qj00FYzA1bVuMEASytLNDl920QJDA8u7Yp-YiskoGG9T0FRMK4-QzMSAy4gBwmY-e2MT_5uYFM_FgFY7EoaeWpo6hotXXbbopRdg9Co; + expires=Mon, 01-Apr-2024 22:08:13 GMT; path=/; domain=.google.com; HttpOnly + Transfer-Encoding: + - chunked + X-Frame-Options: + - SAMEORIGIN + X-XSS-Protection: + - '0' + status: + code: 200 + message: OK version: 1 From fc40b2794cb9f060afa2b9c573ac1f84f5d2968c Mon Sep 17 00:00:00 2001 From: dgw Date: Sun, 1 Oct 2023 18:19:29 -0500 Subject: [PATCH 09/14] tools.memories: remove obsolete LGTM tricks We aren't using LGTM any more, and thus don't need to trick it into suppressing a warning about attributes that were added to `SopelMemory` vs. the base `dict` object it's extending. I don't think this'll do anything in CI, but it fixes a mypy error that seems to only appear on my system. Since there's no technical reason for these assignments to exist, rather than wast time figuring out how to fix the type-check error, let's just remove them. --- sopel/tools/memories.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/sopel/tools/memories.py b/sopel/tools/memories.py index aa21af7dbc..ab79922a2d 100644 --- a/sopel/tools/memories.py +++ b/sopel/tools/memories.py @@ -58,12 +58,6 @@ def __contains__(self, key): self.lock.release() return result - # Needed to make it explicit that we don't care about the `lock` attribute - # when comparing/hashing SopelMemory objects. - __eq__ = dict.__eq__ - __ne__ = dict.__ne__ - __hash__ = dict.__hash__ - class SopelMemoryWithDefault(defaultdict): """Same as SopelMemory, but subclasses from collections.defaultdict. From 0358b04e1048a5f4323bef2f15daaaa75cbc36ea Mon Sep 17 00:00:00 2001 From: dgw Date: Fri, 6 Oct 2023 16:55:23 -0500 Subject: [PATCH 10/14] README: fix link to *latest source* archive The old link went to the latest *release*, not master. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 071e5f8c6d..3877427d83 100644 --- a/README.rst +++ b/README.rst @@ -28,8 +28,8 @@ for installing from the latest source below. Latest source ============= First, either clone the repository with ``git clone -https://github.com/sopel-irc/sopel.git`` or download a tarball `from GitHub -`_. +https://github.com/sopel-irc/sopel.git`` or download a `source archive from +GitHub `_. Note: Sopel requires Python 3.8+ to run. From 278213fb53e7a28a2f5bd0431501d529b88adf15 Mon Sep 17 00:00:00 2001 From: James Gerity Date: Thu, 12 Oct 2023 17:55:54 -0400 Subject: [PATCH 11/14] contrib: drop Python 3.7 from tox config --- contrib/tox.ini | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/contrib/tox.ini b/contrib/tox.ini index 391d8b1158..ebcf3a424e 100644 --- a/contrib/tox.ini +++ b/contrib/tox.ini @@ -2,12 +2,12 @@ package_root = . min_version = 4.3.3 envlist = - py{37,38,39,310,311}-qa + py{38,39,310,311}-qa skip_missing_interpreters = true ignore_base_python_conflict = true labels = - lint = py{37,38,39,310,311}-lint - test = py{37,38,39,310,311}-test + lint = py{38,39,310,311}-lint + test = py{38,39,310,311}-test [testenv] @@ -16,13 +16,11 @@ package = sdist allowlist_externals = make envname = - py37: py37 py38: py38 py39: py39 py310: py310 py311: py311 envdir = - py37: {toxinidir}/.tox/py37 py38: {toxinidir}/.tox/py38 py39: {toxinidir}/.tox/py39 py310: {toxinidir}/.tox/py310 From 0c6f63c3126e050fc1213fcd5ee85b5a7f204728 Mon Sep 17 00:00:00 2001 From: James Gerity Date: Fri, 13 Oct 2023 21:55:34 -0400 Subject: [PATCH 12/14] lifecycle: simplify deprecation warning version check --- sopel/lifecycle.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sopel/lifecycle.py b/sopel/lifecycle.py index cbc51ce094..6210e8dea1 100644 --- a/sopel/lifecycle.py +++ b/sopel/lifecycle.py @@ -141,8 +141,10 @@ def decorator(func): @functools.wraps(func) def deprecated_func(*args, **kwargs): - if not (warning_in and - parse_version(warning_in) >= parse_version(__version__)): + warn_ver = warning_in and parse_version(warning_in) + this_ver = parse_version(__version__) + + if not (warn_ver and warn_ver >= this_ver): original_frame = inspect.stack()[-stack_frame] mod = inspect.getmodule(original_frame[0]) module_name = None From 586202311c4cb3a38bd91c5c99e1ae9d43471fd1 Mon Sep 17 00:00:00 2001 From: James Gerity Date: Fri, 13 Oct 2023 21:56:21 -0400 Subject: [PATCH 13/14] lifecycle: use release version only for deprecation warning --- sopel/lifecycle.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sopel/lifecycle.py b/sopel/lifecycle.py index 6210e8dea1..fbd897bad4 100644 --- a/sopel/lifecycle.py +++ b/sopel/lifecycle.py @@ -141,8 +141,8 @@ def decorator(func): @functools.wraps(func) def deprecated_func(*args, **kwargs): - warn_ver = warning_in and parse_version(warning_in) - this_ver = parse_version(__version__) + warn_ver = warning_in and parse_version(warning_in).release + this_ver = parse_version(__version__).release if not (warn_ver and warn_ver >= this_ver): original_frame = inspect.stack()[-stack_frame] From 8060ab7121ff8dbddeac20ed595056fa448c137d Mon Sep 17 00:00:00 2001 From: James Gerity Date: Fri, 13 Oct 2023 23:55:02 -0400 Subject: [PATCH 14/14] bot: add deprecation notice to SopelWrapper --- sopel/bot.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/sopel/bot.py b/sopel/bot.py index 28847c0ba1..f7747aa980 100644 --- a/sopel/bot.py +++ b/sopel/bot.py @@ -1279,6 +1279,14 @@ class SopelWrapper: their ``bot`` argument. It acts as a proxy, providing the ``trigger``'s ``sender`` (source channel or private message) as the default ``destination`` argument for overridden methods. + + .. deprecated:: 8.0 + + ``SopelWrapper`` is being replaced with a ``contextvars`` based + alternative. For more information, see `#2460`__. + + .. __: https://github.com/sopel-irc/sopel/issues/2460 + """ def __init__(self, sopel, trigger, output_prefix=''): if not output_prefix: