prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  TLDExtract Private Domains for Dynamic DNS Providers
  Hello all,
  
  I'm currently putting together the plugin for dynu.com (listed in NYI proiders).
  As Dynu also acts as Dynamic DNS provider with several toplevel domains as base for the dynamic domain (`yourhost.dynu.net`, `yourhost.freeddns.org`..., also, wildcards), I had some trouble putting together the plugin.
  
  As an example, I'm making up `mydomain.dynu.net` as my target dynamic dns hostname.
  
  Now, the `tldextract` package used to determine the part of the domain that belongs to the toplevel will spit out `net` as the toplevel, `dynu` as the TLD and then drops `mydomain` in further processing as seen [in client.py](../blob/master/lexicon/client.py#L43).
  
  In turn, finding the right domain from the list of dns entries in `_authenticate` is not possible by default (as `self.domain` is set to `dynu.net`).
  
  I discovered two workarounds for this:
  
  1. use `--delegated "mydomain.dynu.net"` to explicitly target the subdomain
  2. change the code [in client.py](../blob/master/lexicon/client.py#L41) to this:
  
  ```python
  extract = tldextract.TLDExtract(include_psl_private_domains=True)
  
  # Process domain, strip subdomain
  domain_parts = extract(
      self.config.resolve('lexicon:domain'))
  runtime_config['domain'] = '{0}.{1}'.format(
      domain_parts.domain, domain_parts.suffix)
  ```
  
  The latter is taken from [the tldextract README](https://github.com/john-kurkowski/tldextract#public-vs-private-domains).
  And because Dynu probably isn't the only Dynamic DNS provider using subdomains for their users, I guess this should be the default solution.
  There's a catch however that is still in ongoing development [tldextract#144](https://github.com/john-kurkowski/tldextract/pull/144):
  The list of TLDs is cached on first load of the extension, so if the config is not set to `include_psl_private_domains` before the package is first initialized, it won't work. So either an update has to be triggered manually, or, lexicon should be installed and used from a virtualenv in the first place.
  
  Since I'm already making use of method 2 in my dev enviroment, I could open a PR right away, but I'm not 100% sure on side effects for other plugins, hence my hesitation.
  
  Thanks and best,
  Chris
  
  edit// whitespace in codeblock, typos, grammar
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `lexicon/client.py`
  Content:
  ```
  1 """Main module of Lexicon. Defines the Client class, that holds all Lexicon logic."""
  2 from __future__ import absolute_import
  3 import importlib
  4 
  5 import tldextract
  6 
  7 from lexicon import discovery
  8 from lexicon.config import (
  9     ConfigResolver, DictConfigSource,
  10     legacy_config_resolver, non_interactive_config_resolver,
  11 )
  12 
  13 
  14 class ProviderNotAvailableError(Exception):
  15     """
  16     Custom exception to raise when a provider is not available,
  17     typically because some optional dependencies are missing
  18     """
  19 
  20 
  21 class Client(object):  # pylint: disable=useless-object-inheritance,too-few-public-methods
  22     """This is the Lexicon client, that will execute all the logic."""
  23 
  24     def __init__(self, config=None):
  25         if not config:
  26             # If there is not config specified, we load a non-interactive configuration.
  27             self.config = non_interactive_config_resolver()
  28         elif not isinstance(config, ConfigResolver):
  29             # If config is not a ConfigResolver, we are in a legacy situation.
  30             # We protect this part of the Client API.
  31             self.config = legacy_config_resolver(config)
  32         else:
  33             self.config = config
  34 
  35         # Validate configuration
  36         self._validate_config()
  37 
  38         runtime_config = {}
  39 
  40         # Process domain, strip subdomain
  41         domain_parts = tldextract.extract(
  42             self.config.resolve('lexicon:domain'))
  43         runtime_config['domain'] = '{0}.{1}'.format(
  44             domain_parts.domain, domain_parts.suffix)
  45 
  46         if self.config.resolve('lexicon:delegated'):
  47             # handle delegated domain
  48             delegated = self.config.resolve('lexicon:delegated').rstrip('.')
  49             if delegated != runtime_config.get('domain'):
  50                 # convert to relative name
  51                 if delegated.endswith(runtime_config.get('domain')):
  52                     delegated = delegated[:-len(runtime_config.get('domain'))]
  53                     delegated = delegated.rstrip('.')
  54                 # update domain
  55                 runtime_config['domain'] = '{0}.{1}'.format(
  56                     delegated, runtime_config.get('domain'))
  57 
  58         self.action = self.config.resolve('lexicon:action')
  59         self.provider_name = (self.config.resolve('lexicon:provider_name')
  60                               or self.config.resolve('lexicon:provider'))
  61 
  62         self.config.add_config_source(DictConfigSource(runtime_config), 0)
  63 
  64         provider_module = importlib.import_module(
  65             'lexicon.providers.' + self.provider_name)
  66         provider_class = getattr(provider_module, 'Provider')
  67         self.provider = provider_class(self.config)
  68 
  69     def execute(self):
  70         """Execute provided configuration in class constructor to the DNS records"""
  71         self.provider.authenticate()
  72         identifier = self.config.resolve('lexicon:identifier')
  73         record_type = self.config.resolve('lexicon:type')
  74         name = self.config.resolve('lexicon:name')
  75         content = self.config.resolve('lexicon:content')
  76 
  77         if self.action == 'create':
  78             return self.provider.create_record(record_type, name, content)
  79 
  80         if self.action == 'list':
  81             return self.provider.list_records(record_type, name, content)
  82 
  83         if self.action == 'update':
  84             return self.provider.update_record(identifier, record_type, name, content)
  85 
  86         if self.action == 'delete':
  87             return self.provider.delete_record(identifier, record_type, name, content)
  88 
  89         raise ValueError('Invalid action statement: {0}'.format(self.action))
  90 
  91     def _validate_config(self):
  92         provider_name = self.config.resolve('lexicon:provider_name')
  93         if not self.config.resolve('lexicon:provider_name'):
  94             raise AttributeError('provider_name')
  95 
  96         try:
  97             available = discovery.find_providers()[self.config.resolve('lexicon:provider_name')]
  98         except KeyError:
  99             raise ProviderNotAvailableError('This provider ({0}) is not supported by Lexicon.'
  100                                             .format(provider_name))
  101         else:
  102             if not available:
  103                 raise ProviderNotAvailableError(
  104                     'This provider ({0}) has required dependencies that are missing. '
  105                     'Please install lexicon[{0}] first.'.format(provider_name))
  106 
  107         if not self.config.resolve('lexicon:action'):
  108             raise AttributeError('action')
  109         if not self.config.resolve('lexicon:domain'):
  110             raise AttributeError('domain')
  111         if not self.config.resolve('lexicon:type'):
  112             raise AttributeError('type')
  113
  ```
  --- 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()
  
  ```
  
golden_diff: diff --git a/lexicon/client.py b/lexicon/client.py
  --- a/lexicon/client.py
  +++ b/lexicon/client.py
  @@ -1,6 +1,7 @@
   """Main module of Lexicon. Defines the Client class, that holds all Lexicon logic."""
   from __future__ import absolute_import
   import importlib
  +import os
   
   import tldextract
   
  @@ -10,6 +11,9 @@
       legacy_config_resolver, non_interactive_config_resolver,
   )
   
  +TLDEXTRACT_CACHE_FILE_DEFAULT = os.path.join('~', '.lexicon_tld_set')
  +TLDEXTRACT_CACHE_FILE = os.path.expanduser(os.environ.get("LEXICON_TLDEXTRACT_CACHE",
  +                                                          TLDEXTRACT_CACHE_FILE_DEFAULT))
   
   class ProviderNotAvailableError(Exception):
       """
  @@ -38,7 +42,9 @@
           runtime_config = {}
   
           # Process domain, strip subdomain
  -        domain_parts = tldextract.extract(
  +        domain_extractor = tldextract.TLDExtract(cache_file=TLDEXTRACT_CACHE_FILE,
  +                                                 include_psl_private_domains=True)
  +        domain_parts = domain_extractor(
               self.config.resolve('lexicon:domain'))
           runtime_config['domain'] = '{0}.{1}'.format(
               domain_parts.domain, domain_parts.suffix)
  
verification_info: {"golden_diff": "diff --git a/lexicon/client.py b/lexicon/client.py\n--- a/lexicon/client.py\n+++ b/lexicon/client.py\n@@ -1,6 +1,7 @@\n \"\"\"Main module of Lexicon. Defines the Client class, that holds all Lexicon logic.\"\"\"\n from __future__ import absolute_import\n import importlib\n+import os\n \n import tldextract\n \n@@ -10,6 +11,9 @@\n     legacy_config_resolver, non_interactive_config_resolver,\n )\n \n+TLDEXTRACT_CACHE_FILE_DEFAULT = os.path.join('~', '.lexicon_tld_set')\n+TLDEXTRACT_CACHE_FILE = os.path.expanduser(os.environ.get(\"LEXICON_TLDEXTRACT_CACHE\",\n+                                                          TLDEXTRACT_CACHE_FILE_DEFAULT))\n \n class ProviderNotAvailableError(Exception):\n     \"\"\"\n@@ -38,7 +42,9 @@\n         runtime_config = {}\n \n         # Process domain, strip subdomain\n-        domain_parts = tldextract.extract(\n+        domain_extractor = tldextract.TLDExtract(cache_file=TLDEXTRACT_CACHE_FILE,\n+                                                 include_psl_private_domains=True)\n+        domain_parts = domain_extractor(\n             self.config.resolve('lexicon:domain'))\n         runtime_config['domain'] = '{0}.{1}'.format(\n             domain_parts.domain, domain_parts.suffix)\n", "issue": "TLDExtract Private Domains for Dynamic DNS Providers\nHello all,\r\n\r\nI'm currently putting together the plugin for dynu.com (listed in NYI proiders).\r\nAs Dynu also acts as Dynamic DNS provider with several toplevel domains as base for the dynamic domain (`yourhost.dynu.net`, `yourhost.freeddns.org`..., also, wildcards), I had some trouble putting together the plugin.\r\n\r\nAs an example, I'm making up `mydomain.dynu.net` as my target dynamic dns hostname.\r\n\r\nNow, the `tldextract` package used to determine the part of the domain that belongs to the toplevel will spit out `net` as the toplevel, `dynu` as the TLD and then drops `mydomain` in further processing as seen [in client.py](../blob/master/lexicon/client.py#L43).\r\n\r\nIn turn, finding the right domain from the list of dns entries in `_authenticate` is not possible by default (as `self.domain` is set to `dynu.net`).\r\n\r\nI discovered two workarounds for this:\r\n\r\n1. use `--delegated \"mydomain.dynu.net\"` to explicitly target the subdomain\r\n2. change the code [in client.py](../blob/master/lexicon/client.py#L41) to this:\r\n\r\n```python\r\nextract = tldextract.TLDExtract(include_psl_private_domains=True)\r\n\r\n# Process domain, strip subdomain\r\ndomain_parts = extract(\r\n    self.config.resolve('lexicon:domain'))\r\nruntime_config['domain'] = '{0}.{1}'.format(\r\n    domain_parts.domain, domain_parts.suffix)\r\n```\r\n\r\nThe latter is taken from [the tldextract README](https://github.com/john-kurkowski/tldextract#public-vs-private-domains).\r\nAnd because Dynu probably isn't the only Dynamic DNS provider using subdomains for their users, I guess this should be the default solution.\r\nThere's a catch however that is still in ongoing development [tldextract#144](https://github.com/john-kurkowski/tldextract/pull/144):\r\nThe list of TLDs is cached on first load of the extension, so if the config is not set to `include_psl_private_domains` before the package is first initialized, it won't work. So either an update has to be triggered manually, or, lexicon should be installed and used from a virtualenv in the first place.\r\n\r\nSince I'm already making use of method 2 in my dev enviroment, I could open a PR right away, but I'm not 100% sure on side effects for other plugins, hence my hesitation.\r\n\r\nThanks and best,\r\nChris\r\n\r\nedit// whitespace in codeblock, typos, grammar\n", "before_files": [{"content": "\"\"\"Main module of Lexicon. Defines the Client class, that holds all Lexicon logic.\"\"\"\nfrom __future__ import absolute_import\nimport importlib\n\nimport tldextract\n\nfrom lexicon import discovery\nfrom lexicon.config import (\n    ConfigResolver, DictConfigSource,\n    legacy_config_resolver, non_interactive_config_resolver,\n)\n\n\nclass ProviderNotAvailableError(Exception):\n    \"\"\"\n    Custom exception to raise when a provider is not available,\n    typically because some optional dependencies are missing\n    \"\"\"\n\n\nclass Client(object):  # pylint: disable=useless-object-inheritance,too-few-public-methods\n    \"\"\"This is the Lexicon client, that will execute all the logic.\"\"\"\n\n    def __init__(self, config=None):\n        if not config:\n            # If there is not config specified, we load a non-interactive configuration.\n            self.config = non_interactive_config_resolver()\n        elif not isinstance(config, ConfigResolver):\n            # If config is not a ConfigResolver, we are in a legacy situation.\n            # We protect this part of the Client API.\n            self.config = legacy_config_resolver(config)\n        else:\n            self.config = config\n\n        # Validate configuration\n        self._validate_config()\n\n        runtime_config = {}\n\n        # Process domain, strip subdomain\n        domain_parts = tldextract.extract(\n            self.config.resolve('lexicon:domain'))\n        runtime_config['domain'] = '{0}.{1}'.format(\n            domain_parts.domain, domain_parts.suffix)\n\n        if self.config.resolve('lexicon:delegated'):\n            # handle delegated domain\n            delegated = self.config.resolve('lexicon:delegated').rstrip('.')\n            if delegated != runtime_config.get('domain'):\n                # convert to relative name\n                if delegated.endswith(runtime_config.get('domain')):\n                    delegated = delegated[:-len(runtime_config.get('domain'))]\n                    delegated = delegated.rstrip('.')\n                # update domain\n                runtime_config['domain'] = '{0}.{1}'.format(\n                    delegated, runtime_config.get('domain'))\n\n        self.action = self.config.resolve('lexicon:action')\n        self.provider_name = (self.config.resolve('lexicon:provider_name')\n                              or self.config.resolve('lexicon:provider'))\n\n        self.config.add_config_source(DictConfigSource(runtime_config), 0)\n\n        provider_module = importlib.import_module(\n            'lexicon.providers.' + self.provider_name)\n        provider_class = getattr(provider_module, 'Provider')\n        self.provider = provider_class(self.config)\n\n    def execute(self):\n        \"\"\"Execute provided configuration in class constructor to the DNS records\"\"\"\n        self.provider.authenticate()\n        identifier = self.config.resolve('lexicon:identifier')\n        record_type = self.config.resolve('lexicon:type')\n        name = self.config.resolve('lexicon:name')\n        content = self.config.resolve('lexicon:content')\n\n        if self.action == 'create':\n            return self.provider.create_record(record_type, name, content)\n\n        if self.action == 'list':\n            return self.provider.list_records(record_type, name, content)\n\n        if self.action == 'update':\n            return self.provider.update_record(identifier, record_type, name, content)\n\n        if self.action == 'delete':\n            return self.provider.delete_record(identifier, record_type, name, content)\n\n        raise ValueError('Invalid action statement: {0}'.format(self.action))\n\n    def _validate_config(self):\n        provider_name = self.config.resolve('lexicon:provider_name')\n        if not self.config.resolve('lexicon:provider_name'):\n            raise AttributeError('provider_name')\n\n        try:\n            available = discovery.find_providers()[self.config.resolve('lexicon:provider_name')]\n        except KeyError:\n            raise ProviderNotAvailableError('This provider ({0}) is not supported by Lexicon.'\n                                            .format(provider_name))\n        else:\n            if not available:\n                raise ProviderNotAvailableError(\n                    'This provider ({0}) has required dependencies that are missing. '\n                    'Please install lexicon[{0}] first.'.format(provider_name))\n\n        if not self.config.resolve('lexicon:action'):\n            raise AttributeError('action')\n        if not self.config.resolve('lexicon:domain'):\n            raise AttributeError('domain')\n        if not self.config.resolve('lexicon:type'):\n            raise AttributeError('type')\n", "path": "lexicon/client.py"}], "after_files": [{"content": "\"\"\"Main module of Lexicon. Defines the Client class, that holds all Lexicon logic.\"\"\"\nfrom __future__ import absolute_import\nimport importlib\nimport os\n\nimport tldextract\n\nfrom lexicon import discovery\nfrom lexicon.config import (\n    ConfigResolver, DictConfigSource,\n    legacy_config_resolver, non_interactive_config_resolver,\n)\n\nTLDEXTRACT_CACHE_FILE_DEFAULT = os.path.join('~', '.lexicon_tld_set')\nTLDEXTRACT_CACHE_FILE = os.path.expanduser(os.environ.get(\"LEXICON_TLDEXTRACT_CACHE\",\n                                                          TLDEXTRACT_CACHE_FILE_DEFAULT))\n\nclass ProviderNotAvailableError(Exception):\n    \"\"\"\n    Custom exception to raise when a provider is not available,\n    typically because some optional dependencies are missing\n    \"\"\"\n\n\nclass Client(object):  # pylint: disable=useless-object-inheritance,too-few-public-methods\n    \"\"\"This is the Lexicon client, that will execute all the logic.\"\"\"\n\n    def __init__(self, config=None):\n        if not config:\n            # If there is not config specified, we load a non-interactive configuration.\n            self.config = non_interactive_config_resolver()\n        elif not isinstance(config, ConfigResolver):\n            # If config is not a ConfigResolver, we are in a legacy situation.\n            # We protect this part of the Client API.\n            self.config = legacy_config_resolver(config)\n        else:\n            self.config = config\n\n        # Validate configuration\n        self._validate_config()\n\n        runtime_config = {}\n\n        # Process domain, strip subdomain\n        domain_extractor = tldextract.TLDExtract(cache_file=TLDEXTRACT_CACHE_FILE,\n                                                 include_psl_private_domains=True)\n        domain_parts = domain_extractor(\n            self.config.resolve('lexicon:domain'))\n        runtime_config['domain'] = '{0}.{1}'.format(\n            domain_parts.domain, domain_parts.suffix)\n\n        if self.config.resolve('lexicon:delegated'):\n            # handle delegated domain\n            delegated = self.config.resolve('lexicon:delegated').rstrip('.')\n            if delegated != runtime_config.get('domain'):\n                # convert to relative name\n                if delegated.endswith(runtime_config.get('domain')):\n                    delegated = delegated[:-len(runtime_config.get('domain'))]\n                    delegated = delegated.rstrip('.')\n                # update domain\n                runtime_config['domain'] = '{0}.{1}'.format(\n                    delegated, runtime_config.get('domain'))\n\n        self.action = self.config.resolve('lexicon:action')\n        self.provider_name = (self.config.resolve('lexicon:provider_name')\n                              or self.config.resolve('lexicon:provider'))\n\n        self.config.add_config_source(DictConfigSource(runtime_config), 0)\n\n        provider_module = importlib.import_module(\n            'lexicon.providers.' + self.provider_name)\n        provider_class = getattr(provider_module, 'Provider')\n        self.provider = provider_class(self.config)\n\n    def execute(self):\n        \"\"\"Execute provided configuration in class constructor to the DNS records\"\"\"\n        self.provider.authenticate()\n        identifier = self.config.resolve('lexicon:identifier')\n        record_type = self.config.resolve('lexicon:type')\n        name = self.config.resolve('lexicon:name')\n        content = self.config.resolve('lexicon:content')\n\n        if self.action == 'create':\n            return self.provider.create_record(record_type, name, content)\n\n        if self.action == 'list':\n            return self.provider.list_records(record_type, name, content)\n\n        if self.action == 'update':\n            return self.provider.update_record(identifier, record_type, name, content)\n\n        if self.action == 'delete':\n            return self.provider.delete_record(identifier, record_type, name, content)\n\n        raise ValueError('Invalid action statement: {0}'.format(self.action))\n\n    def _validate_config(self):\n        provider_name = self.config.resolve('lexicon:provider_name')\n        if not self.config.resolve('lexicon:provider_name'):\n            raise AttributeError('provider_name')\n\n        try:\n            available = discovery.find_providers()[self.config.resolve('lexicon:provider_name')]\n        except KeyError:\n            raise ProviderNotAvailableError('This provider ({0}) is not supported by Lexicon.'\n                                            .format(provider_name))\n        else:\n            if not available:\n                raise ProviderNotAvailableError(\n                    'This provider ({0}) has required dependencies that are missing. '\n                    'Please install lexicon[{0}] first.'.format(provider_name))\n\n        if not self.config.resolve('lexicon:action'):\n            raise AttributeError('action')\n        if not self.config.resolve('lexicon:domain'):\n            raise AttributeError('domain')\n        if not self.config.resolve('lexicon:type'):\n            raise AttributeError('type')\n", "path": "lexicon/client.py"}]}