For Developers

If you are a developer, you may be interested in extending Ruddr with your own notifier and/or updater modules. Or, you may want to integrate Ruddr’s functionality into a larger program. Or, you may just be looking for info about contributing to the project. In any case, this page is for you.

Note

If you have questions or comments on developing updaters, notifiers, or any other Ruddr development, feel free to start a discussion on GitHub. I especially welcome feedback about this developer documentation.

Writing Your Own Notifier

A notifier in Ruddr is a class that monitors a source of IP address information and calls a notify function from time to time with the current address. The goal, of course, is to tell Ruddr about a new IP address shortly after it changes.

Most notifiers will fall into one of a few categories:

Event-based

Monitor some external source of events providing new IP addresses.

Polling

Poll the current IP address periodically through some means and notify each time. For example, this is what the iface and web notifiers do.

Event-triggered lookup

Monitor some external source of events to know when the IP address changes, and then check what the current IP address is through some other means. For example, the systemd notifier does this: systemd-networkd sends a DBus event when there is a network status change, and when the notifier receives it, it checks the current address assigned to the network interface.

Note

There is no harm in notifying extra times, i.e. when the address hasn’t changed. In fact, polling-style notifiers rely on it. Ruddr keeps track of the address most recently sent to each provider and doesn’t send duplicates.

Ruddr provides a convenient base class, Notifier, which can be used to implement all three styles of notifier. It boils down to a few core methods:

setup()

This abstract method is called when it’s time to start the notifier. If the notifier needs to subscribe to any event sources, open any connections, start any background threads, etc., this is the place to do it.

teardown()

The opposite of setup(): This abstract method is called when it’s time to stop the notifier. It should clean up any connections, resources, threads, etc. that are no longer needed.

check_once()

For notifiers that support checking the current IP address(es) on demand, this abstract method should do so. It’s called after setup(), when Ruddr receives SIGUSR1, and can be set to run periodically with set_check_intervals().

set_check_intervals()

This provides a way to set check_once() to run automatically on an interval. It also allows you to set the retry delay for when it fails (whether it’s scheduled to run automatically or not). Call it in the constructor of your notifier, if necessary.

Any new notifier should begin by inheriting from Notifier, and then its constructor needs to be written. The constructor’s main job here is to 1) call the superclass constructor and 2) read any required parameters from the configuration. The constructor signature must match this:

Notifier.__init__(name, config)
Parameters:
  • name (str) – Notifier name, taken from [notifier.<name>] in the Ruddr config

  • config (Dict[str, str]) – A dict of this notifier’s configuration values, all as strings, plus any global configuration options that may be useful (currently, only datadir, which notifiers may use to cache data if necessary).

If there are any errors in the configuration, catch them in the constructor and raise ConfigError. The constructor is also the place to call set_check_intervals(), if necessary (but more on that in the following sections). Note that the constructor is not the place to do any setup that should happen as part of notifier startup—that should happen in setup().

After calling the superclass constructor, two member variables will be available for your convenience: self.name, containing the name of the notifier, and self.log, a Python logger named after the notifier. You are encouraged to use self.log often.

The rest of the implementation varies depending on the style of notifier. The next three sections, one for each style, discuss that in more detail. Following those is an API reference for the Notifier class.

Note

Handling IPv4 vs. IPv6 Addressing

Different networks will have different requirements for IPv4 vs. IPv6 addressing: Some may require one or both, some may not, some may want to ignore one or both. Notifiers must handle this properly, and the Notifier class has methods to help.

  • If your notifier has config that’s required only for IPv4 or only for IPv6, be sure to implement the ipv4_ready() and ipv6_ready() functions.

  • Ruddr may not need both IPv4 and IPv6 addresses from your notifier. It should call want_ipv4() and want_ipv6(), and if either returns False, there is no need to notify for that type of address at all.

  • Even if an address type is wanted, it may or may not be an error if your notifier can’t obtain it. If need_ipv4() returns True but check_once() cannot currently obtain an IPv4 address, your notifier should raise NotifyError (after notifying for the other address type, if necessary). The same goes for need_ipv6() and IPv6 addressing. (This bullet point does not apply if check_once() is not implemented.)

Event-Based Notifiers

This style of notifier receives events from some external source with the current IP address. Since it gets IP addresses from these events, it can’t check the IP address on-demand, so this style of notifier will leave check_once() unimplemented and doesn’t need to call set_check_intervals().

It should, however, implement setup() and teardown(). Typically, setup() would involve setting up a thread to listen on a socket, or setting up a callback for some event, or something along those lines. Then, teardown() would do the opposite.

Be sure to follow the guidelines in the note about IPv4 and IPv6 addressing above. Then, whenever an IPv4 address or IPv6 prefix is received, call notify_ipv4() or notify_ipv6().

Be careful not to leave your notifier in an invalid state if teardown() happens at an inconvenient time. It’s guaranteed not to be called before setup() completes, but apart from that, it is up to you to ensure that anything happening in a background thread isn’t interrupted in a way that breaks it.

Polling Notifiers

This style of notifier is fairly simple. It periodically checks the current IP address and notifies each time.

Start by calling set_check_intervals() in the constructor. That will usually look something like this (but customize the values according to your needs):

self.set_check_intervals(retry_min_interval=60,
                         retry_max_interval=86400,
                         success_interval=10800,
                         config=config)

The first three parameters set default values, and the last one provides the config dict, which will override those defaults with any entries that match (see the API documentation for set_check_interval()).

The important part there is that success_interval is set to a default other than zero. That causes the notifier to automatically call check_once() periodically, waiting that many seconds between calls (assuming there were no errors).

The retry_min_interval and retry_max_interval parameters control what happens if there is an error in check_once() (more specifically, if it raises NotifyError). Such an error triggers the retry logic, which uses an exponential backoff. The first retry is after retry_min_interval seconds. If it fails again, each successive retry interval is twice as long, maxing out at retry_max_interval. Once the retry interval reaches the max, it remains constant until the call succeeds. As soon as a retry succeeds, the notifier returns to calling check_once() every success_interval seconds.

With set_check_intervals() out of the way, it’s time to implement check_once(). That’s where the core functionality of a polling notifier happens. Taking care to follow the guidelines in the note about IPv4 and IPv6 addressing above, implement the logic to fetch the current IP address(es) and call notify_ipv4() and/or notify_ipv6().

For many polling notifiers, that will be the entire implementation. Since setting success_interval causes the checks to happen automatically, there’s not usually a need to implement setup() or teardown(). Nonetheless, they can be implemented if necessary.

For an example of this style of notifier, look at the sources for ruddr.notifiers.web.WebNotifier or ruddr.notifiers.iface.IFaceNotifier.

Event-Triggered Lookup Notifiers

This style of notifier is sort of a hybrid between the other two. It receives events when the current IP address may have changed, but still has to look up the IP address itself to find out what it is.

The strategy here is to implement the IP address lookup functionality in check_once() as with Polling Notifiers, but set success_interval to zero. That way, the retry logic is still there, and on-demand notifying when Ruddr receives SIGUSR1 will work, but otherwise, check_once() will only run when the notifier triggers it by calling check().

If you already read through the instructions for Polling Notifiers, the first part here is going to look pretty familiar.

Start by calling set_check_intervals() in the constructor. That will generally look something like this:

self.set_check_intervals(retry_min_interval=60,
                         retry_max_interval=86400,
                         success_interval=0,
                         config=config)

The important part there is that success_interval is set to zero. That’s what stops the notifier from automatically calling check_once() except in the retry logic. (That being said, there is no reason a notifier can’t have automatic polling and trigger extra checks itself, if that would be useful. In fact, that’s what the systemd notifier does.)

The retry_min_interval and retry_max_interval parameters control what happens if there is an error in check_once() (more specifically, if it raises NotifyError). Such an error triggers the retry logic, which uses an exponential backoff. The first retry is after retry_min_interval seconds. If it fails again, each successive retry interval is twice as long, maxing out at retry_max_interval. Once the retry interval reaches the max, it remains constant until the call succeeds.

The retry parameters directly passed in to the function act as defaults. If the config dict contains keys matching the names retry_min_interval or retry_max_interval, those take precedence.

Next, implement check_once(). As mentioned, this is where the logic to look up the current IP address should go. It should call notify_ipv4() and/or notify_ipv6() with the addresses it obtains. Make sure to follow the guidelines in the note about IPv4 and IPv6 addressing.

That takes care of the IP address lookup part. Next is the events for changed IP addresses. For that part, you will need to implement setup() and teardown(). As with the Event-Based Notifiers style above, setup() would typically involve setting up a thread to listen on a socket, setting up a callback for some event, or something along those lines, and teardown() would do the opposite.

Finally, whenever your notifier becomes aware that the IP address may have changed, call check(). That will call check_once(), but will properly handle the retries for you.

For an example of this style of notifier, look at the sources for ruddr.notifiers.systemd.SystemdNotifier. (One caveat: That notifier also uses polling, but setting success_interval=0 in the call to set_check_intervals() would disable that.)

Notifier Base Class

class ruddr.Notifier(name, config)

A base class for notifiers. Supports a variety of notifier strategies, such as polling, event-based notifying, and hybrids. See the docs on writing your own notifier for more detail on what that means.

Also handles setting up a logger, setting some useful member variables for subclasses, and attaching to update functions.

Parameters:
  • name (str) – Name of the notifier

  • config (Dict[str, str]) – Dict of config options for this notifier

Raises:

ConfigError – if the configuration is invalid

set_check_intervals(retry_min_interval=300, retry_max_interval=86400, success_interval=0, config=None)

Set the retry intervals for the check_once() function, and optionally set check_once() to run periodically when successful.

When check_once() fails (by raising NotifyError), the next invocation will be scheduled using an exponential backoff strategy, starting with retry_min_interval seconds. Subsequent consecutive failures will be scheduled using successively longer intervals, until reaching the maximum failure interval, retry_max_interval seconds.

The success_interval parameter triggers some additional behavior:

  • If success_interval is greater than zero, then when check_once() succeeds, another invocation will be scheduled for success_interval seconds later. This is useful for notifiers that check the current address by polling, like the web notifier. Since check() runs at notifier startup, that means setup() and teardown() may not have to be implemented at all for these polling-style notifiers.

  • If success_interval is zero, check_once() will only be scheduled for future invocation for retries.

If the config parameter is provided, it will be checked for keys retry_min_interval, retry_max_interval, and interval. If either of the retry_* keys are found, their values override the parameters passed into this function. If the interval key is found, its value overrides the success_interval parameter if and only if the parameter was already nonzero (preventing a configuration mistake from changing a non-polling notifier into a polling notifier).

In practice, that means this function should be called using the notifier’s default values for retry_min_interval, retry_max_interval, and success_interval (if it needs defaults other than the method defaults) and passing in the config to allow the user to override them.

Subclasses should call this before returning from their constructor (if it needs to be called at all).

This has no effect if check_once() raises NotImplementedError.

Parameters:
  • retry_min_interval (int) – Minimum retry interval

  • retry_max_interval (int) – Maximum retry interval

  • success_interval (int) – Normal polling interval, or 0

  • config (Dict[str, str] | None) – Config dict for this updater

Raises:

ConfigError – if the retry intervals are less than 1, the success interval is less than 0, or the values in the config cannot be converted to int

abstract setup()

Do any setup and start ongoing IP address notifications. Setup should be complete before this function returns (e.g. opening socket connections, etc.) but ongoing notifications should continue in the background, e.g. on a separate thread.

Should be overridden by subclasses if required.

Raises:

NotifierSetupError – when there is a nonrecoverable error preventing notifier startup.

abstract teardown()

Halt ongoing IP address notifications, do any teardown, and stop any non-daemon threads so Python may exit.

Should be overridden by subclasses if required.

When this is called, there will be no pending invocations of check_once(), and it’s guaranteed that setup() is complete. Apart from that, it is up to the implementation to ensure that inconvenient timing won’t break any operations happening in background threads (e.g. that were started by setup()).

This must not raise any exceptions (other than NotImplementedError if not implemented).

abstract check_once()

Check the current IP address and do a notify, if possible.

Should be overridden by subclasses if supported.

Some notifiers do not support notifying on demand (for example, they get the current address from an event, thus they can only notify when such an event happens). For those updaters, this method should raise NotImplementedError when called (which is the default behavior when not overridden).

For any notifier that does support obtaining the current IP address on demand, this function should do that immediately and notify using notify_ipv4() and notify_ipv6(). Some additional guidelines:

  • Ruddr may not need both IPv4 and IPv6 addresses from this notifier. Call want_ipv4() and want_ipv6() to determine if either should be skipped.

  • Even if Ruddr wants an address type, it may or may not be an error if it cannot be provided. If need_ipv4() returns True and an IPv4 address cannot be obtained, raise NotifyError (after notifying for the other address type, if necessary). The same goes for need_ipv6() and availability of an IPv6 prefix.

This is called at notifier startup, for on-demand notifies, and any time the notifier calls check() itself.

Raises:
  • NotifyError – if checking the current IP address failed (will trigger a retry after a delay)

  • NotImplementedError – if not supported by this notifier

notify_ipv4(address)

Subclasses must call this to notify all the attached IPv4 updaters of a (possibly) new IPv4 address.

Subclasses may, but need not, call this if want_ipv4() is false.

Parameters:

address (IPv4Address) – The (possibly) new IPv4 address

notify_ipv6(prefix)

Subclasses must call this to notify all the attached IPv6 updaters of a (possibly) new IPv6 prefix.

Subclasses may, but need not, call this if want_ipv6() is false.

Parameters:

prefix (IPv6Network) – The (possibly) new IPv6 network prefix

want_ipv4()

Subclasses should call this to determine whether to check for current IPv4 addresses at all.

Returns:

True if so, False if not

Return type:

bool

want_ipv6()

Subclasses should call this to determine whether to check for current IPv6 addresses at all.

Returns:

True if so, False if not

Return type:

bool

need_ipv4()

Subclasses must call this to determine if a lack of IPv4 addressing is an error.

Returns:

True if so, False if not

Return type:

bool

need_ipv6()

Subclasses must call this to determine if a lack of IPv6 addressing is an error.

Returns:

True if so, False if not

Return type:

bool

abstract ipv4_ready()

Check if all configuration required for IPv4 notifying is present.

Subclasses must override if there is any configuration only required for IPv4.

Returns:

True if so, False if not

Return type:

bool

abstract ipv6_ready()

Check if all configuration required for IPv6 notifying is present.

Subclasses must override if there is any configuration only required for IPv6.

Returns:

True if so, False if not

Return type:

bool

Writing Your Own Updater

An updater in Ruddr is, at its core, a class that provides two methods: one to update the IPv4 address and one to update the IPv6 address(es). That being said, those methods are actually responsible for quite a bit, such as detecting duplicate notifies, working with the addrfile, and retrying failed updates. Ruddr provides a few base classes that handle all those responsibilities and lay the groundwork for several common types of dynamic DNS provider APIs. Each of them provides certain abstract methods appropriate to the specific style of API they support.

To create an updater, create a class that inherits from one of those base classes, listed below, and implement its abstract methods as described under High-Level Updater Base Classes.

OneWayUpdater

A base class for providers with “one way” protocols. That is, protocols that allow domain updates but have no way to check the current address(es) assigned to domains. It obtains the current address using DNS lookups instead.

TwoWayUpdater

A base class for providers with “two way” protocols. That is, protocols that allow updating the address(es) at a domain name as well as querying the current address at a domain name. It’s best suited for providers whose API has no concept of zones (e.g. there’s no API calls related to zones, nor does any operation require a zone as a parameter).

TwoWayZoneUpdater

This is like TwoWayUpdater, except it’s well-suited for providers whose APIs do care about zones. For example, this is the base class to use if there is a way to query all records for a zone, or if domain updates require specifying the zone for the update.

Those three base classes should cover the vast majority of use cases. However, if you need even more flexibility, you can inherit directly from the low-level Updater base class instead, or the most primitive, the BaseUpdater base class. These are described under Low-Level Updater Base Classes.

A few additional guidelines and tips:

  • All updaters must have a constructor that matches the following:

    Updater.__init__(name, addrfile, config)
    Parameters:
    • name (str) – Updater name, taken from [updater.<name>] in the Ruddr config

    • addrfile (ruddr.Addrfile) – The Addrfile object this updater should use

    • config (Dict[str, str]) – A dict of this updater’s configuration values, all as strings, plus any global configuration options that may be useful (currently, only datadir, which updaters may use to cache data if necessary).

  • The first two parameters (name and addrfile) can be passed directly to the super class constructor, and it’s strongly recommended that that be the first thing your constructor does (so the variables in the next bullet point will be initialized).

  • The BaseUpdater class, which is a superclass of all updaters (directly or indirectly), makes the self.name and self.log member variables available. self.name is a str with the updater name and self.log is a Python logger named after the updater. You may use either of these variables whenever convenient, but you are especially encouraged to use self.log often.

High-Level Updater Base Classes

class ruddr.OneWayUpdater(name, addrfile)

Base class for updaters supporting protocols that are one-way, that is, the API has no way to obtain the current address for a host. Ruddr requires the current address for IPv6 updates since it only updates the prefix. This class handles that requirement either by using hardcoded IPv6 addresses or by looking up the current IPv6 address in DNS.

The update process for this type of updater works as follows:

  1. The list of hosts to be updated is fetched from config.

    • For an IPv4 update, publish_ipv4_one_host() is called for each host, and the process is done.

    • For an IPv6 update, the process continues with the next steps.

  2. For IPv6, Ruddr obtains a “current” address for each host (“current” in quotes because only the host portion of the address actually matters, since that’s the part it needs to reuse).

    • If an address for the host is hardcoded in config, it uses that.

    • If an FQDN was provided for the host, it looks up that domain name in DNS, optionally at a specific nameserver.

    • Otherwise, if neither was given, it skips this host for IPv6 updates.

  3. Ruddr takes the host portion from each address in step 2 and combines it with the new prefix from the notifier to get the new address, then calls publish_ipv6_one_host() on each one.

Parameters:
  • name – Name of the updater (from config section heading)

  • addrfile – The Addrfile object

init_params(hosts, nameserver=None, min_retry=300)

Initialize the hosts list, nameserver, and min retry interval.

This is separate from __init__() so subclasses can rely on the logger while doing their config parsing, then pass the relevant config options in by calling this method. It must be called before your subclass’s constructor completes.

Parameters:
  • hosts (List[Tuple[str, IPv6Address | str | None]] | str) – A list of 2-tuples (hostname, ipv6_src) specifying which hosts should be updated and where their IPv6 addresses should come from. ipv6_src can be an IPv6Address to hardcode the host portion of the address, a str containing an FQDN to look up in DNS, or None if this host should not get IPv6 updates at all. Alternatively, this entire parameter may be in unparsed string form—see the docs for the standard updater for the expected format.

  • nameserver (str | None) – The nameserver to use to look up AAAA records for the FQDNs, if any. If None, system DNS is used.

  • min_retry – The minimum retry interval after failed updates, in seconds. (There is an exponential backoff for subsequent retries.)

abstract publish_ipv4_one_host(hostname, address)

Attempt to publish an IPv4 address for the given host.

Must be implemented by subclasses.

Parameters:
  • hostname (str) – The host to publish for

  • address (IPv4Address) – The address to publish

Raises:
  • PublishError – if publishing fails (will automatically retry after a delay)

  • FatalPublishError – if publishing fails in a non-recoverable way (all future publishing will halt)

abstract publish_ipv6_one_host(hostname, address)

Attempt to publish an IPv6 address for the given host.

Must be implemented by subclasses.

Parameters:
  • hostname (str) – The host to publish for

  • address (IPv6Address) – The address to publish

Raises:
  • PublishError – if publishing fails (will automatically retry after a delay)

  • FatalPublishError – if publishing fails in a non-recoverable way (all future publishing will halt)

class ruddr.TwoWayUpdater(name, addrfile, datadir)

Base class for updaters supporting protocols that are two-way and not zone-based, that is:

  • The API supports fetching the current address(es) for hosts, either individually or by fetching all domains in the account

  • The API has no concept of zones, meaning there are not zone-related API calls, nor is the zone required as a parameter for any other operation

It’s meant to be flexible enough for a variety of API styles. For example, most APIs will allow fetching and updating individual domains, but some may only provide a way to fetch or update all domains in the account at once. Still others may be a hybrid, requiring you to fetch all domains in the account but update domains individually. This class supports all of the above by allowing only the appropriate methods to be implemented.

The update process for this type of updater works as follows:

  1. Fetch A/AAAA records

  2. Create replacement records for the hosts to be updated. If there is not an existing record for a host, it’s a PublishError. If there are multiple existing records, they are replaced with a single one for IPv4, and they are all updated for IPv6. Other records (if there are any in the account) are left untouched.

  3. Write the A/AAAA records

Parameters:
  • name (str) – Name of the updater (from config section heading)

  • addrfile (Addrfile) – The Addrfile object

  • datadir (str) – The configured data directory

init_hosts(hosts)

Provide the list of hosts to be updated.

This is separate from __init__() so subclasses can rely on the logger while doing their config parsing, then pass the list of hosts in via this method after. It must be called before your subclass’s constructor completes.

The list can be provided either as an unparsed str with a whitespace-separated list of domain names or as an actual list of domain names.

Parameters:

hosts (List[str] | str) – The list of hosts to be updated

Raises:

ConfigError – if there is a duplicate

abstract fetch_all_ipv4s()

Get a list of all A (IPv4) records in the account.

Implementing this method in subclasses is optional. If not implemented, then fetch_domain_ipv4s() and put_domain_ipv4() must be implemented.

If implemented, this function should return a list of A (IPv4) records in the form (domain, addr, ttl) where domain is the domain name for the record, addr is an IPv4Address, and ttl is the TTL of the record.

The ttl may be set to None if the API does not provide it. It is only required for providers that would change the TTL back to default if it’s not explicitly included when Ruddr later updates the record.

If there are multiple records/IPv4s for a single domain, return them as separate list items with the same domain. Note that if the domain needs to be updated by Ruddr, it will only produce a single record to replace them.

Returns:

A list of A records in the format described

Raises:
  • NotImplementedError – if not implemented

  • PublishError – if implemented, but there is a failure, or the zone does not exist

Return type:

List[Tuple[str, IPv4Address, int | None]]

abstract fetch_all_ipv6s()

Get a list of all AAAA (IPv6) records in the account.

Implementing this method in subclasses is optional. If not implemented, then fetch_domain_ipv6s() and put_domain_ipv6s() must be implemented.

If implemented, this function should return a list of AAAA (IPv6) records in the form (domain, addr, ttl) where domain is the domain name for the record, addr is an IPv6Address, and ttl is the TTL of the record.

The ttl may be set to None if the API does not provide it. It is only required for providers that would change the TTL back to default if it’s not explicitly included when Ruddr later updates the record.

If there are multiple records/IPv6s for a single domain, return them as separate list items with the same domain. If the domain needs to be updated by Ruddr, it will update all of them.

Returns:

A list of AAAA records in the format described

Raises:
  • NotImplementedError – if not implemented

  • PublishError – if implemented, but there is a failure, or the zone does not exist

Return type:

List[Tuple[str, IPv6Address, int | None]]

abstract fetch_domain_ipv4s(domain)

Get a list of A (IPv4) records for the given domain.

Implementing this method in subclasses is optional. It only needs to be implemented if fetch_all_ipv4s() is not implemented.

This function should return a list of A (IPv4) records for the given domain. The return value is a list of tuples (addr, ttl) where addr is an IPv4Address and ttl is the TTL of the record.

The ttl may be set to None if the API does not provide it. It is only required for providers that would change the TTL back to default if it’s not explicitly included when Ruddr later updates the record.

The return value is a list in case there is more than one A record associated with the domain; however, note that Ruddr will want to replace all of them with a single record.

Parameters:

domain (str) – The domain to fetch records for

Returns:

A list of A records in the format described

Raises:
  • NotImplementedError – if not implemented

  • PublishError – if implemented, but there is a failure, or no such record exists

Return type:

List[Tuple[IPv4Address, int | None]]

abstract fetch_domain_ipv6s(domain)

Get a list of AAAA (IPv6) records for the given domain.

Implementing this method in subclasses is optional. It only needs to be implemented if fetch_all_ipv6s() is not implemented.

This function should return a list of AAAA (IPv6) records for the given domain. The return value is a list of tuples (addr, ttl) where addr is an IPv6Address and ttl is the TTL of the record.

The ttl may be set to None if the API does not provide it. It is only required for providers that would change the TTL back to default if it’s not explicitly included when Ruddr later updates the record.

The return value is a list in case there is more than one AAAA record associated with the domain. Ruddr will update all of them.

Parameters:

domain (str) – The domain to fetch records for

Returns:

A list of AAAA records in the format described

Raises:
  • NotImplementedError – if not implemented

  • PublishError – if implemented, but there is a failure, or no such record exists

Return type:

List[Tuple[IPv6Address, int | None]]

abstract put_all_ipv4s(records)

Publish A (IPv4) records for the account.

Implementing this method in subclasses is optional. However, either this function or put_domain_ipv4() must be implemented. The latter must be implemented if fetch_all_ipv4s() is not implemented.

If implemented, this function should replace all the A (IPv4) records in the account with the records provided. The records are provided as a dict where the keys are the domain names and the values are 2-tuples (addrs, ttl) where addrs is a list of IPv4Address and ttl is an int (or None if the fetch_all_ipv4s() function didn’t provide any).

Records that Ruddr is not configured to update will be passed through from fetch_all_ipv4s() unmodified.

Parameters:

records (Dict[str, Tuple[List[IPv4Address], int | None]]) – The records to publish

Raises:
  • NotImplementedError – if not implemented

  • PublishError – if implemented, but there is a failure

abstract put_all_ipv6s(records)

Publish AAAA (IPv6) records for the account.

Implementing this method in subclasses is optional. However, either this function or put_domain_ipv6s() must be implemented. The latter must be implemented if fetch_all_ipv6s() is not implemented.

If implemented, this function should replace all the AAAA (IPv6) records in the account with the records provided. The records are provided as a dict where the keys are the domain names and the values are 2-tuples (addrs, ttl) where addrs is a list of IPv6Address and ttl is an int (or None if the fetch_all_ipv6s() function didn’t provide any).

Records that Ruddr is not configured to update will be passed through from fetch_all_ipv6s() unmodified.

Parameters:

records (Dict[str, Tuple[List[IPv6Address], int | None]]) – The records to publish

Raises:
  • NotImplementedError – if not implemented

  • PublishError – if implemented, but there is a failure

abstract put_domain_ipv4(domain, address, ttl)

Publish an A (IPv4) record for the given domain.

Implementing this method in subclasses is optional. However, it must be implemented if either fetch_all_ipv4s() or put_all_ipv4s() are not implemented.

This function should replace the A (IPv4) records for the given domain with a single A record matching the given parameters.

This will only be called for the domains Ruddr is configured to update.

Parameters:
  • domain (str) – The domain to publish the record for

  • address (IPv4Address) – The address for the new record

  • ttl (int | None) – The TTL for the new record (or None if the fetch_*_ipv4s functions didn’t provide any). Ruddr passes this through unchanged.

Raises:
  • NotImplementedError – if not implemented

  • PublishError – if implemented, but there is a failure

abstract put_domain_ipv6s(domain, addresses, ttl)

Publish AAAA (IPv6) records for the given domain.

Implementing this method in subclasses is optional. However, it must be implemented if either fetch_all_ipv6s() or put_all_ipv6s() are not implemented.**

This function should replace the AAAA (IPv6) records for the given domain with the records provided.

This will only be called for the domains Ruddr is configured to update.

Parameters:
  • domain (str) – The domain to publish the records for

  • addresses (List[IPv6Address]) – The address for the new records

  • ttl (int | None) – The TTL for the new records (or None if the fetch_*_ipv6s functions didn’t provide any). Ruddr passes this through unchanged.

Raises:
  • NotImplementedError – if not implemented

  • PublishError – if implemented, but there is a failure

class ruddr.TwoWayZoneUpdater(name, addrfile, datadir)

Base class for updaters supporting protocols that are two-way and zone-based, that is:

  • The API supports fetching the current address(es) for hosts, either individually or by fetching whole zones

  • The API involves zones in some way, e.g. entire zones can be fetched or updated at once, or fetching/updating a single domain requires specifying its zone

It’s meant to be flexible enough for a variety of API styles. For example, some APIs may be very flexible, allowing individual domains’ records to be fetched and updated. Others may be strictly zone-based, only providing APIs to fetch and replace entire zones. Still others may be a hybrid, with a way to fetch an entire zone but only update single domains. This class supports all of the above by allowing only the appropriate methods to be implemented.

The update process for this type of updater works as follows:

  1. The list of hosts is organized into zones:

    • Hosts with hardcoded zone in the config are placed in that zone

    • If there are any hosts remaining, first try get_zones() to get a list of zones, and assign zones from that. If there are still any hosts that don’t fit into zones, it’s a PublishError.

    • If get_zones() is not implemented, any hosts without hardcoded zones are assigned to zones using the public suffix list.

  2. For each zone:

    1. Fetch A/AAAA records for the zone

    2. Create replacement records for the hosts to be updated. If there is not an existing record for a host, it’s a PublishError. If there are multiple existing records, they are replaced with a single one for IPv4, and they are all updated for IPv6. Other records are left untouched.

    3. Write the A/AAAA records for the zone

Parameters:
  • name (str) – Name of the updater (from config section heading)

  • addrfile (Addrfile) – The Addrfile object

  • datadir (str) – The configured data directory

init_hosts_and_zones(hosts)

Provide the list of hosts to be updated, optionally with their zones if configured.

This is separate from __init__() so subclasses can rely on the logger while doing their config parsing, then pass the list of hosts in via this method after. It must be called before your subclass’s constructor completes.

The list can be provided either as an unparsed str or as a list of 2-tuples (fqdn, zone):

  • When provided as an unparsed str, it should be a whitespace-separated list whose items are in the format foo.example.com or foo.example.com/example.com (the latter format explicitly setting the zone)

  • When provided as a list of 2-tuples, fqdn is the FQDN for the host and zone is either None or a str explicitly specifying the zone for this host.

For hosts without a zone explicitly specified (which can be all of them), Ruddr will use get_zones() to determine the zone, or the public suffix list if get_zones() is not implemented.

Parameters:

hosts (List[Tuple[str, str | None]] | str) – The list of hosts to be updated

Raises:

ConfigError – if an FQDN does not reside in the zone provided with it, or is a duplicate

abstract get_zones()

Get a list of zones under the account.

Implementing this method in subclasses is optional.

If implemented, this function should return a list of zones (more specifically, the domain name for each zone). The FQDNs-to-be-updated will be compared against the zone list. This serves two purposes:

  1. It allows better error checking. If any of the FQDNs do not fall into one of the available zones, Ruddr can catch that and log it for the user.

  2. If any of the zones are not immediate subdomains of a public suffix (public suffix being .com, .co.uk, etc., see public suffix list), for example, myzone.provider.com, this allows Ruddr to get the correct zone without it being manually configured.

If not implemented, Ruddr uses the public suffix list to assign zones to any FQDNs without explicitly-configured zones.

Returns:

A list of zones

Raises:
  • NotImplementedError – if not implemented

  • PublishError – if fetching the zones is implemented, but failed

Return type:

List[str]

abstract fetch_zone_ipv4s(zone)

Get a list of A (IPv4) records for the given zone.

Implementing this method in subclasses is optional. If not implemented, then fetch_subdomain_ipv4s() and put_subdomain_ipv4() must be implemented.

If implemented, this function should return a list of A (IPv4) records in the given zone in the form (name, addr, ttl) where name is the subdomain portion (e.g. a record for “foo.bar.example.com” in zone “example.com” should return “foo.bar” as the name), addr is an IPv4Address, and ttl is the TTL of the record.

Some notes:

  • name should be empty for the root domain in the zone

  • The subdomain_of() function may be helpful for the name element if the provider’s API returns FQDNs

  • The ttl may be set to None if the API does not provide it. It is only required for providers that would change the TTL back to default if it’s not explicitly included when Ruddr later updates the record.

  • If there are multiple records/IPv4s for a single subdomain, return them as separate list items with the same name. Note that if the subdomain needs to be updated by Ruddr, it will only produce a single record to replace them.

Parameters:

zone (str) – The zone to fetch records for

Returns:

A list of A records in the format described

Raises:
  • NotImplementedError – if not implemented

  • PublishError – if implemented, but there is a failure, or the zone does not exist

Return type:

List[Tuple[str, IPv4Address, int | None]]

abstract fetch_zone_ipv6s(zone)

Get a list of AAAA (IPv6) records for the given zone.

Implementing this method in subclasses is optional. If not implemented, then fetch_subdomain_ipv6s() and put_subdomain_ipv6s() must be implemented.

If implemented, this function should return a list of AAAA (IPv6) records in the given zone in the form (name, addr, ttl) where name is the subdomain portion (e.g. a record for “foo.bar.example.com” in zone “example.com” should return “foo.bar” as the name), addr is an IPv6Address, and ttl is the TTL of the record.

Some notes:

  • name should be empty for the root domain in the zone

  • The subdomain_of() function may be helpful for the name element if the provider’s API returns FQDNs

  • The ttl may be set to None if the API does not provide it. It is only required for providers that would change the TTL back to default if it’s not explicitly included when Ruddr later updates the record.

  • If there are multiple records/IPv6s for a single subdomain, return them as separate list items with the same name. If the subdomain needs to be updated by Ruddr, it will update all of them.

Parameters:

zone (str) – The zone to fetch records for

Returns:

A list of AAAA records in the format described

Raises:
  • NotImplementedError – if not implemented

  • PublishError – if implemented, but there is a failure, or the zone does not exist

Return type:

List[Tuple[str, IPv6Address, int | None]]

abstract fetch_subdomain_ipv4s(subdomain, zone)

Get a list of A (IPv4) records for the given domain.

Implementing this method in subclasses is optional. It only needs to be implemented if fetch_zone_ipv4s() is not implemented.

This function should return a list of A (IPv4) records for the given domain. If this provider’s API requires using the original FQDN (rather than separate subdomain and zone fields), use fqdn_of() on the parameters to obtain it.

The return value is a list of tuples (addr, ttl) where addr is an IPv4Address and ttl is the TTL of the record. As with fetch_zone_ipv4s():

  • The ttl may be set to None if the API does not provide it. It is only required for providers that would change the TTL back to default if it’s not explicitly included when Ruddr later updates the record.

  • The return value is a list in case there is more than one A record associated with the domain; however, note that Ruddr will want to replace all of them with a single record.

Parameters:
  • subdomain (str) – The subdomain to fetch records for (only the subdomain portion), empty for the root domain of the zone

  • zone (str) – The zone the subdomain belongs to

Returns:

A list of A records in the format described

Raises:
  • NotImplementedError – if not implemented

  • PublishError – if implemented, but there is a failure, or no such record exists

Return type:

List[Tuple[IPv4Address, int | None]]

abstract fetch_subdomain_ipv6s(subdomain, zone)

Get a list of AAAA (IPv6) records for the given domain.

Implementing this method in subclasses is optional. It only needs to be implemented if fetch_zone_ipv6s() is not implemented.

This function should return a list of AAAA (IPv6) records for the given domain. If this provider’s API requires using the original FQDN (rather than separate subdomain and zone fields), use fqdn_of() on the parameters to obtain it.

The return value is a list of tuples (addr, ttl) where addr is an IPv6Address and ttl is the TTL of the record. As with fetch_zone_ipv6s():

  • The ttl may be set to None if the API does not provide it. It is only required for providers that would change the TTL back to default if it’s not explicitly included when Ruddr later updates the record.

  • The return value is a list in case there is more than one AAAA record associated with the domain. Ruddr will update all of them.

Parameters:
  • subdomain (str) – The subdomain to fetch records for (only the subdomain portion), empty for the root domain of the zone

  • zone (str) – The zone the subdomain belongs to

Returns:

A list of AAAA records in the format described

Raises:
  • NotImplementedError – if not implemented

  • PublishError – if implemented, but there is a failure, or no such record exists

Return type:

List[Tuple[IPv6Address, int | None]]

abstract put_zone_ipv4s(zone, records)

Publish A (IPv4) records for the given zone.

Implementing this method in subclasses is optional. However, either this function or put_subdomain_ipv4() must be implemented. The latter must be implemented if fetch_zone_ipv4s() is not implemented.

If implemented, this function should replace all the A records for the given zone with the records provided. The records are provided as a dict where the keys are the subdomain names and the values are 2-tuples (addrs, ttl) where addrs is a list of IPv4Address and ttl is an int (or None if the fetch_zone_ipv4s() function didn’t provide any).

Records that Ruddr is not configured to update will be passed through from fetch_zone_ipv4s() unmodified.

Parameters:
  • zone (str) – The zone to publish records for

  • records (Dict[str, Tuple[List[IPv4Address], int | None]]) – The records to publish

Raises:
  • NotImplementedError – if not implemented

  • PublishError – if implemented, but there is a failure

abstract put_zone_ipv6s(zone, records)

Publish AAAA (IPv6) records for the given zone.

Implementing this method in subclasses is optional. However, either this function or put_subdomain_ipv6s() must be implemented. The latter must be implemented if fetch_zone_ipv6s() is not implemented.

If implemented, this function should replace all the AAAA records for the given zone with the records provided. The records are provided as a dict where the keys are the subdomain names and the values are 2-tuples (addrs, ttl) where addrs is a list of IPv6Address and ttl is an int (or None if the fetch_zone_ipv6s() function didn’t provide any).

Records that Ruddr is not configured to update will be passed through from fetch_zone_ipv6s() unmodified.

Parameters:
  • zone (str) – The zone to publish records for

  • records (Dict[str, Tuple[List[IPv6Address], int | None]]) – The records to publish

Raises:
  • NotImplementedError – if not implemented

  • PublishError – if implemented, but there is a failure

abstract put_subdomain_ipv4(subdomain, zone, address, ttl)

Publish an A (IPv4) record for the given domain.

Implementing this method in subclasses is optional. However, it must be implemented if either fetch_zone_ipv4s() or put_zone_ipv4s() are not implemented.

This function should replace the A records for the given domain with a single A record matching the given parameters. If this provider’s API requires using the original FQDN (rather than separate subdomain and zone fields), use fqdn_of() on the parameters to obtain it.

This will only be called for the domains Ruddr is configured to update.

Parameters:
  • subdomain (str) – The subdomain to publish the record for (only the subdomain portion), empty for the root domain of the zone

  • zone (str) – The zone the subdomain belongs to

  • address (IPv4Address) – The address for the new record

  • ttl (int | None) – The TTL for the new record (or None if the fetch_*_ipv4s functions didn’t provide any). Ruddr passes this through unchanged.

Raises:
  • NotImplementedError – if not implemented

  • PublishError – if implemented, but there is a failure

abstract put_subdomain_ipv6s(subdomain, zone, addresses, ttl)

Publish AAAA (IPv6) records for the given domain.

Implementing this method in subclasses is optional. However, it must be implemented if either fetch_zone_ipv6s() or put_zone_ipv6s() are not implemented.

This function should replace the AAAA records for the given domain with the records provided. If this provider’s API requires using the original FQDN (rather than separate subdomain and zone fields), use fqdn_of() on the parameters to obtain it.

This will only be called for the domains Ruddr is configured to update.

Parameters:
  • subdomain (str) – The subdomain to publish the records for (only the subdomain portion), empty for the root domain of the zone

  • zone (str) – The zone the subdomain belongs to

  • addresses (List[IPv6Address]) – The addresses for the new records

  • ttl (int | None) – The TTL for the new records (or None if the fetch_*_ipv6s functions didn’t provide any). Ruddr passes this through unchanged.

Raises:
  • NotImplementedError – if not implemented

  • PublishError – if implemented, but there is a failure

static subdomain_of(fqdn, zone)

Return the subdomain portion of the given FQDN.

Parameters:
  • fqdn (str) – The FQDN to get the subdomain of (without trailing dot)

  • zone (str) – The zone this FQDN belongs to (empty for root zone)

Returns:

The subdomain portion, e.g. “foo.bar” for FQDN “foo.bar.example.com” with zone “example.com”, or the empty string if the FQDN is the zone’s root domain

Raises:

ValueError – if the FQDN is not in the given zone

Return type:

str

static fqdn_of(subdomain, zone)

Return an FQDN for the given subdomain in the given zone.

Parameters:
  • subdomain (str) – The subdomain to return an FQDN for, or the empty string for the zone’s root domain

  • zone (str) – The zone the subdomain resides in (empty for root zone)

Returns:

An FQDN, without trailing dot

Return type:

str

Low-Level Updater Base Classes

class ruddr.Updater(name, addrfile)

Base class for Ruddr updaters. Handles setting up logging, retries, the initial update, and working with the addrfile.

Parameters:
  • name (str) – Name of the updater (from config section heading)

  • addrfile (Addrfile) – The Addrfile object

publish_ipv4(address)

Publish a new IPv4 address to the appropriate DDNS provider. Will only be called if an update contains a new address or a previous update failed.

Must be implemented by subclasses if they support IPv4 updates.

Parameters:

address (IPv4Address) – IPv4Address to publish

Raises:

PublishError – when publishing fails (will retry automatically after a delay)

publish_ipv6(network)

Publish a new IPv6 prefix to the appropriate DDNS provider. Will only be called if an update contains a new address or a previous update failed.

Must be implemented by subclasses if they support IPv6 updates.

Parameters:

network (IPv6Network) – IPv6Network with the prefix to publish

Raises:

PublishError – when publishing fails (will retry automatically after a delay)

static replace_ipv6_prefix(network, address)

Replace the prefix portion of the given IPv6 address with the network prefix provided and return the result

Parameters:
  • network (IPv6Network) – The network prefix to set

  • address (IPv6Address) – The address to set the network prefix on

Returns:

The modified address

Return type:

IPv6Address

class ruddr.BaseUpdater(name, addrfile)

Skeletal superclass for Updater. It sets up the logger, sets up some useful member variables, and little else. Custom updaters can opt to override this instead if the default logic in Updater does not suit their needs (e.g. if the protocol requires IPv4 and IPv6 updates to be sent simultaneously, custom retry logic, etc.).

Parameters:
  • name (str) – Name of the updater (from config section heading)

  • addrfile (Addrfile) – The Addrfile object

name: str

Updater name (from config section heading)

addrfile: Addrfile

Addrfile for avoiding duplicate updates

min_retry_interval: int

Minimum retry interval (some providers may require a minimum delay when there are server errors, in which case, subclasses can modify this)

halt: bool

@Retry will set this to True when there has been a fatal error and no more updates should be issued.

initial_update(ipv4_attached, ipv6_attached)

Do the initial update: Check the addrfile, and if either address is defunct but has a last-attempted-address, try to publish it again.

Parameters:
  • ipv4_attached (bool) – Whether this updater should do an initial update for IPv4 (that is, whether it is attached to a notifier for IPv4)

  • ipv6_attached (bool) – Whether this updater should do an initial update for IPv6 (that is, whether it is attached to a notifier for IPv6)

update_ipv4(address)

Receive a new IPv4 address from the attached notifier. If it does not match the current address, call the subclass’ publish function, update the addrfile if successful, and retry if not.

Parameters:

address (IPv4Address) – IPv4Address to update with

update_ipv6(address)

Receive a new IPv6 prefix from the attached notifier. If it does not match the current prefix, call the subclass’ publish function, update the addrfile if successful, and retry if not.

Parameters:

address (IPv6Network) – IPv6Network to update with

static replace_ipv6_prefix(network, address)

Replace the prefix portion of the given IPv6 address with the network prefix provided and return the result

Parameters:
  • network (IPv6Network) – The network prefix to set

  • address (IPv6Address) – The address to set the network prefix on

Returns:

The modified address

Return type:

IPv6Address

static pick_error(curr_err, new_err)

Return the current error, unless there isn’t one or new_err is a higher priority error

Using your Custom Updater or Notifier

Once you have a custom updater or notifier class, there are two ways to start using it.

The first way is by module name and class name. For this to work, the module containing your updater/notifier class must be in the module search path. Typically, this means you’ll either have to install it or make sure the PYTHONPATH environment variable includes the path to your module when Ruddr is run. Then, you can use the module and type options in your updater or notifier config, and Ruddr will import and use it.

For example, suppose you have an updater class MyUpdater in a file named myupdater.py. Assuming that Python file is in some directory in your PYTHONPATH, you can use an updater configuration like this:

[updater.main]
module = myupdater
type = MyUpdater
# ...

The second way to start using it is by creating a Python package with a ruddr.updater or ruddr.notifier entry point. This requires slightly more work upfront (you have to create a pyproject.toml), but has the advantage that it becomes very easy to publish your updater or notifier to PyPI for others to use, if you so choose. If you want to go this route, you can follow these steps:

  1. Set up an empty directory for your package and put the module containing your updater or notifier inside. In the simplest case, the module may be a single .py file, but it can be a package with submodules, etc. For demonstration, we will assume you have a notifier in a single-file Python module, mynotifier.py, and the notifier class inside is MyNotifier.

  2. If you intend to share your updater or notifier, e.g. on PyPI, GitHub, or otherwise, you may want to create a README.md in the same directory.

  3. Create a file pyproject.toml in the directory with contents similar to this:

    [build-system]
    requires = ["setuptools>=61.0"]
    build-backend = "setuptools.build_meta"
    
    [project]
    # This becomes the package name on PyPI, if you choose to publish it
    name = "ruddr_notifier_mynotifier"
    version = "0.0.1"
    authors = [
        { name="Your Name", email="your_email@example.com" },
    ]
    description = "My Ruddr Notifier"
    # Uncomment the next line if you created a README
    #readme = "README.md"
    requires-python = ">=3.7"
    classifiers = [
        "Programming Language :: Python :: 3",
    ]
    
    [project.entry-points."ruddr.notifier"]
    my_notifier = "mynotifier:MyNotifier"
    

    Be sure to set the name, version, authors, and description as appropriate, but that last section is the important part. In that example, an entry point named my_notifier is created in the ruddr.notifier group, and it points to the MyNotifier class in the mynotifier module.

  4. You now have an installable Python package. Use pip install -U . to install it from the current directory. (If you wish to make it public, you can also publish it to PyPI and install it by name.)

  5. Once installed, the entry point name, my_notifier in the example above, can be used as the notifier type in your Ruddr config. For example:

    [notifier.main]
    type = my_notifier
    # ...
    

Using Ruddr as a Library

Ruddr’s primary use case is as a standalone service, but it can be integrated into other Python programs as a library as well. The steps boil down to this:

  1. First, create an instance of Config. It can be created directly, or you may use read_config() or read_config_from_path().

  2. Use the Config to create a DDNSManager.

  3. Call start() on the DDNSManager you created. This will return once Ruddr finishes starting. Ruddr runs in background non-daemon threads (“non-daemon” meaning that your program will not end until they are stopped as described in the next step).

  4. When ready for Ruddr to stop, call stop() on your :class`DDNSManager` object. Ruddr will halt the background threads gracefully.

An immediate update (if possible) can be triggered on a started DDNSManager by calling its do_notify() method. This is not always possible if the configured notifiers do not support it, though most do.

See the next section for the APIs involved.

Warning

The config file reader functions can throw ConfigError. The DDNSManager constructor can raise ConfigError and its start() function can raise NotifierSetupError. Be ready to handle those exceptions. Both of them can be caught under RuddrSetupError.

Manager and Config API

ruddr.read_config(configfile)

Read configuration in from the given file-like object opened in text mode

Parameters:

configfile (Iterable[str]) – Filelike object to read the config from

Raises:

ConfigError – if the config file cannot be read or is invalid

Returns:

A Config ready to be passed to DDNSManager

Return type:

Config

ruddr.read_config_from_path(filename)

Read configuration from the named file or Path

Parameters:

filename (str | Path) – Filename or path to read from

Raises:

ConfigError – if the config file cannot be read or is invalid

Returns:

A Config ready to be passed to DDNSManager

Return type:

Config

class ruddr.Config(main, notifiers, updaters)

Contains all Ruddr configuration required by DDNSManager. Normally, this would be created from a configuration file by read_config() or read_config_from_path(), but it can also be created directly when using Ruddr as a library.

Note that all configuration values should be strings, as they would be from Python’s configparser.ConfigParser.

The configuration must be finalized before use, but programs using Ruddr as a library need not concern themselves with that. DDNSManager will do that itself.

Parameters:
  • main (Dict[str, str]) – A dictionary of global configuration options, that is, the options that go under [ruddr] in the configuration file.

  • notifiers (Dict[str, Dict[str, str]]) – A dictionary of notifier configurations. Keys are notifier names (i.e. the XYZ part of [notifier.XYZ] if it were from a configuration file) and values are themselves dicts of config options for that notifier.

  • updaters (Dict[str, Dict[str, str]]) – A dictionary of updater configurations. Keys are updater names (i.e. the XYZ part of [updater.XYZ] if it were from a configuration file) and values are themselves dicts of config options for that updater.

finalize(validate_notifier_type, validate_updater_type)

Used by DDNSManager to finalize the configuration. This consists of validating the updater and notifier types, filling default values, and doing some normalization.

Programs using Ruddr as a library need not call this function themselves; DDNSManager will handle it.

Parameters:
  • validate_notifier_type (Callable[[str | None, str], bool]) – A callable to check if a notifier type is valid. First parameter is a module name, or None if it’s a built-in notifier type. Second parameter is a class name or built-in notifier type name.

  • validate_updater_type (Callable[[str | None, str], bool]) – A callable to check if an updater type is valid. Parameters are the same as above.

Raises:

ConfigError – if the configuration is invalid

class ruddr.DDNSManager(config)

Manages the rest of the Ruddr system. Creates notifiers and updaters and manages the addrfile.

Parameters:

config (Config) – A Config with the configuration to use

Raises:

ConfigError – if configuration is not valid

start()

Start running all notifiers. Returns after they start. The notifiers will continue running in background threads.

Raises:

NotifierSetupError – if a notifier fails to start

do_notify()

Do an on-demand notify from all notifiers.

Not all notifiers will support this, but most will.

Does not raise any exceptions.

stop()

Stop all running notifiers gracefully. This will allow Python to exit naturally.

Does not raise any exceptions, even if not yet started.

Ruddr Exceptions

Below is a summary of all the exceptions that can be raised by Ruddr or in custom notifiers and updaters. Note that the rest of the API documentation on this page describes more precisely when particular exceptions might be raised and when it’s appropriate for subclasses to raise them.

exception ruddr.RuddrException

Bases: Exception

Base class for all Ruddr exceptions

exception ruddr.RuddrSetupError

Bases: RuddrException

Base class for Ruddr exceptions that happen during startup

exception ruddr.ConfigError

Bases: RuddrSetupError

Raised when the configuration is malformed or has other errors

exception ruddr.NotifierSetupError

Bases: RuddrSetupError

Raised by a notifier when there is a fatal error during setup for persistent checks

exception ruddr.NotStartedError

Bases: RuddrException

Raised when requesting an on-demand notify before starting the notifier

exception ruddr.NotifyError

Bases: RuddrException

Notifiers should raise when an attempt to check the current IP address fails. Doing so triggers the retry mechanism.

exception ruddr.PublishError

Bases: RuddrException

Updaters should raise when an attempt to publish an update fails. Doing so triggers the retry mechanism.

exception ruddr.FatalPublishError

Bases: PublishError

Updaters should raise when an attempt to publish an update fails in a non-recoverable way. Doing so causes the updater to halt until the next time Ruddr is started.

Development on Ruddr Itself

Everything discussed so far on this page has been about development that ties into Ruddr. This section is for development on Ruddr itself, for example fixing bugs or adding new features.

Installation for Development

The latest sources for Ruddr are available on GitHub. Once you have cloned the repo, the easiest way to work on development is to optionally set up a virtual environment, then install directly out of the repo with the dev extra.

Assuming you have a shell open in the repo:

# Optionally, set up a virtual environment
python3 -m venv venv
. venv/bin/activate
# Install in develop mode with the "dev" extra
pip install -U -e .[dev]

The dev extra includes everything required to check style, check types, run unit tests, and regenerate the documentation.

Running Tests

Ruddr’s full set of checks and tests can be run with tox. It includes style checks and linting, type checking, and unit tests with coverage. If you installed with the dev extra above, you have everything you need. (Alternatively, the test extra includes just the testing tools from the dev extra.)

To run the full test suite, make sure your virtual environment is active (if you are using one) and run the tox command:

# Skip this line if not using a virtual environment
. venv/bin/activate
tox

This will first run flake8 and pytype. Then it will run pytest with coverage on each supported version of Python. Lastly, it will generate the coverage report in the terminal and write it to htmlcov/index.html.

These tools can also be run individually:

  • flake8 src/ test/

  • pytype

  • pytest --cov

Generating Docs

The documentation is available online at https://ruddr.dcpx.org/, but if you would like to generate a local copy (for reference or to preview changes), install the docs extra (the dev extra includes the docs extra) and build the docs in docs/ as usual for Sphinx:

# Assuming you are in the git repo:
pip install .[docs]
cd docs
make html

Open docs/_build/html/index.html to read them.

You can also generate other formats with make <format>, provided the necessary tools are available (e.g. make latexpdf requires a LaTeX distribution to be installed). The output will be in docs/_build/<format>/.

Contributions

If you have code you would like to contribute, please feel free to submit a pull request on GitHub. (Note that Issues and Bugs and Feature Requests are also helpful and very much appreciated!)

There are a few guidelines that make it more likely a PR can be accepted:

  • Generally speaking, development happens on the dev branch. The master branch is reserved for released code only. (If you submit a pull request to master, we will change it to dev.)

  • The automated test suite should run when pull requests are submitted. If there are any problems, you should do your best to fix them (or explain why the test is flagging when it shouldn’t). Code that passes has a much higher chance of being accepted than code that fails. See Running Tests above.

  • Pay attention to code style. Flake8 runs as part of the test suite. #noqa is allowed, but with good reason.

  • If you add new functionality, it has a higher chance of being accepted if you add additional documentation and tests to go with it. The automated test suite generates a code coverage report, both locally and online at TODO.

  • Pull requests need not be related to an existing issue, but if you submit one that is, you should reference the issue number somewhere in the pull request.

None of these are automatic deal breakers if you do not follow them, but following them does increase the chances of your pull request being accepted.

All merged code contributions will be mentioned in the CHANGELOG with attribution to the contributor.

Contributing Updaters and Notifiers

If you have written a new updater or notifier and wish to share it with the community, you have two options:

  1. Contribute it for inclusion in Ruddr itself. To do this:

    1. Add a new .py file with your updater/notifier under the appropriate package in the Ruddr sources (src/ruddr/updaters or src/ruddr/notifiers)

    2. Add a new entry to the updaters or notifiers dict in the __init__.py file in the same directory. The key will become the built-in type name of the updater or notifier, used for the type= config option. The value must be the class for the new updater/notifier.

    3. Add documentation for the new updater/notifier. Add a new section to docs/updaters.rst or docs/notifiers.rst listing the name, a brief description of the updater/notifier, a sample config snippet, and a detailed list of the configuration options it accepts.

    4. Open a pull request.

  2. If you would prefer to independently maintain your updater or notifier, you can publish it to PyPI with a ruddr.updater or ruddr.notifier entry point. Anyone who installs your updater/notifier from PyPI will then be able to use that entry point name as a type= option in their config.

    For example, if you declare this entry point in your pyproject.toml:

    [project.entry-points."ruddr.updater"]
    my_updater = "myupdater:MyUpdater"
    

    then someone can use your myupdater.MyUpdater class as an updater with this Ruddr config snippet:

    [updater.foo]
    type = my_updater
    # ... other config for the updater
    

    For more information on publishing an updater like this, see the second method under Using your Custom Updater or Notifier.

Some conventions when developing updaters or notifiers for inclusion in Ruddr:

  • Make liberal use of the logger, especially when there is a problem. In particular, Ruddr uses exceptions mainly to control contingency behavior when there is a problem. The exception message is, for the most part, ignored. That’s not to say exceptions shouldn’t carry an appropriate message, but the primary way errors are communicated to the user is through logging. If there is a problem, an error should always be logged (critical if the problem is fatal to the updater/notifier), and a warning should be logged for potential problems.

  • Do as much useful work as possible, even if errors require skipping some parts. For example, if an updater can’t update one domain due to a typo in its name, it should still update the rest of the configured domains.

  • Don’t trust any input, whether from user config or API calls. For example, an improperly formatted IP address should be caught, logged, and an appropriate Ruddr exception raised, preventing a ValueError from crashing the whole program.