Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-certbot-dns-cloudflare for 
openSUSE:Factory checked in at 2025-06-16 12:26:25
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-certbot-dns-cloudflare (Old)
 and      /work/SRC/openSUSE:Factory/.python-certbot-dns-cloudflare.new.19631 
(New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-certbot-dns-cloudflare"

Mon Jun 16 12:26:25 2025 rev:45 rq:1286008 version:4.1.1

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-certbot-dns-cloudflare/python-certbot-dns-cloudflare.changes
      2025-04-22 17:29:43.734725420 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-certbot-dns-cloudflare.new.19631/python-certbot-dns-cloudflare.changes
   2025-06-16 12:26:27.068200320 +0200
@@ -1,0 +2,7 @@
+Fri Jun 13 14:50:42 UTC 2025 - Markéta Machová <mmach...@suse.com>
+
+- update to version 4.1.1
+  * Switched to src-layout from flat-layout to accommodate PEP 517 pip
+    editable installs
+
+-------------------------------------------------------------------

Old:
----
  certbot_dns_cloudflare-4.0.0.tar.gz

New:
----
  certbot_dns_cloudflare-4.1.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-certbot-dns-cloudflare.spec ++++++
--- /var/tmp/diff_new_pack.sJarsl/_old  2025-06-16 12:26:27.668225305 +0200
+++ /var/tmp/diff_new_pack.sJarsl/_new  2025-06-16 12:26:27.668225305 +0200
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-certbot-dns-cloudflare
-Version:        4.0.0
+Version:        4.1.1
 Release:        0
 Summary:        Cloudflare Authenticator plugin for Certbot
 License:        Apache-2.0

++++++ certbot_dns_cloudflare-4.0.0.tar.gz -> 
certbot_dns_cloudflare-4.1.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot_dns_cloudflare-4.0.0/MANIFEST.in 
new/certbot_dns_cloudflare-4.1.1/MANIFEST.in
--- old/certbot_dns_cloudflare-4.0.0/MANIFEST.in        2025-04-08 
00:03:33.000000000 +0200
+++ new/certbot_dns_cloudflare-4.1.1/MANIFEST.in        2025-06-12 
20:08:34.000000000 +0200
@@ -1,6 +1,6 @@
 include LICENSE.txt
 include README.rst
 recursive-include docs *
-include certbot_dns_cloudflare/py.typed
+include src/certbot_dns_cloudflare/py.typed
 global-exclude __pycache__
 global-exclude *.py[cod]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot_dns_cloudflare-4.0.0/PKG-INFO 
new/certbot_dns_cloudflare-4.1.1/PKG-INFO
--- old/certbot_dns_cloudflare-4.0.0/PKG-INFO   2025-04-08 00:03:36.605529000 
+0200
+++ new/certbot_dns_cloudflare-4.1.1/PKG-INFO   2025-06-12 20:08:38.535003200 
+0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: certbot-dns-cloudflare
-Version: 4.0.0
+Version: 4.1.1
 Summary: Cloudflare DNS Authenticator plugin for Certbot
 Home-page: https://github.com/certbot/certbot
 Author: Certbot Project
@@ -24,11 +24,11 @@
 Classifier: Topic :: System :: Networking
 Classifier: Topic :: System :: Systems Administration
 Classifier: Topic :: Utilities
-Requires-Python: >=3.9
+Requires-Python: >=3.9.2
 License-File: LICENSE.txt
 Requires-Dist: cloudflare<2.20,>=2.19
-Requires-Dist: acme>=4.0.0
-Requires-Dist: certbot>=4.0.0
+Requires-Dist: acme>=4.1.1
+Requires-Dist: certbot>=4.1.1
 Provides-Extra: docs
 Requires-Dist: Sphinx>=1.0; extra == "docs"
 Requires-Dist: sphinx_rtd_theme; extra == "docs"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare/__init__.py 
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare/__init__.py
--- old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare/__init__.py 
2025-04-08 00:03:33.000000000 +0200
+++ new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare/__init__.py 
1970-01-01 01:00:00.000000000 +0100
@@ -1,124 +0,0 @@
-"""
-The `~certbot_dns_cloudflare.dns_cloudflare` plugin automates the process of
-completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and
-subsequently removing, TXT records using the Cloudflare API.
-
-.. note::
-   The plugin is not installed by default. It can be installed by heading to
-   `certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, 
choosing your system and
-   selecting the Wildcard tab.
-
-Named Arguments
----------------
-
-========================================  =====================================
-``--dns-cloudflare-credentials``          Cloudflare credentials_ INI file.
-                                          (Required)
-``--dns-cloudflare-propagation-seconds``  The number of seconds to wait for DNS
-                                          to propagate before asking the ACME
-                                          server to verify the DNS record.
-                                          (Default: 10)
-========================================  =====================================
-
-
-Credentials
------------
-
-Use of this plugin requires a configuration file containing Cloudflare API
-credentials, obtained from your
-`Cloudflare dashboard 
<https://dash.cloudflare.com/?to=/:account/profile/api-tokens>`_.
-
-Previously, Cloudflare's "Global API Key" was used for authentication, however
-this key can access the entire Cloudflare API for all domains in your account,
-meaning it could cause a lot of damage if leaked.
-
-Cloudflare's newer API Tokens can be restricted to specific domains and
-operations, and are therefore now the recommended authentication option.
-
-The Token needed by Certbot requires ``Zone:DNS:Edit`` permissions for only the
-zones you need certificates for.
-
-Using Cloudflare Tokens also requires at least version 2.3.1 of the 
``cloudflare``
-Python module. If the version that automatically installed with this plugin is
-older than that, and you can't upgrade it on your system, you'll have to stick 
to
-the Global key.
-
-.. code-block:: ini
-   :name: certbot_cloudflare_token.ini
-   :caption: Example credentials file using restricted API Token (recommended):
-
-   # Cloudflare API token used by Certbot
-   dns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567
-
-.. code-block:: ini
-   :name: certbot_cloudflare_key.ini
-   :caption: Example credentials file using Global API Key (not recommended):
-
-   # Cloudflare API credentials used by Certbot
-   dns_cloudflare_email = cloudfl...@example.com
-   dns_cloudflare_api_key = 0123456789abcdef0123456789abcdef01234
-
-The path to this file can be provided interactively or using the
-``--dns-cloudflare-credentials`` command-line argument. Certbot records the 
path
-to this file for use during renewal, but does not store the file's contents.
-
-.. caution::
-   You should protect these API credentials as you would the password to your
-   Cloudflare account. Users who can read this file can use these credentials
-   to issue arbitrary API calls on your behalf. Users who can cause Certbot to
-   run using these credentials can complete a ``dns-01`` challenge to acquire
-   new certificates or revoke existing certificates for associated domains,
-   even if those domains aren't being managed by this server.
-
-Certbot will emit a warning if it detects that the credentials file can be
-accessed by other users on your system. The warning reads "Unsafe permissions
-on credentials configuration file", followed by the path to the credentials
-file. This warning will be emitted each time Certbot uses the credentials file,
-including for renewal, and cannot be silenced except by addressing the issue
-(e.g., by using a command like ``chmod 600`` to restrict access to the file).
-
-.. note::
-    Please note that the ``cloudflare`` Python module used by the plugin has
-    additional methods of providing credentials to the module, e.g. environment
-    variables or the ``cloudflare.cfg`` configuration file. These methods are 
not
-    supported by Certbot. If any of those additional methods of providing
-    credentials is being used, they must provide the same credentials (i.e.,
-    email and API key *or* an API token) as the credentials file provided to
-    Certbot. If there is a discrepancy, the ``cloudflare`` Python module will
-    raise an error. Also note that the credentials provided to Certbot will 
take
-    precedence over any other method of providing credentials to the 
``cloudflare``
-    Python module.
-
-
-Examples
---------
-
-.. code-block:: bash
-   :caption: To acquire a certificate for ``example.com``
-
-   certbot certonly \\
-     --dns-cloudflare \\
-     --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \\
-     -d example.com
-
-.. code-block:: bash
-   :caption: To acquire a single certificate for both ``example.com`` and
-             ``www.example.com``
-
-   certbot certonly \\
-     --dns-cloudflare \\
-     --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \\
-     -d example.com \\
-     -d www.example.com
-
-.. code-block:: bash
-   :caption: To acquire a certificate for ``example.com``, waiting 60 seconds
-             for DNS propagation
-
-   certbot certonly \\
-     --dns-cloudflare \\
-     --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \\
-     --dns-cloudflare-propagation-seconds 60 \\
-     -d example.com
-
-"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare/_internal/__init__.py 
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare/_internal/__init__.py
--- 
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare/_internal/__init__.py   
    2025-04-08 00:03:33.000000000 +0200
+++ 
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare/_internal/__init__.py   
    1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-"""Internal implementation of `~certbot_dns_cloudflare.dns_cloudflare` 
plugin."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare/_internal/dns_cloudflare.py
 
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare/_internal/dns_cloudflare.py
--- 
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare/_internal/dns_cloudflare.py
 2025-04-08 00:03:33.000000000 +0200
+++ 
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare/_internal/dns_cloudflare.py
 1970-01-01 01:00:00.000000000 +0100
@@ -1,273 +0,0 @@
-"""DNS Authenticator for Cloudflare."""
-import logging
-from typing import Any
-from typing import Callable
-from typing import Dict
-from typing import List
-from typing import Optional
-from typing import cast
-
-import CloudFlare
-
-from certbot import errors
-from certbot.plugins import dns_common
-from certbot.plugins.dns_common import CredentialsConfiguration
-
-logger = logging.getLogger(__name__)
-
-ACCOUNT_URL = 'https://dash.cloudflare.com/?to=/:account/profile/api-tokens'
-
-
-class Authenticator(dns_common.DNSAuthenticator):
-    """DNS Authenticator for Cloudflare
-
-    This Authenticator uses the Cloudflare API to fulfill a dns-01 challenge.
-    """
-
-    description = ('Obtain certificates using a DNS TXT record (if you are 
using Cloudflare for '
-                   'DNS).')
-    ttl = 120
-
-    def __init__(self, *args: Any, **kwargs: Any) -> None:
-        super().__init__(*args, **kwargs)
-        self.credentials: Optional[CredentialsConfiguration] = None
-
-    @classmethod
-    def add_parser_arguments(cls, add: Callable[..., None],
-                             default_propagation_seconds: int = 10) -> None:
-        super().add_parser_arguments(add, default_propagation_seconds)
-        add('credentials', help='Cloudflare credentials INI file.')
-
-    def more_info(self) -> str:
-        return 'This plugin configures a DNS TXT record to respond to a dns-01 
challenge using ' + \
-               'the Cloudflare API.'
-
-    def _validate_credentials(self, credentials: CredentialsConfiguration) -> 
None:
-        token = credentials.conf('api-token')
-        email = credentials.conf('email')
-        key = credentials.conf('api-key')
-        if token:
-            if email or key:
-                raise errors.PluginError('{}: dns_cloudflare_email and 
dns_cloudflare_api_key are '
-                                         'not needed when using an API Token'
-                                         .format(credentials.confobj.filename))
-        elif email or key:
-            if not email:
-                raise errors.PluginError('{}: dns_cloudflare_email is required 
when using a Global '
-                                         'API Key. (should be email address 
associated with '
-                                         'Cloudflare 
account)'.format(credentials.confobj.filename))
-            if not key:
-                raise errors.PluginError('{}: dns_cloudflare_api_key is 
required when using a '
-                                         'Global API Key. (see {})'
-                                         .format(credentials.confobj.filename, 
ACCOUNT_URL))
-        else:
-            raise errors.PluginError('{}: Either dns_cloudflare_api_token 
(recommended), or '
-                                     'dns_cloudflare_email and 
dns_cloudflare_api_key are required.'
-                                     ' (see 
{})'.format(credentials.confobj.filename, ACCOUNT_URL))
-
-    def _setup_credentials(self) -> None:
-        self.credentials = self._configure_credentials(
-            'credentials',
-            'Cloudflare credentials INI file',
-            None,
-            self._validate_credentials
-        )
-
-    def _perform(self, domain: str, validation_name: str, validation: str) -> 
None:
-        self._get_cloudflare_client().add_txt_record(domain, validation_name, 
validation, self.ttl)
-
-    def _cleanup(self, domain: str, validation_name: str, validation: str) -> 
None:
-        self._get_cloudflare_client().del_txt_record(domain, validation_name, 
validation)
-
-    def _get_cloudflare_client(self) -> "_CloudflareClient":
-        if not self.credentials:  # pragma: no cover
-            raise errors.Error("Plugin has not been prepared.")
-        if self.credentials.conf('api-token'):
-            return _CloudflareClient(api_token = 
self.credentials.conf('api-token'))
-        return _CloudflareClient(email = self.credentials.conf('email'),
-                                 api_key = self.credentials.conf('api-key'))
-
-
-class _CloudflareClient:
-    """
-    Encapsulates all communication with the Cloudflare API.
-    """
-
-    def __init__(self, email: Optional[str] = None, api_key: Optional[str] = 
None,
-                 api_token: Optional[str] = None) -> None:
-        if email:
-            # If an email was specified, we're using an email/key combination 
and not a token.
-            # We can't use named arguments in this case, as it would break 
compatibility with
-            # the Cloudflare library since version 2.10.1, as the `token` 
argument was used for
-            # tokens and keys alike and the `key` argument did not exist in 
earlier versions.
-            self.cf = CloudFlare.CloudFlare(email, api_key)
-        else:
-            # If no email was specified, we're using just a token. Let's use 
the named argument
-            # for simplicity, which is compatible with all (current) versions 
of the Cloudflare
-            # library.
-            self.cf = CloudFlare.CloudFlare(token=api_token)
-
-    def add_txt_record(self, domain: str, record_name: str, record_content: 
str,
-                       record_ttl: int) -> None:
-        """
-        Add a TXT record using the supplied information.
-
-        :param str domain: The domain to use to look up the Cloudflare zone.
-        :param str record_name: The record name (typically beginning with 
'_acme-challenge.').
-        :param str record_content: The record content (typically the challenge 
validation).
-        :param int record_ttl: The record TTL (number of seconds that the 
record may be cached).
-        :raises certbot.errors.PluginError: if an error occurs communicating 
with the Cloudflare API
-        """
-
-        zone_id = self._find_zone_id(domain)
-
-        data = {'type': 'TXT',
-                'name': record_name,
-                'content': record_content,
-                'ttl': record_ttl}
-
-        try:
-            logger.debug('Attempting to add record to zone %s: %s', zone_id, 
data)
-            self.cf.zones.dns_records.post(zone_id, data=data)  # zones | 
pylint: disable=no-member
-        except CloudFlare.exceptions.CloudFlareAPIError as e:
-            code = int(e)
-            hint = None
-
-            if code == 1009:
-                hint = 'Does your API token have "Zone:DNS:Edit" permissions?'
-
-            logger.error('Encountered CloudFlareAPIError adding TXT record: %d 
%s', e, e)
-            raise errors.PluginError('Error communicating with the Cloudflare 
API: {0}{1}'
-                                     .format(e, ' ({0})'.format(hint) if hint 
else ''))
-
-        record_id = self._find_txt_record_id(zone_id, record_name, 
record_content)
-        logger.debug('Successfully added TXT record with record_id: %s', 
record_id)
-
-    def del_txt_record(self, domain: str, record_name: str, record_content: 
str) -> None:
-        """
-        Delete a TXT record using the supplied information.
-
-        Note that both the record's name and content are used to ensure that 
similar records
-        created concurrently (e.g., due to concurrent invocations of this 
plugin) are not deleted.
-
-        Failures are logged, but not raised.
-
-        :param str domain: The domain to use to look up the Cloudflare zone.
-        :param str record_name: The record name (typically beginning with 
'_acme-challenge.').
-        :param str record_content: The record content (typically the challenge 
validation).
-        """
-
-        try:
-            zone_id = self._find_zone_id(domain)
-        except errors.PluginError as e:
-            logger.debug('Encountered error finding zone_id during deletion: 
%s', e)
-            return
-
-        if zone_id:
-            record_id = self._find_txt_record_id(zone_id, record_name, 
record_content)
-            if record_id:
-                try:
-                    # zones | pylint: disable=no-member
-                    self.cf.zones.dns_records.delete(zone_id, record_id)
-                    logger.debug('Successfully deleted TXT record.')
-                except CloudFlare.exceptions.CloudFlareAPIError as e:
-                    logger.warning('Encountered CloudFlareAPIError deleting 
TXT record: %s', e)
-            else:
-                logger.debug('TXT record not found; no cleanup needed.')
-        else:
-            logger.debug('Zone not found; no cleanup needed.')
-
-    def _find_zone_id(self, domain: str) -> str:
-        """
-        Find the zone_id for a given domain.
-
-        :param str domain: The domain for which to find the zone_id.
-        :returns: The zone_id, if found.
-        :rtype: str
-        :raises certbot.errors.PluginError: if no zone_id is found.
-        """
-
-        zone_name_guesses = dns_common.base_domain_name_guesses(domain)
-        zones: List[Dict[str, Any]] = []
-        code = msg = None
-
-        for zone_name in zone_name_guesses:
-            params = {'name': zone_name,
-                      'per_page': 1}
-
-            try:
-                zones = self.cf.zones.get(params=params)  # zones | pylint: 
disable=no-member
-            except CloudFlare.exceptions.CloudFlareAPIError as e:
-                code = int(e)
-                msg = str(e)
-                hint = None
-
-                if code == 6003:
-                    hint = ('Did you copy your entire API token/key? To use 
Cloudflare tokens, '
-                            'you\'ll need the python package 
cloudflare>=2.3.1.{}'
-                    .format(' This certbot is running cloudflare ' + 
str(CloudFlare.__version__)
-                    if hasattr(CloudFlare, '__version__') else ''))
-                elif code == 9103:
-                    hint = 'Did you enter the correct email address and Global 
key?'
-                elif code == 9109:
-                    hint = 'Did you enter a valid Cloudflare Token?'
-
-                if hint:
-                    raise errors.PluginError('Error determining zone_id: {0} 
{1}. Please confirm '
-                                  'that you have supplied valid Cloudflare API 
credentials. ({2})'
-                                                                         
.format(code, msg, hint))
-                else:
-                    logger.debug('Unrecognised CloudFlareAPIError while 
finding zone_id: %d %s. '
-                                 'Continuing with next zone guess...', e, e)
-
-            if zones:
-                zone_id: str = zones[0]['id']
-                logger.debug('Found zone_id of %s for %s using name %s', 
zone_id, domain, zone_name)
-                return zone_id
-
-        if msg is not None:
-            if 'com.cloudflare.api.account.zone.list' in msg:
-                raise errors.PluginError('Unable to determine zone_id for {0} 
using zone names: '
-                                         '{1}. Please confirm that the domain 
name has been '
-                                         'entered correctly and your 
Cloudflare Token has access '
-                                         'to the domain.'.format(domain, 
zone_name_guesses))
-            else:
-                raise errors.PluginError('Unable to determine zone_id for {0} 
using zone names: '
-                                         '{1}. The error from Cloudflare was: 
{2} {3}.'
-                                         .format(domain, zone_name_guesses, 
code, msg))
-        else:
-            raise errors.PluginError('Unable to determine zone_id for {0} 
using zone names: '
-                                     '{1}. Please confirm that the domain name 
has been '
-                                     'entered correctly and is already 
associated with the '
-                                     'supplied Cloudflare account.'
-                                     .format(domain, zone_name_guesses))
-
-    def _find_txt_record_id(self, zone_id: str, record_name: str,
-                            record_content: str) -> Optional[str]:
-        """
-        Find the record_id for a TXT record with the given name and content.
-
-        :param str zone_id: The zone_id which contains the record.
-        :param str record_name: The record name (typically beginning with 
'_acme-challenge.').
-        :param str record_content: The record content (typically the challenge 
validation).
-        :returns: The record_id, if found.
-        :rtype: str
-        """
-
-        params = {'type': 'TXT',
-                  'name': record_name,
-                  'content': record_content,
-                  'per_page': 1}
-        try:
-            # zones | pylint: disable=no-member
-            records = self.cf.zones.dns_records.get(zone_id, params=params)
-        except CloudFlare.exceptions.CloudFlareAPIError as e:
-            logger.debug('Encountered CloudFlareAPIError getting TXT 
record_id: %s', e)
-            records = []
-
-        if records:
-            # Cleanup is returning the system to the state we found it. If, 
for some reason,
-            # there are multiple matching records, we only delete one because 
we only added one.
-            return cast(str, records[0]['id'])
-        logger.debug('Unable to find TXT record.')
-        return None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare/_internal/tests/__init__.py
 
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare/_internal/tests/__init__.py
--- 
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare/_internal/tests/__init__.py
 2025-04-08 00:03:33.000000000 +0200
+++ 
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare/_internal/tests/__init__.py
 1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-"""certbot-dns-cloudflare tests"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
 
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
--- 
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
      2025-04-08 00:03:33.000000000 +0200
+++ 
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
      1970-01-01 01:00:00.000000000 +0100
@@ -1,232 +0,0 @@
-"""Tests for certbot_dns_cloudflare._internal.dns_cloudflare."""
-
-import sys
-import unittest
-from unittest import mock
-
-import CloudFlare
-import pytest
-
-from certbot import errors
-from certbot.compat import os
-from certbot.plugins import dns_test_common
-from certbot.plugins.dns_test_common import DOMAIN
-from certbot.tests import util as test_util
-
-API_ERROR = CloudFlare.exceptions.CloudFlareAPIError(1000, '', '')
-
-API_TOKEN = 'an-api-token'
-
-API_KEY = 'an-api-key'
-EMAIL = 'exam...@example.com'
-
-
-class AuthenticatorTest(test_util.TempDirTestCase, 
dns_test_common.BaseAuthenticatorTest):
-
-    def setUp(self):
-        from certbot_dns_cloudflare._internal.dns_cloudflare import 
Authenticator
-
-        super().setUp()
-
-        path = os.path.join(self.tempdir, 'file.ini')
-        dns_test_common.write({"cloudflare_email": EMAIL, 
"cloudflare_api_key": API_KEY}, path)
-
-        self.config = mock.MagicMock(cloudflare_credentials=path,
-                                     cloudflare_propagation_seconds=0)  # 
don't wait during tests
-
-        self.auth = Authenticator(self.config, "cloudflare")
-
-        self.mock_client = mock.MagicMock()
-        # _get_cloudflare_client | pylint: disable=protected-access
-        # workaround for wont-fix https://github.com/python/mypy/issues/2427 
that works with
-        # both strict and non-strict mypy
-        setattr(self.auth, '_get_cloudflare_client', 
mock.MagicMock(return_value=self.mock_client))
-
-    @test_util.patch_display_util()
-    def test_perform(self, unused_mock_get_utility):
-        self.auth.perform([self.achall])
-
-        expected = [mock.call.add_txt_record(DOMAIN, 
'_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]
-        assert expected == self.mock_client.mock_calls
-
-    def test_cleanup(self):
-        # _attempt_cleanup | pylint: disable=protected-access
-        self.auth._attempt_cleanup = True
-        self.auth.cleanup([self.achall])
-
-        expected = [mock.call.del_txt_record(DOMAIN, 
'_acme-challenge.'+DOMAIN, mock.ANY)]
-        assert expected == self.mock_client.mock_calls
-
-    @test_util.patch_display_util()
-    def test_api_token(self, unused_mock_get_utility):
-        dns_test_common.write({"cloudflare_api_token": API_TOKEN},
-                              self.config.cloudflare_credentials)
-        self.auth.perform([self.achall])
-
-        expected = [mock.call.add_txt_record(DOMAIN, 
'_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]
-        assert expected == self.mock_client.mock_calls
-
-    def test_no_creds(self):
-        dns_test_common.write({}, self.config.cloudflare_credentials)
-        with pytest.raises(errors.PluginError):
-            self.auth.perform([self.achall])
-
-    def test_missing_email_or_key(self):
-        dns_test_common.write({"cloudflare_api_key": API_KEY}, 
self.config.cloudflare_credentials)
-        with pytest.raises(errors.PluginError):
-            self.auth.perform([self.achall])
-
-        dns_test_common.write({"cloudflare_email": EMAIL}, 
self.config.cloudflare_credentials)
-        with pytest.raises(errors.PluginError):
-            self.auth.perform([self.achall])
-
-    def test_email_or_key_with_token(self):
-        dns_test_common.write({"cloudflare_api_token": API_TOKEN, 
"cloudflare_email": EMAIL},
-                              self.config.cloudflare_credentials)
-        with pytest.raises(errors.PluginError):
-            self.auth.perform([self.achall])
-
-        dns_test_common.write({"cloudflare_api_token": API_TOKEN, 
"cloudflare_api_key": API_KEY},
-                              self.config.cloudflare_credentials)
-        with pytest.raises(errors.PluginError):
-            self.auth.perform([self.achall])
-
-        dns_test_common.write({"cloudflare_api_token": API_TOKEN, 
"cloudflare_email": EMAIL,
-                               "cloudflare_api_key": API_KEY}, 
self.config.cloudflare_credentials)
-        with pytest.raises(errors.PluginError):
-            self.auth.perform([self.achall])
-
-
-class CloudflareClientTest(unittest.TestCase):
-    record_name = "foo"
-    record_content = "bar"
-    record_ttl = 42
-    zone_id = 1
-    record_id = 2
-
-    def setUp(self):
-        from certbot_dns_cloudflare._internal.dns_cloudflare import 
_CloudflareClient
-
-        self.cloudflare_client = _CloudflareClient(EMAIL, API_KEY)
-
-        self.cf = mock.MagicMock()
-        self.cloudflare_client.cf = self.cf
-
-    def test_add_txt_record(self):
-        self.cf.zones.get.return_value = [{'id': self.zone_id}]
-
-        self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, 
self.record_content,
-                                              self.record_ttl)
-
-        self.cf.zones.dns_records.post.assert_called_with(self.zone_id, 
data=mock.ANY)
-
-        post_data = self.cf.zones.dns_records.post.call_args[1]['data']
-
-        assert 'TXT' == post_data['type']
-        assert self.record_name == post_data['name']
-        assert self.record_content == post_data['content']
-        assert self.record_ttl == post_data['ttl']
-
-    def test_add_txt_record_error(self):
-        self.cf.zones.get.return_value = [{'id': self.zone_id}]
-
-        self.cf.zones.dns_records.post.side_effect = 
CloudFlare.exceptions.CloudFlareAPIError(1009, '', '')
-
-        with pytest.raises(errors.PluginError):
-            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, 
self.record_content, self.record_ttl)
-
-    def test_add_txt_record_error_during_zone_lookup(self):
-        self.cf.zones.get.side_effect = API_ERROR
-
-        with pytest.raises(errors.PluginError):
-            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, 
self.record_content, self.record_ttl)
-
-    def test_add_txt_record_zone_not_found(self):
-        self.cf.zones.get.return_value = []
-
-        with pytest.raises(errors.PluginError):
-            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, 
self.record_content, self.record_ttl)
-
-    def test_add_txt_record_bad_creds(self):
-        self.cf.zones.get.side_effect = 
CloudFlare.exceptions.CloudFlareAPIError(6003, '', '')
-        with pytest.raises(errors.PluginError):
-            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, 
self.record_content, self.record_ttl)
-
-        self.cf.zones.get.side_effect = 
CloudFlare.exceptions.CloudFlareAPIError(9103, '', '')
-        with pytest.raises(errors.PluginError):
-            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, 
self.record_content, self.record_ttl)
-
-        self.cf.zones.get.side_effect = 
CloudFlare.exceptions.CloudFlareAPIError(9109, '', '')
-        with pytest.raises(errors.PluginError):
-            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, 
self.record_content, self.record_ttl)
-
-        self.cf.zones.get.side_effect = 
CloudFlare.exceptions.CloudFlareAPIError(0, 
'com.cloudflare.api.account.zone.list', '')
-        with pytest.raises(errors.PluginError):
-            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, 
self.record_content, self.record_ttl)
-
-    def test_del_txt_record(self):
-        self.cf.zones.get.return_value = [{'id': self.zone_id}]
-        self.cf.zones.dns_records.get.return_value = [{'id': self.record_id}]
-
-        self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, 
self.record_content)
-
-        expected = [mock.call.zones.get(params=mock.ANY),
-                    mock.call.zones.dns_records.get(self.zone_id, 
params=mock.ANY),
-                    mock.call.zones.dns_records.delete(self.zone_id, 
self.record_id)]
-
-        assert expected == self.cf.mock_calls
-
-        get_data = self.cf.zones.dns_records.get.call_args[1]['params']
-
-        assert 'TXT' == get_data['type']
-        assert self.record_name == get_data['name']
-        assert self.record_content == get_data['content']
-
-    def test_del_txt_record_error_during_zone_lookup(self):
-        self.cf.zones.get.side_effect = API_ERROR
-
-        self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, 
self.record_content)
-
-    def test_del_txt_record_error_during_delete(self):
-        self.cf.zones.get.return_value = [{'id': self.zone_id}]
-        self.cf.zones.dns_records.get.return_value = [{'id': self.record_id}]
-        self.cf.zones.dns_records.delete.side_effect = API_ERROR
-
-        self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, 
self.record_content)
-        expected = [mock.call.zones.get(params=mock.ANY),
-                    mock.call.zones.dns_records.get(self.zone_id, 
params=mock.ANY),
-                    mock.call.zones.dns_records.delete(self.zone_id, 
self.record_id)]
-
-        assert expected == self.cf.mock_calls
-
-    def test_del_txt_record_error_during_get(self):
-        self.cf.zones.get.return_value = [{'id': self.zone_id}]
-        self.cf.zones.dns_records.get.side_effect = API_ERROR
-
-        self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, 
self.record_content)
-        expected = [mock.call.zones.get(params=mock.ANY),
-                    mock.call.zones.dns_records.get(self.zone_id, 
params=mock.ANY)]
-
-        assert expected == self.cf.mock_calls
-
-    def test_del_txt_record_no_record(self):
-        self.cf.zones.get.return_value = [{'id': self.zone_id}]
-        self.cf.zones.dns_records.get.return_value = []
-
-        self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, 
self.record_content)
-        expected = [mock.call.zones.get(params=mock.ANY),
-                    mock.call.zones.dns_records.get(self.zone_id, 
params=mock.ANY)]
-
-        assert expected == self.cf.mock_calls
-
-    def test_del_txt_record_no_zone(self):
-        self.cf.zones.get.return_value = [{'id': None}]
-
-        self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, 
self.record_content)
-        expected = [mock.call.zones.get(params=mock.ANY)]
-
-        assert expected == self.cf.mock_calls
-
-
-if __name__ == "__main__":
-    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/PKG-INFO 
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/PKG-INFO
--- old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/PKG-INFO   
2025-04-08 00:03:36.000000000 +0200
+++ new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/PKG-INFO   
1970-01-01 01:00:00.000000000 +0100
@@ -1,46 +0,0 @@
-Metadata-Version: 2.4
-Name: certbot-dns-cloudflare
-Version: 4.0.0
-Summary: Cloudflare DNS Authenticator plugin for Certbot
-Home-page: https://github.com/certbot/certbot
-Author: Certbot Project
-Author-email: certbot-...@eff.org
-License: Apache License 2.0
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: Environment :: Plugins
-Classifier: Intended Audience :: System Administrators
-Classifier: License :: OSI Approved :: Apache Software License
-Classifier: Operating System :: POSIX :: Linux
-Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.9
-Classifier: Programming Language :: Python :: 3.10
-Classifier: Programming Language :: Python :: 3.11
-Classifier: Programming Language :: Python :: 3.12
-Classifier: Programming Language :: Python :: 3.13
-Classifier: Topic :: Internet :: WWW/HTTP
-Classifier: Topic :: Security
-Classifier: Topic :: System :: Installation/Setup
-Classifier: Topic :: System :: Networking
-Classifier: Topic :: System :: Systems Administration
-Classifier: Topic :: Utilities
-Requires-Python: >=3.9
-License-File: LICENSE.txt
-Requires-Dist: cloudflare<2.20,>=2.19
-Requires-Dist: acme>=4.0.0
-Requires-Dist: certbot>=4.0.0
-Provides-Extra: docs
-Requires-Dist: Sphinx>=1.0; extra == "docs"
-Requires-Dist: sphinx_rtd_theme; extra == "docs"
-Provides-Extra: test
-Requires-Dist: pytest; extra == "test"
-Dynamic: author
-Dynamic: author-email
-Dynamic: classifier
-Dynamic: home-page
-Dynamic: license
-Dynamic: license-file
-Dynamic: provides-extra
-Dynamic: requires-dist
-Dynamic: requires-python
-Dynamic: summary
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/SOURCES.txt 
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/SOURCES.txt
--- 
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/SOURCES.txt    
    2025-04-08 00:03:36.000000000 +0200
+++ 
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/SOURCES.txt    
    1970-01-01 01:00:00.000000000 +0100
@@ -1,22 +0,0 @@
-LICENSE.txt
-MANIFEST.in
-README.rst
-setup.py
-certbot_dns_cloudflare/__init__.py
-certbot_dns_cloudflare/py.typed
-certbot_dns_cloudflare.egg-info/PKG-INFO
-certbot_dns_cloudflare.egg-info/SOURCES.txt
-certbot_dns_cloudflare.egg-info/dependency_links.txt
-certbot_dns_cloudflare.egg-info/entry_points.txt
-certbot_dns_cloudflare.egg-info/requires.txt
-certbot_dns_cloudflare.egg-info/top_level.txt
-certbot_dns_cloudflare/_internal/__init__.py
-certbot_dns_cloudflare/_internal/dns_cloudflare.py
-certbot_dns_cloudflare/_internal/tests/__init__.py
-certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
-docs/.gitignore
-docs/Makefile
-docs/api.rst
-docs/conf.py
-docs/index.rst
-docs/make.bat
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/dependency_links.txt
 
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/dependency_links.txt
--- 
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/dependency_links.txt
       2025-04-08 00:03:36.000000000 +0200
+++ 
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/dependency_links.txt
       1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/entry_points.txt
 
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/entry_points.txt
--- 
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/entry_points.txt
   2025-04-08 00:03:36.000000000 +0200
+++ 
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/entry_points.txt
   1970-01-01 01:00:00.000000000 +0100
@@ -1,2 +0,0 @@
-[certbot.plugins]
-dns-cloudflare = certbot_dns_cloudflare._internal.dns_cloudflare:Authenticator
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/requires.txt 
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/requires.txt
--- 
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/requires.txt   
    2025-04-08 00:03:36.000000000 +0200
+++ 
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/requires.txt   
    1970-01-01 01:00:00.000000000 +0100
@@ -1,10 +0,0 @@
-cloudflare<2.20,>=2.19
-acme>=4.0.0
-certbot>=4.0.0
-
-[docs]
-Sphinx>=1.0
-sphinx_rtd_theme
-
-[test]
-pytest
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/top_level.txt 
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/top_level.txt
--- 
old/certbot_dns_cloudflare-4.0.0/certbot_dns_cloudflare.egg-info/top_level.txt  
    2025-04-08 00:03:36.000000000 +0200
+++ 
new/certbot_dns_cloudflare-4.1.1/certbot_dns_cloudflare.egg-info/top_level.txt  
    1970-01-01 01:00:00.000000000 +0100
@@ -1 +0,0 @@
-certbot_dns_cloudflare
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/certbot_dns_cloudflare-4.0.0/setup.py 
new/certbot_dns_cloudflare-4.1.1/setup.py
--- old/certbot_dns_cloudflare-4.0.0/setup.py   2025-04-08 00:03:33.000000000 
+0200
+++ new/certbot_dns_cloudflare-4.1.1/setup.py   2025-06-12 20:08:35.000000000 
+0200
@@ -4,7 +4,7 @@
 from setuptools import find_packages
 from setuptools import setup
 
-version = '4.0.0'
+version = '4.1.1'
 
 install_requires = [
     # for now, do not upgrade to cloudflare>=2.20 to avoid deprecation 
warnings and the breaking
@@ -40,7 +40,7 @@
     author="Certbot Project",
     author_email='certbot-...@eff.org',
     license='Apache License 2.0',
-    python_requires='>=3.9',
+    python_requires='>=3.9.2',
     classifiers=[
         'Development Status :: 5 - Production/Stable',
         'Environment :: Plugins',
@@ -62,7 +62,8 @@
         'Topic :: Utilities',
     ],
 
-    packages=find_packages(),
+    packages=find_packages(where='src'),
+    package_dir={'': 'src'},
     include_package_data=True,
     install_requires=install_requires,
     extras_require={
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare/__init__.py 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare/__init__.py
--- old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare/__init__.py     
1970-01-01 01:00:00.000000000 +0100
+++ new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare/__init__.py     
2025-06-12 20:08:34.000000000 +0200
@@ -0,0 +1,124 @@
+"""
+The `~certbot_dns_cloudflare.dns_cloudflare` plugin automates the process of
+completing a ``dns-01`` challenge (`~acme.challenges.DNS01`) by creating, and
+subsequently removing, TXT records using the Cloudflare API.
+
+.. note::
+   The plugin is not installed by default. It can be installed by heading to
+   `certbot.eff.org <https://certbot.eff.org/instructions#wildcard>`_, 
choosing your system and
+   selecting the Wildcard tab.
+
+Named Arguments
+---------------
+
+========================================  =====================================
+``--dns-cloudflare-credentials``          Cloudflare credentials_ INI file.
+                                          (Required)
+``--dns-cloudflare-propagation-seconds``  The number of seconds to wait for DNS
+                                          to propagate before asking the ACME
+                                          server to verify the DNS record.
+                                          (Default: 10)
+========================================  =====================================
+
+
+Credentials
+-----------
+
+Use of this plugin requires a configuration file containing Cloudflare API
+credentials, obtained from your
+`Cloudflare dashboard 
<https://dash.cloudflare.com/?to=/:account/profile/api-tokens>`_.
+
+Previously, Cloudflare's "Global API Key" was used for authentication, however
+this key can access the entire Cloudflare API for all domains in your account,
+meaning it could cause a lot of damage if leaked.
+
+Cloudflare's newer API Tokens can be restricted to specific domains and
+operations, and are therefore now the recommended authentication option.
+
+The Token needed by Certbot requires ``Zone:DNS:Edit`` permissions for only the
+zones you need certificates for.
+
+Using Cloudflare Tokens also requires at least version 2.3.1 of the 
``cloudflare``
+Python module. If the version that automatically installed with this plugin is
+older than that, and you can't upgrade it on your system, you'll have to stick 
to
+the Global key.
+
+.. code-block:: ini
+   :name: certbot_cloudflare_token.ini
+   :caption: Example credentials file using restricted API Token (recommended):
+
+   # Cloudflare API token used by Certbot
+   dns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567
+
+.. code-block:: ini
+   :name: certbot_cloudflare_key.ini
+   :caption: Example credentials file using Global API Key (not recommended):
+
+   # Cloudflare API credentials used by Certbot
+   dns_cloudflare_email = cloudfl...@example.com
+   dns_cloudflare_api_key = 0123456789abcdef0123456789abcdef01234
+
+The path to this file can be provided interactively or using the
+``--dns-cloudflare-credentials`` command-line argument. Certbot records the 
path
+to this file for use during renewal, but does not store the file's contents.
+
+.. caution::
+   You should protect these API credentials as you would the password to your
+   Cloudflare account. Users who can read this file can use these credentials
+   to issue arbitrary API calls on your behalf. Users who can cause Certbot to
+   run using these credentials can complete a ``dns-01`` challenge to acquire
+   new certificates or revoke existing certificates for associated domains,
+   even if those domains aren't being managed by this server.
+
+Certbot will emit a warning if it detects that the credentials file can be
+accessed by other users on your system. The warning reads "Unsafe permissions
+on credentials configuration file", followed by the path to the credentials
+file. This warning will be emitted each time Certbot uses the credentials file,
+including for renewal, and cannot be silenced except by addressing the issue
+(e.g., by using a command like ``chmod 600`` to restrict access to the file).
+
+.. note::
+    Please note that the ``cloudflare`` Python module used by the plugin has
+    additional methods of providing credentials to the module, e.g. environment
+    variables or the ``cloudflare.cfg`` configuration file. These methods are 
not
+    supported by Certbot. If any of those additional methods of providing
+    credentials is being used, they must provide the same credentials (i.e.,
+    email and API key *or* an API token) as the credentials file provided to
+    Certbot. If there is a discrepancy, the ``cloudflare`` Python module will
+    raise an error. Also note that the credentials provided to Certbot will 
take
+    precedence over any other method of providing credentials to the 
``cloudflare``
+    Python module.
+
+
+Examples
+--------
+
+.. code-block:: bash
+   :caption: To acquire a certificate for ``example.com``
+
+   certbot certonly \\
+     --dns-cloudflare \\
+     --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \\
+     -d example.com
+
+.. code-block:: bash
+   :caption: To acquire a single certificate for both ``example.com`` and
+             ``www.example.com``
+
+   certbot certonly \\
+     --dns-cloudflare \\
+     --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \\
+     -d example.com \\
+     -d www.example.com
+
+.. code-block:: bash
+   :caption: To acquire a certificate for ``example.com``, waiting 60 seconds
+             for DNS propagation
+
+   certbot certonly \\
+     --dns-cloudflare \\
+     --dns-cloudflare-credentials ~/.secrets/certbot/cloudflare.ini \\
+     --dns-cloudflare-propagation-seconds 60 \\
+     -d example.com
+
+"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare/_internal/__init__.py
 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare/_internal/__init__.py
--- 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare/_internal/__init__.py
   1970-01-01 01:00:00.000000000 +0100
+++ 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare/_internal/__init__.py
   2025-06-12 20:08:34.000000000 +0200
@@ -0,0 +1 @@
+"""Internal implementation of `~certbot_dns_cloudflare.dns_cloudflare` 
plugin."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py
 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py
--- 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py
     1970-01-01 01:00:00.000000000 +0100
+++ 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py
     2025-06-12 20:08:34.000000000 +0200
@@ -0,0 +1,273 @@
+"""DNS Authenticator for Cloudflare."""
+import logging
+from typing import Any
+from typing import Callable
+from typing import Dict
+from typing import List
+from typing import Optional
+from typing import cast
+
+import CloudFlare
+
+from certbot import errors
+from certbot.plugins import dns_common
+from certbot.plugins.dns_common import CredentialsConfiguration
+
+logger = logging.getLogger(__name__)
+
+ACCOUNT_URL = 'https://dash.cloudflare.com/?to=/:account/profile/api-tokens'
+
+
+class Authenticator(dns_common.DNSAuthenticator):
+    """DNS Authenticator for Cloudflare
+
+    This Authenticator uses the Cloudflare API to fulfill a dns-01 challenge.
+    """
+
+    description = ('Obtain certificates using a DNS TXT record (if you are 
using Cloudflare for '
+                   'DNS).')
+    ttl = 120
+
+    def __init__(self, *args: Any, **kwargs: Any) -> None:
+        super().__init__(*args, **kwargs)
+        self.credentials: Optional[CredentialsConfiguration] = None
+
+    @classmethod
+    def add_parser_arguments(cls, add: Callable[..., None],
+                             default_propagation_seconds: int = 10) -> None:
+        super().add_parser_arguments(add, default_propagation_seconds)
+        add('credentials', help='Cloudflare credentials INI file.')
+
+    def more_info(self) -> str:
+        return 'This plugin configures a DNS TXT record to respond to a dns-01 
challenge using ' + \
+               'the Cloudflare API.'
+
+    def _validate_credentials(self, credentials: CredentialsConfiguration) -> 
None:
+        token = credentials.conf('api-token')
+        email = credentials.conf('email')
+        key = credentials.conf('api-key')
+        if token:
+            if email or key:
+                raise errors.PluginError('{}: dns_cloudflare_email and 
dns_cloudflare_api_key are '
+                                         'not needed when using an API Token'
+                                         .format(credentials.confobj.filename))
+        elif email or key:
+            if not email:
+                raise errors.PluginError('{}: dns_cloudflare_email is required 
when using a Global '
+                                         'API Key. (should be email address 
associated with '
+                                         'Cloudflare 
account)'.format(credentials.confobj.filename))
+            if not key:
+                raise errors.PluginError('{}: dns_cloudflare_api_key is 
required when using a '
+                                         'Global API Key. (see {})'
+                                         .format(credentials.confobj.filename, 
ACCOUNT_URL))
+        else:
+            raise errors.PluginError('{}: Either dns_cloudflare_api_token 
(recommended), or '
+                                     'dns_cloudflare_email and 
dns_cloudflare_api_key are required.'
+                                     ' (see 
{})'.format(credentials.confobj.filename, ACCOUNT_URL))
+
+    def _setup_credentials(self) -> None:
+        self.credentials = self._configure_credentials(
+            'credentials',
+            'Cloudflare credentials INI file',
+            None,
+            self._validate_credentials
+        )
+
+    def _perform(self, domain: str, validation_name: str, validation: str) -> 
None:
+        self._get_cloudflare_client().add_txt_record(domain, validation_name, 
validation, self.ttl)
+
+    def _cleanup(self, domain: str, validation_name: str, validation: str) -> 
None:
+        self._get_cloudflare_client().del_txt_record(domain, validation_name, 
validation)
+
+    def _get_cloudflare_client(self) -> "_CloudflareClient":
+        if not self.credentials:  # pragma: no cover
+            raise errors.Error("Plugin has not been prepared.")
+        if self.credentials.conf('api-token'):
+            return _CloudflareClient(api_token = 
self.credentials.conf('api-token'))
+        return _CloudflareClient(email = self.credentials.conf('email'),
+                                 api_key = self.credentials.conf('api-key'))
+
+
+class _CloudflareClient:
+    """
+    Encapsulates all communication with the Cloudflare API.
+    """
+
+    def __init__(self, email: Optional[str] = None, api_key: Optional[str] = 
None,
+                 api_token: Optional[str] = None) -> None:
+        if email:
+            # If an email was specified, we're using an email/key combination 
and not a token.
+            # We can't use named arguments in this case, as it would break 
compatibility with
+            # the Cloudflare library since version 2.10.1, as the `token` 
argument was used for
+            # tokens and keys alike and the `key` argument did not exist in 
earlier versions.
+            self.cf = CloudFlare.CloudFlare(email, api_key)
+        else:
+            # If no email was specified, we're using just a token. Let's use 
the named argument
+            # for simplicity, which is compatible with all (current) versions 
of the Cloudflare
+            # library.
+            self.cf = CloudFlare.CloudFlare(token=api_token)
+
+    def add_txt_record(self, domain: str, record_name: str, record_content: 
str,
+                       record_ttl: int) -> None:
+        """
+        Add a TXT record using the supplied information.
+
+        :param str domain: The domain to use to look up the Cloudflare zone.
+        :param str record_name: The record name (typically beginning with 
'_acme-challenge.').
+        :param str record_content: The record content (typically the challenge 
validation).
+        :param int record_ttl: The record TTL (number of seconds that the 
record may be cached).
+        :raises certbot.errors.PluginError: if an error occurs communicating 
with the Cloudflare API
+        """
+
+        zone_id = self._find_zone_id(domain)
+
+        data = {'type': 'TXT',
+                'name': record_name,
+                'content': record_content,
+                'ttl': record_ttl}
+
+        try:
+            logger.debug('Attempting to add record to zone %s: %s', zone_id, 
data)
+            self.cf.zones.dns_records.post(zone_id, data=data)  # zones | 
pylint: disable=no-member
+        except CloudFlare.exceptions.CloudFlareAPIError as e:
+            code = int(e)
+            hint = None
+
+            if code == 1009:
+                hint = 'Does your API token have "Zone:DNS:Edit" permissions?'
+
+            logger.error('Encountered CloudFlareAPIError adding TXT record: %d 
%s', e, e)
+            raise errors.PluginError('Error communicating with the Cloudflare 
API: {0}{1}'
+                                     .format(e, ' ({0})'.format(hint) if hint 
else ''))
+
+        record_id = self._find_txt_record_id(zone_id, record_name, 
record_content)
+        logger.debug('Successfully added TXT record with record_id: %s', 
record_id)
+
+    def del_txt_record(self, domain: str, record_name: str, record_content: 
str) -> None:
+        """
+        Delete a TXT record using the supplied information.
+
+        Note that both the record's name and content are used to ensure that 
similar records
+        created concurrently (e.g., due to concurrent invocations of this 
plugin) are not deleted.
+
+        Failures are logged, but not raised.
+
+        :param str domain: The domain to use to look up the Cloudflare zone.
+        :param str record_name: The record name (typically beginning with 
'_acme-challenge.').
+        :param str record_content: The record content (typically the challenge 
validation).
+        """
+
+        try:
+            zone_id = self._find_zone_id(domain)
+        except errors.PluginError as e:
+            logger.debug('Encountered error finding zone_id during deletion: 
%s', e)
+            return
+
+        if zone_id:
+            record_id = self._find_txt_record_id(zone_id, record_name, 
record_content)
+            if record_id:
+                try:
+                    # zones | pylint: disable=no-member
+                    self.cf.zones.dns_records.delete(zone_id, record_id)
+                    logger.debug('Successfully deleted TXT record.')
+                except CloudFlare.exceptions.CloudFlareAPIError as e:
+                    logger.warning('Encountered CloudFlareAPIError deleting 
TXT record: %s', e)
+            else:
+                logger.debug('TXT record not found; no cleanup needed.')
+        else:
+            logger.debug('Zone not found; no cleanup needed.')
+
+    def _find_zone_id(self, domain: str) -> str:
+        """
+        Find the zone_id for a given domain.
+
+        :param str domain: The domain for which to find the zone_id.
+        :returns: The zone_id, if found.
+        :rtype: str
+        :raises certbot.errors.PluginError: if no zone_id is found.
+        """
+
+        zone_name_guesses = dns_common.base_domain_name_guesses(domain)
+        zones: List[Dict[str, Any]] = []
+        code = msg = None
+
+        for zone_name in zone_name_guesses:
+            params = {'name': zone_name,
+                      'per_page': 1}
+
+            try:
+                zones = self.cf.zones.get(params=params)  # zones | pylint: 
disable=no-member
+            except CloudFlare.exceptions.CloudFlareAPIError as e:
+                code = int(e)
+                msg = str(e)
+                hint = None
+
+                if code == 6003:
+                    hint = ('Did you copy your entire API token/key? To use 
Cloudflare tokens, '
+                            'you\'ll need the python package 
cloudflare>=2.3.1.{}'
+                    .format(' This certbot is running cloudflare ' + 
str(CloudFlare.__version__)
+                    if hasattr(CloudFlare, '__version__') else ''))
+                elif code == 9103:
+                    hint = 'Did you enter the correct email address and Global 
key?'
+                elif code == 9109:
+                    hint = 'Did you enter a valid Cloudflare Token?'
+
+                if hint:
+                    raise errors.PluginError('Error determining zone_id: {0} 
{1}. Please confirm '
+                                  'that you have supplied valid Cloudflare API 
credentials. ({2})'
+                                                                         
.format(code, msg, hint))
+                else:
+                    logger.debug('Unrecognised CloudFlareAPIError while 
finding zone_id: %d %s. '
+                                 'Continuing with next zone guess...', e, e)
+
+            if zones:
+                zone_id: str = zones[0]['id']
+                logger.debug('Found zone_id of %s for %s using name %s', 
zone_id, domain, zone_name)
+                return zone_id
+
+        if msg is not None:
+            if 'com.cloudflare.api.account.zone.list' in msg:
+                raise errors.PluginError('Unable to determine zone_id for {0} 
using zone names: '
+                                         '{1}. Please confirm that the domain 
name has been '
+                                         'entered correctly and your 
Cloudflare Token has access '
+                                         'to the domain.'.format(domain, 
zone_name_guesses))
+            else:
+                raise errors.PluginError('Unable to determine zone_id for {0} 
using zone names: '
+                                         '{1}. The error from Cloudflare was: 
{2} {3}.'
+                                         .format(domain, zone_name_guesses, 
code, msg))
+        else:
+            raise errors.PluginError('Unable to determine zone_id for {0} 
using zone names: '
+                                     '{1}. Please confirm that the domain name 
has been '
+                                     'entered correctly and is already 
associated with the '
+                                     'supplied Cloudflare account.'
+                                     .format(domain, zone_name_guesses))
+
+    def _find_txt_record_id(self, zone_id: str, record_name: str,
+                            record_content: str) -> Optional[str]:
+        """
+        Find the record_id for a TXT record with the given name and content.
+
+        :param str zone_id: The zone_id which contains the record.
+        :param str record_name: The record name (typically beginning with 
'_acme-challenge.').
+        :param str record_content: The record content (typically the challenge 
validation).
+        :returns: The record_id, if found.
+        :rtype: str
+        """
+
+        params = {'type': 'TXT',
+                  'name': record_name,
+                  'content': record_content,
+                  'per_page': 1}
+        try:
+            # zones | pylint: disable=no-member
+            records = self.cf.zones.dns_records.get(zone_id, params=params)
+        except CloudFlare.exceptions.CloudFlareAPIError as e:
+            logger.debug('Encountered CloudFlareAPIError getting TXT 
record_id: %s', e)
+            records = []
+
+        if records:
+            # Cleanup is returning the system to the state we found it. If, 
for some reason,
+            # there are multiple matching records, we only delete one because 
we only added one.
+            return cast(str, records[0]['id'])
+        logger.debug('Unable to find TXT record.')
+        return None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare/_internal/tests/__init__.py
 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare/_internal/tests/__init__.py
--- 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare/_internal/tests/__init__.py
     1970-01-01 01:00:00.000000000 +0100
+++ 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare/_internal/tests/__init__.py
     2025-06-12 20:08:34.000000000 +0200
@@ -0,0 +1 @@
+"""certbot-dns-cloudflare tests"""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
--- 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
  1970-01-01 01:00:00.000000000 +0100
+++ 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
  2025-06-12 20:08:34.000000000 +0200
@@ -0,0 +1,232 @@
+"""Tests for certbot_dns_cloudflare._internal.dns_cloudflare."""
+
+import sys
+import unittest
+from unittest import mock
+
+import CloudFlare
+import pytest
+
+from certbot import errors
+from certbot.compat import os
+from certbot.plugins import dns_test_common
+from certbot.plugins.dns_test_common import DOMAIN
+from certbot.tests import util as test_util
+
+API_ERROR = CloudFlare.exceptions.CloudFlareAPIError(1000, '', '')
+
+API_TOKEN = 'an-api-token'
+
+API_KEY = 'an-api-key'
+EMAIL = 'exam...@example.com'
+
+
+class AuthenticatorTest(test_util.TempDirTestCase, 
dns_test_common.BaseAuthenticatorTest):
+
+    def setUp(self):
+        from certbot_dns_cloudflare._internal.dns_cloudflare import 
Authenticator
+
+        super().setUp()
+
+        path = os.path.join(self.tempdir, 'file.ini')
+        dns_test_common.write({"cloudflare_email": EMAIL, 
"cloudflare_api_key": API_KEY}, path)
+
+        self.config = mock.MagicMock(cloudflare_credentials=path,
+                                     cloudflare_propagation_seconds=0)  # 
don't wait during tests
+
+        self.auth = Authenticator(self.config, "cloudflare")
+
+        self.mock_client = mock.MagicMock()
+        # _get_cloudflare_client | pylint: disable=protected-access
+        # workaround for wont-fix https://github.com/python/mypy/issues/2427 
that works with
+        # both strict and non-strict mypy
+        setattr(self.auth, '_get_cloudflare_client', 
mock.MagicMock(return_value=self.mock_client))
+
+    @test_util.patch_display_util()
+    def test_perform(self, unused_mock_get_utility):
+        self.auth.perform([self.achall])
+
+        expected = [mock.call.add_txt_record(DOMAIN, 
'_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]
+        assert expected == self.mock_client.mock_calls
+
+    def test_cleanup(self):
+        # _attempt_cleanup | pylint: disable=protected-access
+        self.auth._attempt_cleanup = True
+        self.auth.cleanup([self.achall])
+
+        expected = [mock.call.del_txt_record(DOMAIN, 
'_acme-challenge.'+DOMAIN, mock.ANY)]
+        assert expected == self.mock_client.mock_calls
+
+    @test_util.patch_display_util()
+    def test_api_token(self, unused_mock_get_utility):
+        dns_test_common.write({"cloudflare_api_token": API_TOKEN},
+                              self.config.cloudflare_credentials)
+        self.auth.perform([self.achall])
+
+        expected = [mock.call.add_txt_record(DOMAIN, 
'_acme-challenge.'+DOMAIN, mock.ANY, mock.ANY)]
+        assert expected == self.mock_client.mock_calls
+
+    def test_no_creds(self):
+        dns_test_common.write({}, self.config.cloudflare_credentials)
+        with pytest.raises(errors.PluginError):
+            self.auth.perform([self.achall])
+
+    def test_missing_email_or_key(self):
+        dns_test_common.write({"cloudflare_api_key": API_KEY}, 
self.config.cloudflare_credentials)
+        with pytest.raises(errors.PluginError):
+            self.auth.perform([self.achall])
+
+        dns_test_common.write({"cloudflare_email": EMAIL}, 
self.config.cloudflare_credentials)
+        with pytest.raises(errors.PluginError):
+            self.auth.perform([self.achall])
+
+    def test_email_or_key_with_token(self):
+        dns_test_common.write({"cloudflare_api_token": API_TOKEN, 
"cloudflare_email": EMAIL},
+                              self.config.cloudflare_credentials)
+        with pytest.raises(errors.PluginError):
+            self.auth.perform([self.achall])
+
+        dns_test_common.write({"cloudflare_api_token": API_TOKEN, 
"cloudflare_api_key": API_KEY},
+                              self.config.cloudflare_credentials)
+        with pytest.raises(errors.PluginError):
+            self.auth.perform([self.achall])
+
+        dns_test_common.write({"cloudflare_api_token": API_TOKEN, 
"cloudflare_email": EMAIL,
+                               "cloudflare_api_key": API_KEY}, 
self.config.cloudflare_credentials)
+        with pytest.raises(errors.PluginError):
+            self.auth.perform([self.achall])
+
+
+class CloudflareClientTest(unittest.TestCase):
+    record_name = "foo"
+    record_content = "bar"
+    record_ttl = 42
+    zone_id = 1
+    record_id = 2
+
+    def setUp(self):
+        from certbot_dns_cloudflare._internal.dns_cloudflare import 
_CloudflareClient
+
+        self.cloudflare_client = _CloudflareClient(EMAIL, API_KEY)
+
+        self.cf = mock.MagicMock()
+        self.cloudflare_client.cf = self.cf
+
+    def test_add_txt_record(self):
+        self.cf.zones.get.return_value = [{'id': self.zone_id}]
+
+        self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, 
self.record_content,
+                                              self.record_ttl)
+
+        self.cf.zones.dns_records.post.assert_called_with(self.zone_id, 
data=mock.ANY)
+
+        post_data = self.cf.zones.dns_records.post.call_args[1]['data']
+
+        assert 'TXT' == post_data['type']
+        assert self.record_name == post_data['name']
+        assert self.record_content == post_data['content']
+        assert self.record_ttl == post_data['ttl']
+
+    def test_add_txt_record_error(self):
+        self.cf.zones.get.return_value = [{'id': self.zone_id}]
+
+        self.cf.zones.dns_records.post.side_effect = 
CloudFlare.exceptions.CloudFlareAPIError(1009, '', '')
+
+        with pytest.raises(errors.PluginError):
+            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, 
self.record_content, self.record_ttl)
+
+    def test_add_txt_record_error_during_zone_lookup(self):
+        self.cf.zones.get.side_effect = API_ERROR
+
+        with pytest.raises(errors.PluginError):
+            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, 
self.record_content, self.record_ttl)
+
+    def test_add_txt_record_zone_not_found(self):
+        self.cf.zones.get.return_value = []
+
+        with pytest.raises(errors.PluginError):
+            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, 
self.record_content, self.record_ttl)
+
+    def test_add_txt_record_bad_creds(self):
+        self.cf.zones.get.side_effect = 
CloudFlare.exceptions.CloudFlareAPIError(6003, '', '')
+        with pytest.raises(errors.PluginError):
+            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, 
self.record_content, self.record_ttl)
+
+        self.cf.zones.get.side_effect = 
CloudFlare.exceptions.CloudFlareAPIError(9103, '', '')
+        with pytest.raises(errors.PluginError):
+            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, 
self.record_content, self.record_ttl)
+
+        self.cf.zones.get.side_effect = 
CloudFlare.exceptions.CloudFlareAPIError(9109, '', '')
+        with pytest.raises(errors.PluginError):
+            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, 
self.record_content, self.record_ttl)
+
+        self.cf.zones.get.side_effect = 
CloudFlare.exceptions.CloudFlareAPIError(0, 
'com.cloudflare.api.account.zone.list', '')
+        with pytest.raises(errors.PluginError):
+            self.cloudflare_client.add_txt_record(DOMAIN, self.record_name, 
self.record_content, self.record_ttl)
+
+    def test_del_txt_record(self):
+        self.cf.zones.get.return_value = [{'id': self.zone_id}]
+        self.cf.zones.dns_records.get.return_value = [{'id': self.record_id}]
+
+        self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, 
self.record_content)
+
+        expected = [mock.call.zones.get(params=mock.ANY),
+                    mock.call.zones.dns_records.get(self.zone_id, 
params=mock.ANY),
+                    mock.call.zones.dns_records.delete(self.zone_id, 
self.record_id)]
+
+        assert expected == self.cf.mock_calls
+
+        get_data = self.cf.zones.dns_records.get.call_args[1]['params']
+
+        assert 'TXT' == get_data['type']
+        assert self.record_name == get_data['name']
+        assert self.record_content == get_data['content']
+
+    def test_del_txt_record_error_during_zone_lookup(self):
+        self.cf.zones.get.side_effect = API_ERROR
+
+        self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, 
self.record_content)
+
+    def test_del_txt_record_error_during_delete(self):
+        self.cf.zones.get.return_value = [{'id': self.zone_id}]
+        self.cf.zones.dns_records.get.return_value = [{'id': self.record_id}]
+        self.cf.zones.dns_records.delete.side_effect = API_ERROR
+
+        self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, 
self.record_content)
+        expected = [mock.call.zones.get(params=mock.ANY),
+                    mock.call.zones.dns_records.get(self.zone_id, 
params=mock.ANY),
+                    mock.call.zones.dns_records.delete(self.zone_id, 
self.record_id)]
+
+        assert expected == self.cf.mock_calls
+
+    def test_del_txt_record_error_during_get(self):
+        self.cf.zones.get.return_value = [{'id': self.zone_id}]
+        self.cf.zones.dns_records.get.side_effect = API_ERROR
+
+        self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, 
self.record_content)
+        expected = [mock.call.zones.get(params=mock.ANY),
+                    mock.call.zones.dns_records.get(self.zone_id, 
params=mock.ANY)]
+
+        assert expected == self.cf.mock_calls
+
+    def test_del_txt_record_no_record(self):
+        self.cf.zones.get.return_value = [{'id': self.zone_id}]
+        self.cf.zones.dns_records.get.return_value = []
+
+        self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, 
self.record_content)
+        expected = [mock.call.zones.get(params=mock.ANY),
+                    mock.call.zones.dns_records.get(self.zone_id, 
params=mock.ANY)]
+
+        assert expected == self.cf.mock_calls
+
+    def test_del_txt_record_no_zone(self):
+        self.cf.zones.get.return_value = [{'id': None}]
+
+        self.cloudflare_client.del_txt_record(DOMAIN, self.record_name, 
self.record_content)
+        expected = [mock.call.zones.get(params=mock.ANY)]
+
+        assert expected == self.cf.mock_calls
+
+
+if __name__ == "__main__":
+    sys.exit(pytest.main(sys.argv[1:] + [__file__]))  # pragma: no cover
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/PKG-INFO 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/PKG-INFO
--- 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/PKG-INFO   
    1970-01-01 01:00:00.000000000 +0100
+++ 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/PKG-INFO   
    2025-06-12 20:08:38.000000000 +0200
@@ -0,0 +1,46 @@
+Metadata-Version: 2.4
+Name: certbot-dns-cloudflare
+Version: 4.1.1
+Summary: Cloudflare DNS Authenticator plugin for Certbot
+Home-page: https://github.com/certbot/certbot
+Author: Certbot Project
+Author-email: certbot-...@eff.org
+License: Apache License 2.0
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Plugins
+Classifier: Intended Audience :: System Administrators
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Operating System :: POSIX :: Linux
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.9
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Topic :: Internet :: WWW/HTTP
+Classifier: Topic :: Security
+Classifier: Topic :: System :: Installation/Setup
+Classifier: Topic :: System :: Networking
+Classifier: Topic :: System :: Systems Administration
+Classifier: Topic :: Utilities
+Requires-Python: >=3.9.2
+License-File: LICENSE.txt
+Requires-Dist: cloudflare<2.20,>=2.19
+Requires-Dist: acme>=4.1.1
+Requires-Dist: certbot>=4.1.1
+Provides-Extra: docs
+Requires-Dist: Sphinx>=1.0; extra == "docs"
+Requires-Dist: sphinx_rtd_theme; extra == "docs"
+Provides-Extra: test
+Requires-Dist: pytest; extra == "test"
+Dynamic: author
+Dynamic: author-email
+Dynamic: classifier
+Dynamic: home-page
+Dynamic: license
+Dynamic: license-file
+Dynamic: provides-extra
+Dynamic: requires-dist
+Dynamic: requires-python
+Dynamic: summary
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/SOURCES.txt
 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/SOURCES.txt
--- 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/SOURCES.txt
    1970-01-01 01:00:00.000000000 +0100
+++ 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/SOURCES.txt
    2025-06-12 20:08:38.000000000 +0200
@@ -0,0 +1,22 @@
+LICENSE.txt
+MANIFEST.in
+README.rst
+setup.py
+docs/.gitignore
+docs/Makefile
+docs/api.rst
+docs/conf.py
+docs/index.rst
+docs/make.bat
+src/certbot_dns_cloudflare/__init__.py
+src/certbot_dns_cloudflare/py.typed
+src/certbot_dns_cloudflare.egg-info/PKG-INFO
+src/certbot_dns_cloudflare.egg-info/SOURCES.txt
+src/certbot_dns_cloudflare.egg-info/dependency_links.txt
+src/certbot_dns_cloudflare.egg-info/entry_points.txt
+src/certbot_dns_cloudflare.egg-info/requires.txt
+src/certbot_dns_cloudflare.egg-info/top_level.txt
+src/certbot_dns_cloudflare/_internal/__init__.py
+src/certbot_dns_cloudflare/_internal/dns_cloudflare.py
+src/certbot_dns_cloudflare/_internal/tests/__init__.py
+src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/dependency_links.txt
 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/dependency_links.txt
--- 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/dependency_links.txt
   1970-01-01 01:00:00.000000000 +0100
+++ 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/dependency_links.txt
   2025-06-12 20:08:38.000000000 +0200
@@ -0,0 +1 @@
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/entry_points.txt
 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/entry_points.txt
--- 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/entry_points.txt
       1970-01-01 01:00:00.000000000 +0100
+++ 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/entry_points.txt
       2025-06-12 20:08:38.000000000 +0200
@@ -0,0 +1,2 @@
+[certbot.plugins]
+dns-cloudflare = certbot_dns_cloudflare._internal.dns_cloudflare:Authenticator
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/requires.txt
 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/requires.txt
--- 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/requires.txt
   1970-01-01 01:00:00.000000000 +0100
+++ 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/requires.txt
   2025-06-12 20:08:38.000000000 +0200
@@ -0,0 +1,10 @@
+cloudflare<2.20,>=2.19
+acme>=4.1.1
+certbot>=4.1.1
+
+[docs]
+Sphinx>=1.0
+sphinx_rtd_theme
+
+[test]
+pytest
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/top_level.txt
 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/top_level.txt
--- 
old/certbot_dns_cloudflare-4.0.0/src/certbot_dns_cloudflare.egg-info/top_level.txt
  1970-01-01 01:00:00.000000000 +0100
+++ 
new/certbot_dns_cloudflare-4.1.1/src/certbot_dns_cloudflare.egg-info/top_level.txt
  2025-06-12 20:08:38.000000000 +0200
@@ -0,0 +1 @@
+certbot_dns_cloudflare

Reply via email to