mirror of
https://github.com/certbot/certbot.git
synced 2026-04-24 15:54:10 -04:00
Merge 7307978fd1 into 9599364837
This commit is contained in:
commit
140a297593
6 changed files with 233 additions and 178 deletions
|
|
@ -5,9 +5,7 @@ from setuptools import setup
|
|||
version = '5.5.0.dev0'
|
||||
|
||||
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'):
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
"""DNS Authenticator for Cloudflare."""
|
||||
import logging
|
||||
import warnings
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Optional
|
||||
from typing import cast
|
||||
|
||||
import CloudFlare
|
||||
# cloudflare 4.x includes a pydantic v1 compatibility shim that raises a
|
||||
# UserWarning on Python 3.14+. Suppress it here so that certbot's
|
||||
# filterwarnings=error test configuration does not turn the warning into a
|
||||
# fatal exception during plugin discovery.
|
||||
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 +103,15 @@ class _CloudflareClient:
|
|||
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,22 +127,20 @@ class _CloudflareClient:
|
|||
|
||||
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)
|
||||
logger.debug('Attempting to add record to zone %s: %s', zone_id,
|
||||
{'type': 'TXT', 'name': record_name,
|
||||
'content': record_content, 'ttl': record_ttl})
|
||||
self.cf.dns.records.create(zone_id=zone_id, type='TXT', name=record_name,
|
||||
content=record_content, ttl=record_ttl)
|
||||
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 ''))
|
||||
|
||||
|
|
@ -161,19 +167,15 @@ class _CloudflareClient:
|
|||
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.')
|
||||
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)
|
||||
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:
|
||||
"""
|
||||
|
|
@ -186,25 +188,20 @@ class _CloudflareClient:
|
|||
"""
|
||||
|
||||
zone_name_guesses = dns_common.base_domain_name_guesses(domain)
|
||||
zones: list[dict[str, Any]] = []
|
||||
zone: Optional[Zone] = 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 +212,16 @@ class _CloudflareClient:
|
|||
'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)
|
||||
logger.debug('Unrecognised Cloudflare API error while finding zone_id: %s. '
|
||||
'Continuing with next zone guess...', 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 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 +252,26 @@ class _CloudflareClient:
|
|||
: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 = []
|
||||
|
||||
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
|
||||
|
||||
|
||||
def _cf_error_code(e: cloudflare.APIStatusError) -> Optional[int]:
|
||||
"""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
|
||||
|
|
|
|||
|
|
@ -2,9 +2,17 @@
|
|||
|
||||
import sys
|
||||
import unittest
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
from unittest import mock
|
||||
|
||||
import CloudFlare
|
||||
import warnings
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings('ignore', message='Core Pydantic V1 functionality',
|
||||
category=UserWarning)
|
||||
import cloudflare
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from certbot import errors
|
||||
|
|
@ -13,7 +21,17 @@ 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, '', '')
|
||||
|
||||
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 = httpx.Response(http_status, json=body,
|
||||
request=httpx.Request('GET', 'https://api.cloudflare.com'))
|
||||
return cloudflare.APIStatusError(message=msg or str(cf_code), response=response, body=body)
|
||||
|
||||
|
||||
API_ERROR = _make_api_error(1000)
|
||||
|
||||
API_TOKEN = 'an-api-token'
|
||||
|
||||
|
|
@ -96,13 +114,43 @@ class AuthenticatorTest(test_util.TempDirTestCase, dns_test_common.BaseAuthentic
|
|||
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: Optional[str]) -> 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: Optional[str]) -> 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 +161,111 @@ class CloudflareClientTest(unittest.TestCase):
|
|||
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_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)
|
||||
expected = [mock.call.zones.get(params=mock.ANY)]
|
||||
|
||||
assert expected == self.cf.mock_calls
|
||||
self.cf.zones.list.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -1,44 +1,47 @@
|
|||
# This file was generated by tools/pinning/oldest/repin.sh and can be updated using
|
||||
# that script.
|
||||
annotated-types==0.7.0 ; python_version == "3.10"
|
||||
anyio==4.12.1 ; python_version == "3.10"
|
||||
apacheconfig==0.3.2 ; python_version == "3.10"
|
||||
asn1crypto==0.24.0 ; python_version == "3.10"
|
||||
astroid==4.0.3 ; python_version == "3.10"
|
||||
attrs==25.4.0 ; python_version == "3.10"
|
||||
astroid==4.0.4 ; python_version == "3.10"
|
||||
beautifulsoup4==4.14.3 ; python_version == "3.10"
|
||||
boto3==1.20.34 ; python_version == "3.10"
|
||||
botocore==1.23.34 ; python_version == "3.10"
|
||||
cachetools==5.5.2 ; python_version == "3.10"
|
||||
certifi==2026.1.4 ; python_version == "3.10"
|
||||
certifi==2026.2.25 ; python_version == "3.10"
|
||||
cffi==1.14.1 ; python_version == "3.10"
|
||||
chardet==3.0.4 ; python_version == "3.10"
|
||||
cloudflare==2.19.0 ; python_version == "3.10"
|
||||
cloudflare==4.0.0 ; python_version == "3.10"
|
||||
colorama==0.4.6 ; (sys_platform == "win32" or platform_system == "Windows") and python_version == "3.10"
|
||||
configargparse==1.5.3 ; python_version == "3.10"
|
||||
configobj==5.0.6 ; python_version == "3.10"
|
||||
coverage==7.13.3 ; python_version == "3.10"
|
||||
coverage==7.13.4 ; python_version == "3.10"
|
||||
cryptography==43.0.0 ; python_version == "3.10"
|
||||
cython==0.29.37 ; python_version == "3.10"
|
||||
dill==0.4.1 ; python_version == "3.10"
|
||||
distlib==0.4.0 ; python_version == "3.10"
|
||||
distro==1.0.1 ; python_version == "3.10"
|
||||
distro==1.7.0 ; python_version == "3.10"
|
||||
dns-lexicon==3.15.1 ; python_version == "3.10"
|
||||
dnspython==2.6.1 ; python_version == "3.10"
|
||||
exceptiongroup==1.3.1 ; python_version == "3.10"
|
||||
execnet==2.1.2 ; python_version == "3.10"
|
||||
filelock==3.20.3 ; python_version == "3.10"
|
||||
filelock==3.25.0 ; python_version == "3.10"
|
||||
funcsigs==0.4 ; python_version == "3.10"
|
||||
google-api-python-client==1.6.5 ; python_version == "3.10"
|
||||
google-auth==2.16.0 ; python_version == "3.10"
|
||||
h11==0.16.0 ; python_version == "3.10"
|
||||
httpcore==1.0.9 ; python_version == "3.10"
|
||||
httplib2==0.9.2 ; python_version == "3.10"
|
||||
idna==2.6 ; python_version == "3.10"
|
||||
httpx==0.28.1 ; python_version == "3.10"
|
||||
idna==2.8 ; python_version == "3.10"
|
||||
iniconfig==2.3.0 ; python_version == "3.10"
|
||||
ipaddress==1.0.16 ; python_version == "3.10"
|
||||
isort==7.0.0 ; python_version == "3.10"
|
||||
isort==8.0.1 ; python_version == "3.10"
|
||||
jmespath==0.10.0 ; python_version == "3.10"
|
||||
josepy==2.2.0 ; python_version == "3.10"
|
||||
jsonlines==4.0.0 ; python_version == "3.10"
|
||||
jsonpickle==4.1.1 ; python_version == "3.10"
|
||||
librt==0.7.8 ; python_version == "3.10" and platform_python_implementation != "PyPy"
|
||||
librt==0.8.1 ; python_version == "3.10" and platform_python_implementation != "PyPy"
|
||||
mccabe==0.7.0 ; python_version == "3.10"
|
||||
mypy-extensions==1.1.0 ; python_version == "3.10"
|
||||
mypy==1.19.1 ; python_version == "3.10"
|
||||
|
|
@ -48,16 +51,18 @@ packaging==26.0 ; python_version == "3.10"
|
|||
parsedatetime==2.6 ; python_version == "3.10"
|
||||
pathspec==1.0.4 ; python_version == "3.10"
|
||||
pbr==1.8.0 ; python_version == "3.10"
|
||||
pip==26.0 ; python_version == "3.10"
|
||||
platformdirs==4.5.1 ; python_version == "3.10"
|
||||
pip==26.0.1 ; python_version == "3.10"
|
||||
platformdirs==4.9.2 ; python_version == "3.10"
|
||||
pluggy==1.6.0 ; python_version == "3.10"
|
||||
ply==3.4 ; python_version == "3.10"
|
||||
py==1.11.0 ; python_version == "3.10"
|
||||
pyasn1-modules==0.4.1 ; python_version == "3.10"
|
||||
pyasn1==0.4.8 ; python_version == "3.10"
|
||||
pycparser==2.14 ; python_version == "3.10"
|
||||
pydantic-core==2.41.5 ; python_version == "3.10"
|
||||
pydantic==2.12.5 ; python_version == "3.10"
|
||||
pygments==2.19.2 ; python_version == "3.10"
|
||||
pylint==4.0.4 ; python_version == "3.10"
|
||||
pylint==4.0.5 ; python_version == "3.10"
|
||||
pyopenssl==25.0.0 ; python_version == "3.10"
|
||||
pyotp==2.9.0 ; python_version == "3.10"
|
||||
pyparsing==3.0.0 ; python_version == "3.10"
|
||||
|
|
@ -68,16 +73,18 @@ pytest==9.0.2 ; python_version == "3.10"
|
|||
python-augeas==0.5.0 ; python_version == "3.10"
|
||||
python-dateutil==2.9.0.post0 ; python_version == "3.10"
|
||||
python-digitalocean==1.15.0 ; python_version == "3.10"
|
||||
pytz==2025.2 ; python_version == "3.10"
|
||||
python-discovery==1.1.0 ; python_version == "3.10"
|
||||
pytz==2026.1.post1 ; python_version == "3.10"
|
||||
pywin32==311 ; python_version == "3.10" and sys_platform == "win32"
|
||||
pyyaml==6.0.3 ; python_version == "3.10"
|
||||
requests-file==3.0.1 ; python_version == "3.10"
|
||||
requests==2.25.1 ; python_version == "3.10"
|
||||
rsa==4.9.1 ; python_version == "3.10"
|
||||
ruff==0.15.0 ; python_version == "3.10"
|
||||
ruff==0.15.4 ; python_version == "3.10"
|
||||
s3transfer==0.5.2 ; python_version == "3.10"
|
||||
setuptools==80.10.2 ; python_version == "3.10"
|
||||
setuptools==82.0.0 ; python_version == "3.10"
|
||||
six==1.16.0 ; python_version == "3.10"
|
||||
sniffio==1.3.1 ; python_version == "3.10"
|
||||
soupsieve==2.8.3 ; python_version == "3.10"
|
||||
tldextract==5.3.1 ; python_version == "3.10"
|
||||
tomli==2.4.0 ; python_version == "3.10"
|
||||
|
|
@ -85,14 +92,15 @@ tomlkit==0.14.0 ; python_version == "3.10"
|
|||
tox==3.28.0 ; python_version == "3.10"
|
||||
types-httplib2==0.31.2.20260125 ; python_version == "3.10"
|
||||
types-pyrfc3339==2.0.1.20250825 ; python_version == "3.10"
|
||||
types-python-dateutil==2.9.0.20260124 ; python_version == "3.10"
|
||||
types-python-dateutil==2.9.0.20260302 ; python_version == "3.10"
|
||||
types-pywin32==311.0.0.20251008 ; python_version == "3.10"
|
||||
types-requests==2.31.0.6 ; python_version == "3.10"
|
||||
types-setuptools==80.10.0.20260124 ; python_version == "3.10"
|
||||
types-setuptools==82.0.0.20260210 ; python_version == "3.10"
|
||||
types-urllib3==1.26.25.14 ; python_version == "3.10"
|
||||
typing-extensions==4.15.0 ; python_version == "3.10"
|
||||
typing-inspection==0.4.2 ; python_version == "3.10"
|
||||
uritemplate==3.0.1 ; python_version == "3.10"
|
||||
urllib3==1.26.5 ; python_version == "3.10"
|
||||
uv==0.9.28 ; python_version == "3.10"
|
||||
virtualenv==20.36.1 ; python_version == "3.10"
|
||||
uv==0.10.8 ; python_version == "3.10"
|
||||
virtualenv==21.1.0 ; python_version == "3.10"
|
||||
wheel==0.46.3 ; python_version == "3.10"
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ license = "Apache License 2.0"
|
|||
[tool.poetry.dependencies]
|
||||
# The Python version here should be kept in sync with the one used in our
|
||||
# oldest tests in tox.ini.
|
||||
python = "<3.11 >= 3.10"
|
||||
python = "<3.11,>=3.10"
|
||||
|
||||
# Local dependencies
|
||||
# Any local packages that have dependencies on other local packages must be
|
||||
|
|
@ -59,17 +59,17 @@ boto3 = "1.20.34"
|
|||
botocore = "1.23.34"
|
||||
cffi = "1.14.1"
|
||||
chardet = "3.0.4"
|
||||
cloudflare = "2.19"
|
||||
cloudflare = "4.0.0"
|
||||
configobj = "5.0.6"
|
||||
cryptography = "43.0.0"
|
||||
distro = "1.0.1"
|
||||
distro = "1.7.0"
|
||||
dns-lexicon = "3.15.1"
|
||||
dnspython = "2.6.1"
|
||||
funcsigs = "0.4"
|
||||
google-api-python-client = "1.6.5"
|
||||
google-auth = "2.16.0"
|
||||
httplib2 = "0.9.2"
|
||||
idna = "2.6"
|
||||
idna = "2.8"
|
||||
ipaddress = "1.0.16"
|
||||
ndg-httpsclient = "0.3.2"
|
||||
parsedatetime = "2.6"
|
||||
|
|
|
|||
|
|
@ -6,35 +6,34 @@
|
|||
# https://docs.github.com/en/github/visualizing-repository-data-with-graphs/about-the-dependency-graph#supported-package-ecosystems
|
||||
# for more info.
|
||||
alabaster==1.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
annotated-types==0.7.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
anyio==4.12.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
apacheconfig==0.3.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
astroid==3.3.11 ; python_version >= "3.10" and python_version < "4.0"
|
||||
asttokens==3.0.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
attrs==25.4.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
azure-core==1.38.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
azure-core==1.38.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
azure-devops==7.1.0b4 ; python_version >= "3.10" and python_version < "4.0"
|
||||
babel==2.18.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
backports-tarfile==1.2.0 ; python_version >= "3.10" and python_version < "3.12"
|
||||
bcrypt==5.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
beautifulsoup4==4.14.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
boto3==1.42.40 ; python_version >= "3.10" and python_version < "4.0"
|
||||
botocore==1.42.40 ; python_version >= "3.10" and python_version < "4.0"
|
||||
boto3==1.42.59 ; python_version >= "3.10" and python_version < "4.0"
|
||||
botocore==1.42.59 ; python_version >= "3.10" and python_version < "4.0"
|
||||
build==1.4.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
cachecontrol==0.14.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||
cachetools==7.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
certifi==2026.1.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||
cachetools==7.0.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
certifi==2026.2.25 ; python_version >= "3.10" and python_version < "4.0"
|
||||
cffi==2.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
chardet==5.2.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
charset-normalizer==3.4.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||
cleo==2.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
click==8.3.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
cloudflare==2.19.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||
cloudflare==4.3.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
colorama==0.4.6 ; python_version >= "3.10" and python_version < "4.0"
|
||||
configargparse==1.7.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
configobj==5.0.9 ; python_version >= "3.10" and python_version < "4.0"
|
||||
coverage==7.13.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
coverage==7.13.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||
crashtest==0.4.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
cryptography==46.0.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||
cryptography==46.0.5 ; python_version >= "3.10" and python_version < "4.0"
|
||||
cython==0.29.37 ; python_version >= "3.10" and python_version <= "3.12"
|
||||
cython==3.2.4 ; python_version >= "3.13" and python_version < "4.0"
|
||||
decorator==5.2.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
|
|
@ -46,16 +45,16 @@ dns-lexicon==3.23.2 ; python_version >= "3.10" and python_version < "4.0"
|
|||
dnspython==2.8.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
docutils==0.21.2 ; python_version == "3.10"
|
||||
docutils==0.22.4 ; python_version >= "3.11" and python_version < "4.0"
|
||||
dulwich==1.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
dulwich==1.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
exceptiongroup==1.3.1 ; python_version == "3.10"
|
||||
execnet==2.1.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
executing==2.2.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
fabric==3.2.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
fastjsonschema==2.21.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
filelock==3.20.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
filelock==3.25.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
findpython==0.7.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
google-api-core==2.29.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
google-api-python-client==2.188.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
google-api-core==2.30.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
google-api-python-client==2.191.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
google-auth-httplib2==0.3.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
google-auth==2.48.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
googleapis-common-protos==1.72.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
|
|
@ -63,7 +62,7 @@ h11==0.16.0 ; python_version >= "3.10" and python_version < "4.0"
|
|||
httpcore==1.0.9 ; python_version >= "3.10" and python_version < "4.0"
|
||||
httplib2==0.31.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
httpx==0.28.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
id==1.5.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
id==1.6.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
idna==3.11 ; python_version >= "3.10" and python_version < "4.0"
|
||||
imagesize==1.4.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
importlib-metadata==8.7.1 ; python_version >= "3.10" and python_version < "3.12"
|
||||
|
|
@ -84,7 +83,6 @@ jeepney==0.9.0 ; python_version >= "3.10" and python_version < "4.0" and sys_pla
|
|||
jinja2==3.1.6 ; python_version >= "3.10" and python_version < "4.0"
|
||||
jmespath==1.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
josepy==2.2.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
jsonlines==4.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
jsonpickle==4.1.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
keyring==25.7.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
markdown-it-py==4.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
|
|
@ -97,17 +95,17 @@ msgpack==1.1.2 ; python_version >= "3.10" and python_version < "4.0"
|
|||
msrest==0.7.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
mypy-extensions==1.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
mypy==1.9.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
nh3==0.3.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
nh3==0.3.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
oauthlib==3.3.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
packaging==26.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
paramiko==4.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
parsedatetime==2.6 ; python_version >= "3.10" and python_version < "4.0"
|
||||
parso==0.8.5 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pbs-installer==2026.1.27 ; python_version >= "3.10" and python_version < "4.0"
|
||||
parso==0.8.6 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pbs-installer==2026.2.11 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pexpect==4.9.0 ; python_version >= "3.10" and python_version < "4.0" and sys_platform != "win32" and sys_platform != "emscripten"
|
||||
pip==26.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pip==26.0.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pkginfo==1.12.1.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
platformdirs==4.5.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
platformdirs==4.9.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pluggy==1.6.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
ply==3.11 ; python_version >= "3.10" and python_version < "4.0"
|
||||
poetry-core==2.3.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
|
|
@ -121,6 +119,8 @@ pure-eval==0.2.3 ; python_version >= "3.10" and python_version < "4.0"
|
|||
pyasn1-modules==0.4.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pyasn1==0.6.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pycparser==3.0 ; python_version >= "3.10" and python_version < "4.0" and implementation_name != "PyPy"
|
||||
pydantic-core==2.41.5 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pydantic==2.12.5 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pygments==2.19.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pylint==3.3.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pynacl==1.6.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
|
|
@ -136,6 +136,7 @@ pytest==9.0.2 ; python_version >= "3.10" and python_version < "4.0"
|
|||
python-augeas==1.2.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
python-dateutil==2.9.0.post0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
python-digitalocean==1.17.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
python-discovery==1.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
pywin32-ctypes==0.2.3 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32"
|
||||
pywin32==311 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32"
|
||||
pyyaml==6.0.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
|
|
@ -146,17 +147,18 @@ requests-oauthlib==2.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
|||
requests-toolbelt==1.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
requests==2.32.5 ; python_version >= "3.10" and python_version < "4.0"
|
||||
rfc3986==2.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
rich==14.3.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
rich==14.3.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
roman-numerals==4.1.0 ; python_version >= "3.11" and python_version < "4.0"
|
||||
rsa==4.9.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
ruff==0.15.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
ruff==0.15.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||
s3transfer==0.16.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
secretstorage==3.5.0 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "linux"
|
||||
semantic-version==2.10.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
setuptools-rust==1.12.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
setuptools==80.10.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
setuptools==82.0.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
shellingham==1.5.4 ; python_version >= "3.10" and python_version < "4.0"
|
||||
six==1.17.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
sniffio==1.3.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
snowballstemmer==3.0.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
soupsieve==2.8.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
sphinx-rtd-theme==3.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
|
|
@ -175,22 +177,23 @@ tldextract==5.3.1 ; python_version >= "3.10" and python_version < "4.0"
|
|||
tomli==2.4.0 ; python_version == "3.10"
|
||||
tomlkit==0.14.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
towncrier==25.8.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
tox==4.34.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
tox==4.47.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
traitlets==5.14.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
trove-classifiers==2026.1.14.14 ; python_version >= "3.10" and python_version < "4.0"
|
||||
twine==6.2.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
types-httplib2==0.31.2.20260125 ; python_version >= "3.10" and python_version < "4.0"
|
||||
types-pyrfc3339==2.0.1.20250825 ; python_version >= "3.10" and python_version < "4.0"
|
||||
types-python-dateutil==2.9.0.20260124 ; python_version >= "3.10" and python_version < "4.0"
|
||||
types-python-dateutil==2.9.0.20260302 ; python_version >= "3.10" and python_version < "4.0"
|
||||
types-pywin32==311.0.0.20251008 ; python_version >= "3.10" and python_version < "4.0"
|
||||
types-requests==2.32.4.20260107 ; python_version >= "3.10" and python_version < "4.0"
|
||||
types-setuptools==80.10.0.20260124 ; python_version >= "3.10" and python_version < "4.0"
|
||||
types-setuptools==82.0.0.20260210 ; python_version >= "3.10" and python_version < "4.0"
|
||||
typing-extensions==4.15.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
typing-inspection==0.4.2 ; python_version >= "3.10" and python_version < "4.0"
|
||||
uritemplate==4.2.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
urllib3==2.6.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
uv==0.9.28 ; python_version >= "3.10" and python_version < "4.0"
|
||||
virtualenv==20.36.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
wcwidth==0.5.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
uv==0.10.7 ; python_version >= "3.10" and python_version < "4.0"
|
||||
virtualenv==21.1.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
wcwidth==0.6.0 ; python_version >= "3.10" and python_version < "4.0"
|
||||
wheel==0.46.3 ; python_version >= "3.10" and python_version < "4.0"
|
||||
wrapt==2.1.1 ; python_version >= "3.10" and python_version < "4.0"
|
||||
xattr==1.3.0 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "darwin"
|
||||
|
|
|
|||
Loading…
Reference in a new issue