We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Upgrade to dns-digitalocean 1.6 broke renewals
certbot/dns-digitalocean:v1.6.0

```
$ certbot renew
...
Processing /etc/letsencrypt/renewal/omcr.io.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cert is due for renewal, auto-renewing...
Plugins selected: Authenticator dns-digitalocean, Installer None
Renewing an existing certificate
Performing the following challenges:
dns-01 challenge for omcr.io
Waiting 10 seconds for DNS changes to propagate
Waiting for verification...
Challenge failed for domain omcr.io
dns-01 challenge for omcr.io
Cleaning up challenges
Attempting to renew cert (omcr.io) from /etc/letsencrypt/renewal/omcr.io.conf produced an unexpected error: Some challenges have failed.. Skipping.
...
 - The following errors were reported by the server:

   Domain: omcr.io
   Type:   unauthorized
   Detail: Incorrect TXT record
   "mKIy_DFn65L85GgEMgl8BYiKYi4dSzwbG3_aJoSvwZo" found at
   _acme-challenge.omcr.io

   To fix these errors, please make sure that your domain name was
   entered correctly and the DNS A/AAAA record(s) for that domain
   contain(s) the right IP address.
```


Allow setting DNS TTL
Fixes #7969.

## Pull Request Checklist

- [x] If the change being made is to a [distributed component](https://certbot.eff.org/docs/contributing.html#code-components-and-layout), edit the `master` section of `certbot/CHANGELOG.md` to include a description of the change being made.
- [x] Include your name in `AUTHORS.md` if you like.

# Purpose
This pull request is to allow setting a custom TTL when using DigitalOcean TXT records for authentication, as well as to lower the default to a more reasonable 60 seconds.

# Rationale
The default used by the dependent `python-digitalocean` library is 1800 seconds (one-half hour) which can cause frustrations when testing out configurations with `--dry-run`.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `certbot-dns-digitalocean/certbot_dns_digitalocean/_internal/dns_digitalocean.py`
Content:
```
1 """DNS Authenticator for DigitalOcean."""
2 import logging
3 
4 import digitalocean
5 import zope.interface
6 
7 from certbot import errors
8 from certbot import interfaces
9 from certbot.plugins import dns_common
10 
11 logger = logging.getLogger(__name__)
12 
13 
14 @zope.interface.implementer(interfaces.IAuthenticator)
15 @zope.interface.provider(interfaces.IPluginFactory)
16 class Authenticator(dns_common.DNSAuthenticator):
17     """DNS Authenticator for DigitalOcean
18 
19     This Authenticator uses the DigitalOcean API to fulfill a dns-01 challenge.
20     """
21 
22     description = 'Obtain certificates using a DNS TXT record (if you are ' + \
23                   'using DigitalOcean for DNS).'
24 
25     def __init__(self, *args, **kwargs):
26         super(Authenticator, self).__init__(*args, **kwargs)
27         self.credentials = None
28 
29     @classmethod
30     def add_parser_arguments(cls, add):  # pylint: disable=arguments-differ
31         super(Authenticator, cls).add_parser_arguments(add)
32         add('credentials', help='DigitalOcean credentials INI file.')
33 
34     def more_info(self):  # pylint: disable=missing-function-docstring
35         return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \
36                'the DigitalOcean API.'
37 
38     def _setup_credentials(self):
39         self.credentials = self._configure_credentials(
40             'credentials',
41             'DigitalOcean credentials INI file',
42             {
43                 'token': 'API token for DigitalOcean account'
44             }
45         )
46 
47     def _perform(self, domain, validation_name, validation):
48         self._get_digitalocean_client().add_txt_record(domain, validation_name, validation)
49 
50     def _cleanup(self, domain, validation_name, validation):
51         self._get_digitalocean_client().del_txt_record(domain, validation_name, validation)
52 
53     def _get_digitalocean_client(self):
54         return _DigitalOceanClient(self.credentials.conf('token'))
55 
56 
57 class _DigitalOceanClient:
58     """
59     Encapsulates all communication with the DigitalOcean API.
60     """
61 
62     def __init__(self, token):
63         self.manager = digitalocean.Manager(token=token)
64 
65     def add_txt_record(self, domain_name, record_name, record_content):
66         """
67         Add a TXT record using the supplied information.
68 
69         :param str domain_name: The domain to use to associate the record with.
70         :param str record_name: The record name (typically beginning with '_acme-challenge.').
71         :param str record_content: The record content (typically the challenge validation).
72         :raises certbot.errors.PluginError: if an error occurs communicating with the DigitalOcean
73                                             API
74         """
75 
76         try:
77             domain = self._find_domain(domain_name)
78         except digitalocean.Error as e:
79             hint = None
80 
81             if str(e).startswith("Unable to authenticate"):
82                 hint = 'Did you provide a valid API token?'
83 
84             logger.debug('Error finding domain using the DigitalOcean API: %s', e)
85             raise errors.PluginError('Error finding domain using the DigitalOcean API: {0}{1}'
86                                      .format(e, ' ({0})'.format(hint) if hint else ''))
87 
88         try:
89             result = domain.create_new_domain_record(
90                 type='TXT',
91                 name=self._compute_record_name(domain, record_name),
92                 data=record_content)
93 
94             record_id = result['domain_record']['id']
95 
96             logger.debug('Successfully added TXT record with id: %d', record_id)
97         except digitalocean.Error as e:
98             logger.debug('Error adding TXT record using the DigitalOcean API: %s', e)
99             raise errors.PluginError('Error adding TXT record using the DigitalOcean API: {0}'
100                                      .format(e))
101 
102     def del_txt_record(self, domain_name, record_name, record_content):
103         """
104         Delete a TXT record using the supplied information.
105 
106         Note that both the record's name and content are used to ensure that similar records
107         created concurrently (e.g., due to concurrent invocations of this plugin) are not deleted.
108 
109         Failures are logged, but not raised.
110 
111         :param str domain_name: The domain to use to associate the record with.
112         :param str record_name: The record name (typically beginning with '_acme-challenge.').
113         :param str record_content: The record content (typically the challenge validation).
114         """
115 
116         try:
117             domain = self._find_domain(domain_name)
118         except digitalocean.Error as e:
119             logger.debug('Error finding domain using the DigitalOcean API: %s', e)
120             return
121 
122         try:
123             domain_records = domain.get_records()
124 
125             matching_records = [record for record in domain_records
126                                 if record.type == 'TXT'
127                                 and record.name == self._compute_record_name(domain, record_name)
128                                 and record.data == record_content]
129         except digitalocean.Error as e:
130             logger.debug('Error getting DNS records using the DigitalOcean API: %s', e)
131             return
132 
133         for record in matching_records:
134             try:
135                 logger.debug('Removing TXT record with id: %s', record.id)
136                 record.destroy()
137             except digitalocean.Error as e:
138                 logger.warning('Error deleting TXT record %s using the DigitalOcean API: %s',
139                             record.id, e)
140 
141     def _find_domain(self, domain_name):
142         """
143         Find the domain object for a given domain name.
144 
145         :param str domain_name: The domain name for which to find the corresponding Domain.
146         :returns: The Domain, if found.
147         :rtype: `~digitalocean.Domain`
148         :raises certbot.errors.PluginError: if no matching Domain is found.
149         """
150 
151         domain_name_guesses = dns_common.base_domain_name_guesses(domain_name)
152 
153         domains = self.manager.get_all_domains()
154 
155         for guess in domain_name_guesses:
156             matches = [domain for domain in domains if domain.name == guess]
157 
158             if matches:
159                 domain = matches[0]
160                 logger.debug('Found base domain for %s using name %s', domain_name, guess)
161                 return domain
162 
163         raise errors.PluginError('Unable to determine base domain for {0} using names: {1}.'
164                                  .format(domain_name, domain_name_guesses))
165 
166     @staticmethod
167     def _compute_record_name(domain, full_record_name):
168         # The domain, from DigitalOcean's point of view, is automatically appended.
169         return full_record_name.rpartition("." + domain.name)[0]
170
```

Path: `certbot-dns-digitalocean/setup.py`
Content:
```
1 import os
2 import sys
3 
4 from setuptools import find_packages
5 from setuptools import setup
6 
7 version = '1.14.0.dev0'
8 
9 # Remember to update local-oldest-requirements.txt when changing the minimum
10 # acme/certbot version.
11 install_requires = [
12     'python-digitalocean>=1.11',
13     'setuptools>=39.0.1',
14     'zope.interface',
15 ]
16 
17 if not os.environ.get('SNAP_BUILD'):
18     install_requires.extend([
19         'acme>=0.29.0',
20         'certbot>=1.1.0',
21     ])
22 elif 'bdist_wheel' in sys.argv[1:]:
23     raise RuntimeError('Unset SNAP_BUILD when building wheels '
24                        'to include certbot dependencies.')
25 if os.environ.get('SNAP_BUILD'):
26     install_requires.append('packaging')
27 
28 docs_extras = [
29     'Sphinx>=1.0',  # autodoc_member_order = 'bysource', autodoc_default_flags
30     'sphinx_rtd_theme',
31 ]
32 
33 setup(
34     name='certbot-dns-digitalocean',
35     version=version,
36     description="DigitalOcean DNS Authenticator plugin for Certbot",
37     url='https://github.com/certbot/certbot',
38     author="Certbot Project",
39     author_email='client-dev@letsencrypt.org',
40     license='Apache License 2.0',
41     python_requires='>=3.6',
42     classifiers=[
43         'Development Status :: 5 - Production/Stable',
44         'Environment :: Plugins',
45         'Intended Audience :: System Administrators',
46         'License :: OSI Approved :: Apache Software License',
47         'Operating System :: POSIX :: Linux',
48         'Programming Language :: Python',
49         'Programming Language :: Python :: 3',
50         'Programming Language :: Python :: 3.6',
51         'Programming Language :: Python :: 3.7',
52         'Programming Language :: Python :: 3.8',
53         'Programming Language :: Python :: 3.9',
54         'Topic :: Internet :: WWW/HTTP',
55         'Topic :: Security',
56         'Topic :: System :: Installation/Setup',
57         'Topic :: System :: Networking',
58         'Topic :: System :: Systems Administration',
59         'Topic :: Utilities',
60     ],
61 
62     packages=find_packages(),
63     include_package_data=True,
64     install_requires=install_requires,
65     extras_require={
66         'docs': docs_extras,
67     },
68     entry_points={
69         'certbot.plugins': [
70             'dns-digitalocean = certbot_dns_digitalocean._internal.dns_digitalocean:Authenticator',
71         ],
72     },
73 )
74
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/certbot-dns-digitalocean/certbot_dns_digitalocean/_internal/dns_digitalocean.py b/certbot-dns-digitalocean/certbot_dns_digitalocean/_internal/dns_digitalocean.py
--- a/certbot-dns-digitalocean/certbot_dns_digitalocean/_internal/dns_digitalocean.py
+++ b/certbot-dns-digitalocean/certbot_dns_digitalocean/_internal/dns_digitalocean.py
@@ -21,6 +21,7 @@
 
     description = 'Obtain certificates using a DNS TXT record (if you are ' + \
                   'using DigitalOcean for DNS).'
+    ttl = 30
 
     def __init__(self, *args, **kwargs):
         super(Authenticator, self).__init__(*args, **kwargs)
@@ -45,7 +46,8 @@
         )
 
     def _perform(self, domain, validation_name, validation):
-        self._get_digitalocean_client().add_txt_record(domain, validation_name, validation)
+        self._get_digitalocean_client().add_txt_record(domain, validation_name, validation,
+                                                       self.ttl)
 
     def _cleanup(self, domain, validation_name, validation):
         self._get_digitalocean_client().del_txt_record(domain, validation_name, validation)
@@ -62,13 +64,15 @@
     def __init__(self, token):
         self.manager = digitalocean.Manager(token=token)
 
-    def add_txt_record(self, domain_name, record_name, record_content):
+    def add_txt_record(self, domain_name: str, record_name: str, record_content: str,
+                       record_ttl: int):
         """
         Add a TXT record using the supplied information.
 
         :param str domain_name: The domain to use to associate the record with.
         :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.
         :raises certbot.errors.PluginError: if an error occurs communicating with the DigitalOcean
                                             API
         """
@@ -89,7 +93,8 @@
             result = domain.create_new_domain_record(
                 type='TXT',
                 name=self._compute_record_name(domain, record_name),
-                data=record_content)
+                data=record_content,
+                ttl=record_ttl) # ttl kwarg is only effective starting python-digitalocean 1.15.0
 
             record_id = result['domain_record']['id']
 
@@ -99,7 +104,7 @@
             raise errors.PluginError('Error adding TXT record using the DigitalOcean API: {0}'
                                      .format(e))
 
-    def del_txt_record(self, domain_name, record_name, record_content):
+    def del_txt_record(self, domain_name: str, record_name: str, record_content: str):
         """
         Delete a TXT record using the supplied information.
 
diff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py
--- a/certbot-dns-digitalocean/setup.py
+++ b/certbot-dns-digitalocean/setup.py
@@ -9,7 +9,7 @@
 # Remember to update local-oldest-requirements.txt when changing the minimum
 # acme/certbot version.
 install_requires = [
-    'python-digitalocean>=1.11',
+    'python-digitalocean>=1.11', # 1.15.0 or newer is recommended for TTL support
     'setuptools>=39.0.1',
     'zope.interface',
 ]

{"golden_diff": "diff --git a/certbot-dns-digitalocean/certbot_dns_digitalocean/_internal/dns_digitalocean.py b/certbot-dns-digitalocean/certbot_dns_digitalocean/_internal/dns_digitalocean.py\n--- a/certbot-dns-digitalocean/certbot_dns_digitalocean/_internal/dns_digitalocean.py\n+++ b/certbot-dns-digitalocean/certbot_dns_digitalocean/_internal/dns_digitalocean.py\n@@ -21,6 +21,7 @@\n \n     description = 'Obtain certificates using a DNS TXT record (if you are ' + \\\n                   'using DigitalOcean for DNS).'\n+    ttl = 30\n \n     def __init__(self, *args, **kwargs):\n         super(Authenticator, self).__init__(*args, **kwargs)\n@@ -45,7 +46,8 @@\n         )\n \n     def _perform(self, domain, validation_name, validation):\n-        self._get_digitalocean_client().add_txt_record(domain, validation_name, validation)\n+        self._get_digitalocean_client().add_txt_record(domain, validation_name, validation,\n+                                                       self.ttl)\n \n     def _cleanup(self, domain, validation_name, validation):\n         self._get_digitalocean_client().del_txt_record(domain, validation_name, validation)\n@@ -62,13 +64,15 @@\n     def __init__(self, token):\n         self.manager = digitalocean.Manager(token=token)\n \n-    def add_txt_record(self, domain_name, record_name, record_content):\n+    def add_txt_record(self, domain_name: str, record_name: str, record_content: str,\n+                       record_ttl: int):\n         \"\"\"\n         Add a TXT record using the supplied information.\n \n         :param str domain_name: The domain to use to associate the record with.\n         :param str record_name: The record name (typically beginning with '_acme-challenge.').\n         :param str record_content: The record content (typically the challenge validation).\n+        :param int record_ttl: The record TTL.\n         :raises certbot.errors.PluginError: if an error occurs communicating with the DigitalOcean\n                                             API\n         \"\"\"\n@@ -89,7 +93,8 @@\n             result = domain.create_new_domain_record(\n                 type='TXT',\n                 name=self._compute_record_name(domain, record_name),\n-                data=record_content)\n+                data=record_content,\n+                ttl=record_ttl) # ttl kwarg is only effective starting python-digitalocean 1.15.0\n \n             record_id = result['domain_record']['id']\n \n@@ -99,7 +104,7 @@\n             raise errors.PluginError('Error adding TXT record using the DigitalOcean API: {0}'\n                                      .format(e))\n \n-    def del_txt_record(self, domain_name, record_name, record_content):\n+    def del_txt_record(self, domain_name: str, record_name: str, record_content: str):\n         \"\"\"\n         Delete a TXT record using the supplied information.\n \ndiff --git a/certbot-dns-digitalocean/setup.py b/certbot-dns-digitalocean/setup.py\n--- a/certbot-dns-digitalocean/setup.py\n+++ b/certbot-dns-digitalocean/setup.py\n@@ -9,7 +9,7 @@\n # Remember to update local-oldest-requirements.txt when changing the minimum\n # acme/certbot version.\n install_requires = [\n-    'python-digitalocean>=1.11',\n+    'python-digitalocean>=1.11', # 1.15.0 or newer is recommended for TTL support\n     'setuptools>=39.0.1',\n     'zope.interface',\n ]\n", "issue": "Upgrade to dns-digitalocean 1.6 broke renewals\ncertbot/dns-digitalocean:v1.6.0\r\n\r\n```\r\n$ certbot renew\r\n...\r\nProcessing /etc/letsencrypt/renewal/omcr.io.conf\r\n- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -\r\nCert is due for renewal, auto-renewing...\r\nPlugins selected: Authenticator dns-digitalocean, Installer None\r\nRenewing an existing certificate\r\nPerforming the following challenges:\r\ndns-01 challenge for omcr.io\r\nWaiting 10 seconds for DNS changes to propagate\r\nWaiting for verification...\r\nChallenge failed for domain omcr.io\r\ndns-01 challenge for omcr.io\r\nCleaning up challenges\r\nAttempting to renew cert (omcr.io) from /etc/letsencrypt/renewal/omcr.io.conf produced an unexpected error: Some challenges have failed.. Skipping.\r\n...\r\n - The following errors were reported by the server:\r\n\r\n   Domain: omcr.io\r\n   Type:   unauthorized\r\n   Detail: Incorrect TXT record\r\n   \"mKIy_DFn65L85GgEMgl8BYiKYi4dSzwbG3_aJoSvwZo\" found at\r\n   _acme-challenge.omcr.io\r\n\r\n   To fix these errors, please make sure that your domain name was\r\n   entered correctly and the DNS A/AAAA record(s) for that domain\r\n   contain(s) the right IP address.\r\n```\r\n\r\n\nAllow setting DNS TTL\nFixes #7969.\r\n\r\n## Pull Request Checklist\r\n\r\n- [x] If the change being made is to a [distributed component](https://certbot.eff.org/docs/contributing.html#code-components-and-layout), edit the `master` section of `certbot/CHANGELOG.md` to include a description of the change being made.\r\n- [x] Include your name in `AUTHORS.md` if you like.\r\n\r\n# Purpose\r\nThis pull request is to allow setting a custom TTL when using DigitalOcean TXT records for authentication, as well as to lower the default to a more reasonable 60 seconds.\r\n\r\n# Rationale\r\nThe default used by the dependent `python-digitalocean` library is 1800 seconds (one-half hour) which can cause frustrations when testing out configurations with `--dry-run`.\n", "before_files": [{"content": "\"\"\"DNS Authenticator for DigitalOcean.\"\"\"\nimport logging\n\nimport digitalocean\nimport zope.interface\n\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot.plugins import dns_common\n\nlogger = logging.getLogger(__name__)\n\n\n@zope.interface.implementer(interfaces.IAuthenticator)\n@zope.interface.provider(interfaces.IPluginFactory)\nclass Authenticator(dns_common.DNSAuthenticator):\n    \"\"\"DNS Authenticator for DigitalOcean\n\n    This Authenticator uses the DigitalOcean API to fulfill a dns-01 challenge.\n    \"\"\"\n\n    description = 'Obtain certificates using a DNS TXT record (if you are ' + \\\n                  'using DigitalOcean for DNS).'\n\n    def __init__(self, *args, **kwargs):\n        super(Authenticator, self).__init__(*args, **kwargs)\n        self.credentials = None\n\n    @classmethod\n    def add_parser_arguments(cls, add):  # pylint: disable=arguments-differ\n        super(Authenticator, cls).add_parser_arguments(add)\n        add('credentials', help='DigitalOcean credentials INI file.')\n\n    def more_info(self):  # pylint: disable=missing-function-docstring\n        return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \\\n               'the DigitalOcean API.'\n\n    def _setup_credentials(self):\n        self.credentials = self._configure_credentials(\n            'credentials',\n            'DigitalOcean credentials INI file',\n            {\n                'token': 'API token for DigitalOcean account'\n            }\n        )\n\n    def _perform(self, domain, validation_name, validation):\n        self._get_digitalocean_client().add_txt_record(domain, validation_name, validation)\n\n    def _cleanup(self, domain, validation_name, validation):\n        self._get_digitalocean_client().del_txt_record(domain, validation_name, validation)\n\n    def _get_digitalocean_client(self):\n        return _DigitalOceanClient(self.credentials.conf('token'))\n\n\nclass _DigitalOceanClient:\n    \"\"\"\n    Encapsulates all communication with the DigitalOcean API.\n    \"\"\"\n\n    def __init__(self, token):\n        self.manager = digitalocean.Manager(token=token)\n\n    def add_txt_record(self, domain_name, record_name, record_content):\n        \"\"\"\n        Add a TXT record using the supplied information.\n\n        :param str domain_name: The domain to use to associate the record with.\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        :raises certbot.errors.PluginError: if an error occurs communicating with the DigitalOcean\n                                            API\n        \"\"\"\n\n        try:\n            domain = self._find_domain(domain_name)\n        except digitalocean.Error as e:\n            hint = None\n\n            if str(e).startswith(\"Unable to authenticate\"):\n                hint = 'Did you provide a valid API token?'\n\n            logger.debug('Error finding domain using the DigitalOcean API: %s', e)\n            raise errors.PluginError('Error finding domain using the DigitalOcean API: {0}{1}'\n                                     .format(e, ' ({0})'.format(hint) if hint else ''))\n\n        try:\n            result = domain.create_new_domain_record(\n                type='TXT',\n                name=self._compute_record_name(domain, record_name),\n                data=record_content)\n\n            record_id = result['domain_record']['id']\n\n            logger.debug('Successfully added TXT record with id: %d', record_id)\n        except digitalocean.Error as e:\n            logger.debug('Error adding TXT record using the DigitalOcean API: %s', e)\n            raise errors.PluginError('Error adding TXT record using the DigitalOcean API: {0}'\n                                     .format(e))\n\n    def del_txt_record(self, domain_name, record_name, record_content):\n        \"\"\"\n        Delete a TXT record using the supplied information.\n\n        Note that both the record's name and content are used to ensure that similar records\n        created concurrently (e.g., due to concurrent invocations of this plugin) are not deleted.\n\n        Failures are logged, but not raised.\n\n        :param str domain_name: The domain to use to associate the record with.\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        \"\"\"\n\n        try:\n            domain = self._find_domain(domain_name)\n        except digitalocean.Error as e:\n            logger.debug('Error finding domain using the DigitalOcean API: %s', e)\n            return\n\n        try:\n            domain_records = domain.get_records()\n\n            matching_records = [record for record in domain_records\n                                if record.type == 'TXT'\n                                and record.name == self._compute_record_name(domain, record_name)\n                                and record.data == record_content]\n        except digitalocean.Error as e:\n            logger.debug('Error getting DNS records using the DigitalOcean API: %s', e)\n            return\n\n        for record in matching_records:\n            try:\n                logger.debug('Removing TXT record with id: %s', record.id)\n                record.destroy()\n            except digitalocean.Error as e:\n                logger.warning('Error deleting TXT record %s using the DigitalOcean API: %s',\n                            record.id, e)\n\n    def _find_domain(self, domain_name):\n        \"\"\"\n        Find the domain object for a given domain name.\n\n        :param str domain_name: The domain name for which to find the corresponding Domain.\n        :returns: The Domain, if found.\n        :rtype: `~digitalocean.Domain`\n        :raises certbot.errors.PluginError: if no matching Domain is found.\n        \"\"\"\n\n        domain_name_guesses = dns_common.base_domain_name_guesses(domain_name)\n\n        domains = self.manager.get_all_domains()\n\n        for guess in domain_name_guesses:\n            matches = [domain for domain in domains if domain.name == guess]\n\n            if matches:\n                domain = matches[0]\n                logger.debug('Found base domain for %s using name %s', domain_name, guess)\n                return domain\n\n        raise errors.PluginError('Unable to determine base domain for {0} using names: {1}.'\n                                 .format(domain_name, domain_name_guesses))\n\n    @staticmethod\n    def _compute_record_name(domain, full_record_name):\n        # The domain, from DigitalOcean's point of view, is automatically appended.\n        return full_record_name.rpartition(\".\" + domain.name)[0]\n", "path": "certbot-dns-digitalocean/certbot_dns_digitalocean/_internal/dns_digitalocean.py"}, {"content": "import os\nimport sys\n\nfrom setuptools import find_packages\nfrom setuptools import setup\n\nversion = '1.14.0.dev0'\n\n# Remember to update local-oldest-requirements.txt when changing the minimum\n# acme/certbot version.\ninstall_requires = [\n    'python-digitalocean>=1.11',\n    'setuptools>=39.0.1',\n    'zope.interface',\n]\n\nif not os.environ.get('SNAP_BUILD'):\n    install_requires.extend([\n        'acme>=0.29.0',\n        'certbot>=1.1.0',\n    ])\nelif 'bdist_wheel' in sys.argv[1:]:\n    raise RuntimeError('Unset SNAP_BUILD when building wheels '\n                       'to include certbot dependencies.')\nif os.environ.get('SNAP_BUILD'):\n    install_requires.append('packaging')\n\ndocs_extras = [\n    'Sphinx>=1.0',  # autodoc_member_order = 'bysource', autodoc_default_flags\n    'sphinx_rtd_theme',\n]\n\nsetup(\n    name='certbot-dns-digitalocean',\n    version=version,\n    description=\"DigitalOcean DNS Authenticator plugin for Certbot\",\n    url='https://github.com/certbot/certbot',\n    author=\"Certbot Project\",\n    author_email='client-dev@letsencrypt.org',\n    license='Apache License 2.0',\n    python_requires='>=3.6',\n    classifiers=[\n        'Development Status :: 5 - Production/Stable',\n        'Environment :: Plugins',\n        'Intended Audience :: System Administrators',\n        'License :: OSI Approved :: Apache Software License',\n        'Operating System :: POSIX :: Linux',\n        'Programming Language :: Python',\n        'Programming Language :: Python :: 3',\n        'Programming Language :: Python :: 3.6',\n        'Programming Language :: Python :: 3.7',\n        'Programming Language :: Python :: 3.8',\n        'Programming Language :: Python :: 3.9',\n        'Topic :: Internet :: WWW/HTTP',\n        'Topic :: Security',\n        'Topic :: System :: Installation/Setup',\n        'Topic :: System :: Networking',\n        'Topic :: System :: Systems Administration',\n        'Topic :: Utilities',\n    ],\n\n    packages=find_packages(),\n    include_package_data=True,\n    install_requires=install_requires,\n    extras_require={\n        'docs': docs_extras,\n    },\n    entry_points={\n        'certbot.plugins': [\n            'dns-digitalocean = certbot_dns_digitalocean._internal.dns_digitalocean:Authenticator',\n        ],\n    },\n)\n", "path": "certbot-dns-digitalocean/setup.py"}], "after_files": [{"content": "\"\"\"DNS Authenticator for DigitalOcean.\"\"\"\nimport logging\n\nimport digitalocean\nimport zope.interface\n\nfrom certbot import errors\nfrom certbot import interfaces\nfrom certbot.plugins import dns_common\n\nlogger = logging.getLogger(__name__)\n\n\n@zope.interface.implementer(interfaces.IAuthenticator)\n@zope.interface.provider(interfaces.IPluginFactory)\nclass Authenticator(dns_common.DNSAuthenticator):\n    \"\"\"DNS Authenticator for DigitalOcean\n\n    This Authenticator uses the DigitalOcean API to fulfill a dns-01 challenge.\n    \"\"\"\n\n    description = 'Obtain certificates using a DNS TXT record (if you are ' + \\\n                  'using DigitalOcean for DNS).'\n    ttl = 30\n\n    def __init__(self, *args, **kwargs):\n        super(Authenticator, self).__init__(*args, **kwargs)\n        self.credentials = None\n\n    @classmethod\n    def add_parser_arguments(cls, add):  # pylint: disable=arguments-differ\n        super(Authenticator, cls).add_parser_arguments(add)\n        add('credentials', help='DigitalOcean credentials INI file.')\n\n    def more_info(self):  # pylint: disable=missing-function-docstring\n        return 'This plugin configures a DNS TXT record to respond to a dns-01 challenge using ' + \\\n               'the DigitalOcean API.'\n\n    def _setup_credentials(self):\n        self.credentials = self._configure_credentials(\n            'credentials',\n            'DigitalOcean credentials INI file',\n            {\n                'token': 'API token for DigitalOcean account'\n            }\n        )\n\n    def _perform(self, domain, validation_name, validation):\n        self._get_digitalocean_client().add_txt_record(domain, validation_name, validation,\n                                                       self.ttl)\n\n    def _cleanup(self, domain, validation_name, validation):\n        self._get_digitalocean_client().del_txt_record(domain, validation_name, validation)\n\n    def _get_digitalocean_client(self):\n        return _DigitalOceanClient(self.credentials.conf('token'))\n\n\nclass _DigitalOceanClient:\n    \"\"\"\n    Encapsulates all communication with the DigitalOcean API.\n    \"\"\"\n\n    def __init__(self, token):\n        self.manager = digitalocean.Manager(token=token)\n\n    def add_txt_record(self, domain_name: str, record_name: str, record_content: str,\n                       record_ttl: int):\n        \"\"\"\n        Add a TXT record using the supplied information.\n\n        :param str domain_name: The domain to use to associate the record with.\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        :param int record_ttl: The record TTL.\n        :raises certbot.errors.PluginError: if an error occurs communicating with the DigitalOcean\n                                            API\n        \"\"\"\n\n        try:\n            domain = self._find_domain(domain_name)\n        except digitalocean.Error as e:\n            hint = None\n\n            if str(e).startswith(\"Unable to authenticate\"):\n                hint = 'Did you provide a valid API token?'\n\n            logger.debug('Error finding domain using the DigitalOcean API: %s', e)\n            raise errors.PluginError('Error finding domain using the DigitalOcean API: {0}{1}'\n                                     .format(e, ' ({0})'.format(hint) if hint else ''))\n\n        try:\n            result = domain.create_new_domain_record(\n                type='TXT',\n                name=self._compute_record_name(domain, record_name),\n                data=record_content,\n                ttl=record_ttl) # ttl kwarg is only effective starting python-digitalocean 1.15.0\n\n            record_id = result['domain_record']['id']\n\n            logger.debug('Successfully added TXT record with id: %d', record_id)\n        except digitalocean.Error as e:\n            logger.debug('Error adding TXT record using the DigitalOcean API: %s', e)\n            raise errors.PluginError('Error adding TXT record using the DigitalOcean API: {0}'\n                                     .format(e))\n\n    def del_txt_record(self, domain_name: str, record_name: str, record_content: str):\n        \"\"\"\n        Delete a TXT record using the supplied information.\n\n        Note that both the record's name and content are used to ensure that similar records\n        created concurrently (e.g., due to concurrent invocations of this plugin) are not deleted.\n\n        Failures are logged, but not raised.\n\n        :param str domain_name: The domain to use to associate the record with.\n        :param str record_name: The record name (typically beginning with '_acme-challenge.').\n        :param str record_content: The record content (typically the challenge validation).\n        \"\"\"\n\n        try:\n            domain = self._find_domain(domain_name)\n        except digitalocean.Error as e:\n            logger.debug('Error finding domain using the DigitalOcean API: %s', e)\n            return\n\n        try:\n            domain_records = domain.get_records()\n\n            matching_records = [record for record in domain_records\n                                if record.type == 'TXT'\n                                and record.name == self._compute_record_name(domain, record_name)\n                                and record.data == record_content]\n        except digitalocean.Error as e:\n            logger.debug('Error getting DNS records using the DigitalOcean API: %s', e)\n            return\n\n        for record in matching_records:\n            try:\n                logger.debug('Removing TXT record with id: %s', record.id)\n                record.destroy()\n            except digitalocean.Error as e:\n                logger.warning('Error deleting TXT record %s using the DigitalOcean API: %s',\n                            record.id, e)\n\n    def _find_domain(self, domain_name):\n        \"\"\"\n        Find the domain object for a given domain name.\n\n        :param str domain_name: The domain name for which to find the corresponding Domain.\n        :returns: The Domain, if found.\n        :rtype: `~digitalocean.Domain`\n        :raises certbot.errors.PluginError: if no matching Domain is found.\n        \"\"\"\n\n        domain_name_guesses = dns_common.base_domain_name_guesses(domain_name)\n\n        domains = self.manager.get_all_domains()\n\n        for guess in domain_name_guesses:\n            matches = [domain for domain in domains if domain.name == guess]\n\n            if matches:\n                domain = matches[0]\n                logger.debug('Found base domain for %s using name %s', domain_name, guess)\n                return domain\n\n        raise errors.PluginError('Unable to determine base domain for {0} using names: {1}.'\n                                 .format(domain_name, domain_name_guesses))\n\n    @staticmethod\n    def _compute_record_name(domain, full_record_name):\n        # The domain, from DigitalOcean's point of view, is automatically appended.\n        return full_record_name.rpartition(\".\" + domain.name)[0]\n", "path": "certbot-dns-digitalocean/certbot_dns_digitalocean/_internal/dns_digitalocean.py"}, {"content": "import os\nimport sys\n\nfrom setuptools import find_packages\nfrom setuptools import setup\n\nversion = '1.14.0.dev0'\n\n# Remember to update local-oldest-requirements.txt when changing the minimum\n# acme/certbot version.\ninstall_requires = [\n    'python-digitalocean>=1.11', # 1.15.0 or newer is recommended for TTL support\n    'setuptools>=39.0.1',\n    'zope.interface',\n]\n\nif not os.environ.get('SNAP_BUILD'):\n    install_requires.extend([\n        'acme>=0.29.0',\n        'certbot>=1.1.0',\n    ])\nelif 'bdist_wheel' in sys.argv[1:]:\n    raise RuntimeError('Unset SNAP_BUILD when building wheels '\n                       'to include certbot dependencies.')\nif os.environ.get('SNAP_BUILD'):\n    install_requires.append('packaging')\n\ndocs_extras = [\n    'Sphinx>=1.0',  # autodoc_member_order = 'bysource', autodoc_default_flags\n    'sphinx_rtd_theme',\n]\n\nsetup(\n    name='certbot-dns-digitalocean',\n    version=version,\n    description=\"DigitalOcean DNS Authenticator plugin for Certbot\",\n    url='https://github.com/certbot/certbot',\n    author=\"Certbot Project\",\n    author_email='client-dev@letsencrypt.org',\n    license='Apache License 2.0',\n    python_requires='>=3.6',\n    classifiers=[\n        'Development Status :: 5 - Production/Stable',\n        'Environment :: Plugins',\n        'Intended Audience :: System Administrators',\n        'License :: OSI Approved :: Apache Software License',\n        'Operating System :: POSIX :: Linux',\n        'Programming Language :: Python',\n        'Programming Language :: Python :: 3',\n        'Programming Language :: Python :: 3.6',\n        'Programming Language :: Python :: 3.7',\n        'Programming Language :: Python :: 3.8',\n        'Programming Language :: Python :: 3.9',\n        'Topic :: Internet :: WWW/HTTP',\n        'Topic :: Security',\n        'Topic :: System :: Installation/Setup',\n        'Topic :: System :: Networking',\n        'Topic :: System :: Systems Administration',\n        'Topic :: Utilities',\n    ],\n\n    packages=find_packages(),\n    include_package_data=True,\n    install_requires=install_requires,\n    extras_require={\n        'docs': docs_extras,\n    },\n    entry_points={\n        'certbot.plugins': [\n            'dns-digitalocean = certbot_dns_digitalocean._internal.dns_digitalocean:Authenticator',\n        ],\n    },\n)\n", "path": "certbot-dns-digitalocean/setup.py"}]}