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 2026-05-15 23:52:58
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-certbot-dns-cloudflare (Old)
and /work/SRC/openSUSE:Factory/.python-certbot-dns-cloudflare.new.1966
(New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-certbot-dns-cloudflare"
Fri May 15 23:52:58 2026 rev:53 rq:1353145 version:5.6.0
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-certbot-dns-cloudflare/python-certbot-dns-cloudflare.changes
2026-03-16 15:49:19.654291265 +0100
+++
/work/SRC/openSUSE:Factory/.python-certbot-dns-cloudflare.new.1966/python-certbot-dns-cloudflare.changes
2026-05-15 23:53:04.544036029 +0200
@@ -1,0 +2,24 @@
+Thu May 14 09:15:03 UTC 2026 - Markéta Machová <[email protected]>
+
+- update to version 5.6.0
+ * certbot-dns-cloudflare requires 4.0+ of the Cloudflare Python
+ library
+
+-------------------------------------------------------------------
+Wed May 6 20:44:59 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 5.5.0:
+ * Moved nearly all code for the certbot-nginx and certbot-
+ apache plugins into private modules in the certbot package
+ which now offers "apache" and "nginx" extras. Users should
+ notice no major changes. certbot-apache and certbot-nginx
+ will continue to exist as simple packages that depend on
+ certbot and also help register the plugin with certbot so it
+ knows the functionality is available. Unit tests for these
+ plugins are now also part of the certbot package.
+ * The certbot.ocsp module has been deprecated and will be
+ removed in the next major release. This is not a change to
+ Certbot's OCSP functionality. The code is just being removed
+ from Certbot's public API.
+
+-------------------------------------------------------------------
Old:
----
certbot_dns_cloudflare-5.4.0.tar.gz
New:
----
certbot_dns_cloudflare-5.6.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-certbot-dns-cloudflare.spec ++++++
--- /var/tmp/diff_new_pack.m5d38k/_old 2026-05-15 23:53:05.292066821 +0200
+++ /var/tmp/diff_new_pack.m5d38k/_new 2026-05-15 23:53:05.296066986 +0200
@@ -18,7 +18,7 @@
%{?sle15_python_module_pythons}
Name: python-certbot-dns-cloudflare
-Version: 5.4.0
+Version: 5.6.0
Release: 0
Summary: Cloudflare Authenticator plugin for Certbot
License: Apache-2.0
@@ -26,7 +26,7 @@
Source:
https://files.pythonhosted.org/packages/source/c/certbot-dns-cloudflare/certbot_dns_cloudflare-%{version}.tar.gz
BuildRequires: %{python_module acme >= %{version}}
BuildRequires: %{python_module certbot >= %{version}}
-BuildRequires: %{python_module cloudflare >= 2.19}
+BuildRequires: %{python_module cloudflare >= 4.0}
BuildRequires: %{python_module pip}
BuildRequires: %{python_module pytest}
BuildRequires: %{python_module setuptools}
@@ -34,7 +34,7 @@
BuildRequires: python-rpm-macros
Requires: python-acme >= %{version}
Requires: python-certbot >= %{version}
-Requires: python-cloudflare >= 2.19
+Requires: python-cloudflare >= 4.0
BuildArch: noarch
%python_subpackages
++++++ certbot_dns_cloudflare-5.4.0.tar.gz ->
certbot_dns_cloudflare-5.6.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/certbot_dns_cloudflare-5.4.0/PKG-INFO
new/certbot_dns_cloudflare-5.6.0/PKG-INFO
--- old/certbot_dns_cloudflare-5.4.0/PKG-INFO 2026-03-10 18:47:02.259142600
+0100
+++ new/certbot_dns_cloudflare-5.6.0/PKG-INFO 2026-05-11 17:56:24.594847700
+0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: certbot-dns-cloudflare
-Version: 5.4.0
+Version: 5.6.0
Summary: Cloudflare DNS Authenticator plugin for Certbot
Author: Certbot Project
License-Expression: Apache-2.0
@@ -25,9 +25,9 @@
Requires-Python: >=3.10
Description-Content-Type: text/x-rst
License-File: LICENSE.txt
-Requires-Dist: cloudflare<2.20,>=2.19
-Requires-Dist: acme>=5.4.0
-Requires-Dist: certbot>=5.4.0
+Requires-Dist: cloudflare>=4.0
+Requires-Dist: acme>=5.6.0
+Requires-Dist: certbot>=5.6.0
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-5.4.0/setup.py
new/certbot_dns_cloudflare-5.6.0/setup.py
--- old/certbot_dns_cloudflare-5.4.0/setup.py 2026-03-10 18:46:56.000000000
+0100
+++ new/certbot_dns_cloudflare-5.6.0/setup.py 2026-05-11 17:56:17.000000000
+0200
@@ -2,12 +2,10 @@
from setuptools import setup
-version = '5.4.0'
+version = '5.6.0'
install_requires = [
- # for now, do not upgrade to cloudflare>=2.20 to avoid deprecation
warnings and the breaking
- # changes in version 3.0. see
https://github.com/certbot/certbot/issues/9938
- 'cloudflare>=2.19, <2.20',
+ 'cloudflare>=4.0',
]
if os.environ.get('SNAP_BUILD'):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-5.4.0/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py
new/certbot_dns_cloudflare-5.6.0/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py
---
old/certbot_dns_cloudflare-5.4.0/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py
2026-03-10 18:46:56.000000000 +0100
+++
new/certbot_dns_cloudflare-5.6.0/src/certbot_dns_cloudflare/_internal/dns_cloudflare.py
2026-05-11 17:56:16.000000000 +0200
@@ -1,11 +1,21 @@
"""DNS Authenticator for Cloudflare."""
import logging
+import warnings
from typing import Any
from typing import Callable
+from typing import Literal
from typing import Optional
-from typing import cast
+from typing import TypedDict
-import CloudFlare
+# cloudflare 4.x includes a pydantic v1 compatibility shim that emits a
+# UserWarning on Python 3.14+. Suppress it here so that this internal-detail
+# warning is not shown to users during plugin discovery. In our test suite it
+# is filtered out via pytest.ini so it does not affect filterwarnings=error.
+with warnings.catch_warnings():
+ warnings.filterwarnings('ignore', message='Core Pydantic V1 functionality',
+ category=UserWarning)
+ import cloudflare
+ from cloudflare.types.zones import Zone
from certbot import errors
from certbot.plugins import dns_common
@@ -95,15 +105,15 @@
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)
+ # We use named arguments here to match the cloudflare 4.x SDK's
explicit parameter
+ # names (api_email and api_key), which correspond to the Global
API Key credentials
+ # found in the Cloudflare dashboard under My Profile > API Tokens.
+ self.cf = cloudflare.Cloudflare(api_email=email, api_key=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)
+ # If no email was specified, we're using just an API token. We use
the named argument
+ # for clarity. API Tokens are the recommended authentication
method as they support
+ # fine-grained permissions scoped to specific zones and operations.
+ self.cf = cloudflare.Cloudflare(api_token=api_token)
def add_txt_record(self, domain: str, record_name: str, record_content:
str,
record_ttl: int) -> None:
@@ -119,24 +129,30 @@
zone_id = self._find_zone_id(domain)
- data = {'type': 'TXT',
- 'name': record_name,
- 'content': record_content,
- 'ttl': record_ttl}
+ data: _RecordData = {
+ '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)
+ self.cf.dns.records.create(zone_id=zone_id, **data)
+ except cloudflare.APIStatusError as e:
+ code = _cf_error_code(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)
+ logger.error('Encountered Cloudflare API error adding TXT record:
%s', e)
raise errors.PluginError('Error communicating with the Cloudflare
API: {0}{1}'
.format(e, ' ({0})'.format(hint) if hint
else ''))
+ except cloudflare.APIConnectionError as e:
+ logger.error('Network error talking to the Cloudflare API: %s', e)
+ raise errors.PluginError('Network error communicating with the
Cloudflare API '
+ 'while adding a TXT record:
{0}'.format(e))
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)
@@ -159,52 +175,48 @@
zone_id = self._find_zone_id(domain)
except errors.PluginError as e:
logger.debug('Encountered error finding zone_id during deletion:
%s', e)
+ logger.debug('Zone not found; no cleanup needed.')
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.')
+ record_id = self._find_txt_record_id(zone_id, record_name,
record_content)
+ if record_id:
+ try:
+ self.cf.dns.records.delete(dns_record_id=record_id,
zone_id=zone_id)
+ logger.debug('Successfully deleted TXT record.')
+ except cloudflare.APIStatusError as e:
+ logger.warning('Encountered Cloudflare API error deleting TXT
record: %s', e)
+ except cloudflare.APIConnectionError as e:
+ logger.warning('Network error deleting TXT record from
Cloudflare: %s', e)
else:
- logger.debug('Zone not found; no cleanup needed.')
+ logger.debug('TXT record 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.
+ :returns: The zone_id for the first matching zone that has a non-empty
+ identifier. A zone with an empty/invalid id is treated as if no
zone
+ were found, so this method never returns an empty string.
: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]] = []
+ zone: Zone | None = None
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)
+ zone = next(iter(self.cf.zones.list(name=zone_name,
per_page=1)), None)
+ except cloudflare.APIStatusError as e:
+ code = _cf_error_code(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 ''))
+ hint = ('Did you copy your entire API token/key? '
+ 'See {} to manage your API
tokens.'.format(ACCOUNT_URL))
elif code == 9103:
hint = 'Did you enter the correct email address and Global
key?'
elif code == 9109:
@@ -215,13 +227,19 @@
'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
+ logger.debug('Unrecognised Cloudflare API error while
finding zone_id: %s. '
+ 'Continuing with next zone guess...', e)
+ except cloudflare.APIConnectionError as e:
+ raise errors.PluginError('Network error contacting the
Cloudflare API while '
+ 'looking up the zone for {0}:
{1}'.format(domain, e))
+
+ if zone:
+ zone_id = zone.id
+ if zone_id:
+ logger.debug('Found zone_id of %s for %s using name %s',
+ zone_id, domain, zone_name)
+ return zone_id
+ break # Found a zone but it has no usable ID; stop searching
if msg is not None:
if 'com.cloudflare.api.account.zone.list' in msg:
@@ -252,20 +270,38 @@
: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 = list(self.cf.dns.records.list(
+ zone_id=zone_id, type='TXT', name={'exact': record_name},
+ content={'exact': record_content}, per_page=1))
+ except cloudflare.APIStatusError as e:
+ logger.debug('Encountered Cloudflare API error getting TXT
record_id: %s', e)
+ records = []
+ except cloudflare.APIConnectionError as e:
+ logger.debug('Network error getting TXT record_id from Cloudflare:
%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'])
+ return records[0].id
logger.debug('Unable to find TXT record.')
return None
+
+
+class _RecordData(TypedDict):
+ """Offers type hints for dictionaries of Cloudflare API parameters."""
+
+ type: Literal['TXT']
+ name: str
+ content: str
+ ttl: int
+
+
+def _cf_error_code(e: cloudflare.APIStatusError) -> int | None:
+ """Extract the first Cloudflare error code from an API error response."""
+ try:
+ body = e.response.json()
+ return int(body['errors'][0]['code'])
+ except (ValueError, KeyError, IndexError, TypeError): # pragma: no cover
+ return None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-5.4.0/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
new/certbot_dns_cloudflare-5.6.0/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
---
old/certbot_dns_cloudflare-5.4.0/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
2026-03-10 18:46:56.000000000 +0100
+++
new/certbot_dns_cloudflare-5.6.0/src/certbot_dns_cloudflare/_internal/tests/dns_cloudflare_test.py
2026-05-11 17:56:16.000000000 +0200
@@ -2,9 +2,10 @@
import sys
import unittest
+from typing import Any
from unittest import mock
-import CloudFlare
+import cloudflare
import pytest
from certbot import errors
@@ -13,7 +14,23 @@
from certbot.plugins.dns_test_common import DOMAIN
from certbot.tests import util as test_util
-API_ERROR = CloudFlare.exceptions.CloudFlareAPIError(1000, '', '')
+
+def _make_api_error(cf_code: int, msg: str = '', http_status: int = 400
+ ) -> cloudflare.APIStatusError:
+ """Build a cloudflare.APIStatusError with a Cloudflare error code in the
body."""
+ body: Any = {'success': False, 'errors': [{'code': cf_code, 'message':
msg}]}
+ response = mock.Mock(status_code=http_status, request=mock.Mock())
+ response.json.return_value = body
+ return cloudflare.APIStatusError(message=msg or str(cf_code),
response=response, body=body)
+
+
+def _make_connection_error(msg: str = 'Connection error.') ->
cloudflare.APIConnectionError:
+ """Build a cloudflare.APIConnectionError (e.g. transient network
failure)."""
+ return cloudflare.APIConnectionError(message=msg, request=mock.Mock())
+
+
+API_ERROR = _make_api_error(1000)
+CONNECTION_ERROR = _make_connection_error()
API_TOKEN = 'an-api-token'
@@ -96,13 +113,43 @@
with pytest.raises(errors.PluginError):
self.auth.perform([self.achall])
+ def test_get_cloudflare_client_with_api_token(self):
+ from certbot_dns_cloudflare._internal.dns_cloudflare import
Authenticator
+ from certbot_dns_cloudflare._internal.dns_cloudflare import
_CloudflareClient
+ mock_auth = mock.MagicMock()
+ mock_auth.credentials.conf.return_value = API_TOKEN
+ client = Authenticator._get_cloudflare_client(mock_auth)
+ self.assertIsInstance(client, _CloudflareClient)
+
+ def test_get_cloudflare_client_with_email_key(self):
+ from certbot_dns_cloudflare._internal.dns_cloudflare import
Authenticator
+ from certbot_dns_cloudflare._internal.dns_cloudflare import
_CloudflareClient
+ mock_auth = mock.MagicMock()
+ mock_auth.credentials.conf.side_effect = lambda k: None if k ==
'api-token' else 'some_value'
+ client = Authenticator._get_cloudflare_client(mock_auth)
+ self.assertIsInstance(client, _CloudflareClient)
+
+
+def _mock_zone(zone_id: str | None) -> mock.MagicMock:
+ """Create a mock zone object with an .id attribute."""
+ zone = mock.MagicMock()
+ zone.id = zone_id
+ return zone
+
+
+def _mock_record(record_id: str | None) -> mock.MagicMock:
+ """Create a mock DNS record object with an .id attribute."""
+ record = mock.MagicMock()
+ record.id = record_id
+ return record
+
class CloudflareClientTest(unittest.TestCase):
record_name = "foo"
record_content = "bar"
record_ttl = 42
- zone_id = 1
- record_id = 2
+ zone_id = "zone-id-1"
+ record_id = "record-id-2"
def setUp(self):
from certbot_dns_cloudflare._internal.dns_cloudflare import
_CloudflareClient
@@ -113,119 +160,145 @@
self.cloudflare_client.cf = self.cf
def test_add_txt_record(self):
- self.cf.zones.get.return_value = [{'id': self.zone_id}]
+ self.cf.zones.list.return_value = [_mock_zone(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']
+ self.cf.dns.records.create.assert_called_with(
+ zone_id=self.zone_id, type='TXT', name=self.record_name,
+ content=self.record_content, ttl=self.record_ttl)
def test_add_txt_record_error(self):
- self.cf.zones.get.return_value = [{'id': self.zone_id}]
+ self.cf.zones.list.return_value = [_mock_zone(self.zone_id)]
- self.cf.zones.dns_records.post.side_effect =
CloudFlare.exceptions.CloudFlareAPIError(1009, '', '')
+ self.cf.dns.records.create.side_effect = _make_api_error(1009)
with pytest.raises(errors.PluginError):
- self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content, self.record_ttl)
+ 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
+ self.cf.zones.list.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)
+ 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 = []
+ self.cf.zones.list.return_value = []
with pytest.raises(errors.PluginError):
- self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content, self.record_ttl)
+ self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content,
+ self.record_ttl)
+
+ def test_add_txt_record_connection_error_on_create(self):
+ self.cf.zones.list.return_value = [_mock_zone(self.zone_id)]
+ self.cf.dns.records.create.side_effect = CONNECTION_ERROR
+
+ with pytest.raises(errors.PluginError, match='Network error'):
+ self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content,
+ self.record_ttl)
+
+ def test_add_txt_record_connection_error_during_zone_lookup(self):
+ self.cf.zones.list.side_effect = CONNECTION_ERROR
+
+ with pytest.raises(errors.PluginError, match='Network error'):
+ 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, '', '')
+ self.cf.zones.list.side_effect = _make_api_error(6003)
with pytest.raises(errors.PluginError):
- self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content, self.record_ttl)
+ 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, '', '')
+ self.cf.zones.list.side_effect = _make_api_error(9103)
with pytest.raises(errors.PluginError):
- self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content, self.record_ttl)
+ 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, '', '')
+ self.cf.zones.list.side_effect = _make_api_error(9109)
with pytest.raises(errors.PluginError):
- self.cloudflare_client.add_txt_record(DOMAIN, self.record_name,
self.record_content, self.record_ttl)
+ 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', '')
+ self.cf.zones.list.side_effect = _make_api_error(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)
+ 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.cf.zones.list.return_value = [_mock_zone(self.zone_id)]
+ self.cf.dns.records.list.return_value = [_mock_record(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']
+ self.cf.zones.list.assert_called_once()
+ self.cf.dns.records.list.assert_called_once_with(
+ zone_id=self.zone_id, type='TXT', name={'exact': self.record_name},
+ content={'exact': self.record_content}, per_page=1)
+ self.cf.dns.records.delete.assert_called_once_with(
+ dns_record_id=self.record_id, zone_id=self.zone_id)
def test_del_txt_record_error_during_zone_lookup(self):
- self.cf.zones.get.side_effect = API_ERROR
+ self.cf.zones.list.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.cf.zones.list.return_value = [_mock_zone(self.zone_id)]
+ self.cf.dns.records.list.return_value = [_mock_record(self.record_id)]
+ self.cf.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
+ self.cf.dns.records.delete.assert_called_once_with(
+ dns_record_id=self.record_id, zone_id=self.zone_id)
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.cf.zones.list.return_value = [_mock_zone(self.zone_id)]
+ self.cf.dns.records.list.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
+ self.cf.dns.records.list.assert_called_once()
+ self.cf.dns.records.delete.assert_not_called()
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.cf.zones.list.return_value = [_mock_zone(self.zone_id)]
+ self.cf.dns.records.list.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
+ self.cf.dns.records.list.assert_called_once()
+ self.cf.dns.records.delete.assert_not_called()
def test_del_txt_record_no_zone(self):
- self.cf.zones.get.return_value = [{'id': None}]
+ self.cf.zones.list.return_value = [_mock_zone(None)]
+
+ self.cloudflare_client.del_txt_record(DOMAIN, self.record_name,
self.record_content)
+
+ self.cf.zones.list.assert_called_once()
+
+ def test_del_txt_record_connection_error_on_delete(self):
+ self.cf.zones.list.return_value = [_mock_zone(self.zone_id)]
+ self.cf.dns.records.list.return_value = [_mock_record(self.record_id)]
+ self.cf.dns.records.delete.side_effect = CONNECTION_ERROR
+
+ self.cloudflare_client.del_txt_record(DOMAIN, self.record_name,
self.record_content)
+
+ self.cf.dns.records.delete.assert_called_once_with(
+ dns_record_id=self.record_id, zone_id=self.zone_id)
+
+ def test_del_txt_record_connection_error_on_get(self):
+ self.cf.zones.list.return_value = [_mock_zone(self.zone_id)]
+ self.cf.dns.records.list.side_effect = CONNECTION_ERROR
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
+ self.cf.dns.records.list.assert_called_once()
+ self.cf.dns.records.delete.assert_not_called()
if __name__ == "__main__":
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/certbot_dns_cloudflare-5.4.0/src/certbot_dns_cloudflare.egg-info/PKG-INFO
new/certbot_dns_cloudflare-5.6.0/src/certbot_dns_cloudflare.egg-info/PKG-INFO
---
old/certbot_dns_cloudflare-5.4.0/src/certbot_dns_cloudflare.egg-info/PKG-INFO
2026-03-10 18:47:02.000000000 +0100
+++
new/certbot_dns_cloudflare-5.6.0/src/certbot_dns_cloudflare.egg-info/PKG-INFO
2026-05-11 17:56:24.000000000 +0200
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: certbot-dns-cloudflare
-Version: 5.4.0
+Version: 5.6.0
Summary: Cloudflare DNS Authenticator plugin for Certbot
Author: Certbot Project
License-Expression: Apache-2.0
@@ -25,9 +25,9 @@
Requires-Python: >=3.10
Description-Content-Type: text/x-rst
License-File: LICENSE.txt
-Requires-Dist: cloudflare<2.20,>=2.19
-Requires-Dist: acme>=5.4.0
-Requires-Dist: certbot>=5.4.0
+Requires-Dist: cloudflare>=4.0
+Requires-Dist: acme>=5.6.0
+Requires-Dist: certbot>=5.6.0
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-5.4.0/src/certbot_dns_cloudflare.egg-info/requires.txt
new/certbot_dns_cloudflare-5.6.0/src/certbot_dns_cloudflare.egg-info/requires.txt
---
old/certbot_dns_cloudflare-5.4.0/src/certbot_dns_cloudflare.egg-info/requires.txt
2026-03-10 18:47:02.000000000 +0100
+++
new/certbot_dns_cloudflare-5.6.0/src/certbot_dns_cloudflare.egg-info/requires.txt
2026-05-11 17:56:24.000000000 +0200
@@ -1,6 +1,6 @@
-cloudflare<2.20,>=2.19
-acme>=5.4.0
-certbot>=5.4.0
+cloudflare>=4.0
+acme>=5.6.0
+certbot>=5.6.0
[docs]
Sphinx>=1.0