|
|
@ -12,6 +12,17 @@ from .record import Create, Delete |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SubzoneRecordException(Exception): |
|
|
class SubzoneRecordException(Exception): |
|
|
|
|
|
''' |
|
|
|
|
|
Exception raised when a record belongs in a sub-zone but is added to the parent. |
|
|
|
|
|
|
|
|
|
|
|
This exception is raised when attempting to add a record to a zone that |
|
|
|
|
|
should actually be managed in a configured sub-zone. Only NS and DS records |
|
|
|
|
|
are allowed at the sub-zone boundary. |
|
|
|
|
|
|
|
|
|
|
|
:param record: The record that caused the exception. |
|
|
|
|
|
:type record: octodns.record.base.Record |
|
|
|
|
|
''' |
|
|
|
|
|
|
|
|
def __init__(self, msg, record): |
|
|
def __init__(self, msg, record): |
|
|
self.record = record |
|
|
self.record = record |
|
|
|
|
|
|
|
|
@ -22,6 +33,19 @@ class SubzoneRecordException(Exception): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class DuplicateRecordException(Exception): |
|
|
class DuplicateRecordException(Exception): |
|
|
|
|
|
''' |
|
|
|
|
|
Exception raised when attempting to add a duplicate record to a zone. |
|
|
|
|
|
|
|
|
|
|
|
A duplicate is defined as a record with the same name and type as an |
|
|
|
|
|
existing record in the zone. The exception includes references to both |
|
|
|
|
|
the existing and new records for debugging. |
|
|
|
|
|
|
|
|
|
|
|
:param existing: The existing record in the zone. |
|
|
|
|
|
:type existing: octodns.record.base.Record |
|
|
|
|
|
:param new: The new record being added. |
|
|
|
|
|
:type new: octodns.record.base.Record |
|
|
|
|
|
''' |
|
|
|
|
|
|
|
|
def __init__(self, msg, existing, new): |
|
|
def __init__(self, msg, existing, new): |
|
|
self.existing = existing |
|
|
self.existing = existing |
|
|
self.new = new |
|
|
self.new = new |
|
|
@ -44,6 +68,17 @@ class DuplicateRecordException(Exception): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidNodeException(Exception): |
|
|
class InvalidNodeException(Exception): |
|
|
|
|
|
''' |
|
|
|
|
|
Exception raised when CNAME records coexist with other records at a node. |
|
|
|
|
|
|
|
|
|
|
|
Per DNS standards, CNAME records cannot coexist with other record types |
|
|
|
|
|
at the same node. This exception is raised when such an invalid |
|
|
|
|
|
configuration is detected. |
|
|
|
|
|
|
|
|
|
|
|
:param record: The record that caused the exception. |
|
|
|
|
|
:type record: octodns.record.base.Record |
|
|
|
|
|
''' |
|
|
|
|
|
|
|
|
def __init__(self, msg, record): |
|
|
def __init__(self, msg, record): |
|
|
self.record = record |
|
|
self.record = record |
|
|
|
|
|
|
|
|
@ -54,10 +89,59 @@ class InvalidNodeException(Exception): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidNameError(Exception): |
|
|
class InvalidNameError(Exception): |
|
|
|
|
|
''' |
|
|
|
|
|
Exception raised when a zone name is invalid. |
|
|
|
|
|
|
|
|
|
|
|
Zone names must: |
|
|
|
|
|
- End with a dot (.) |
|
|
|
|
|
- Not contain double dots (..) |
|
|
|
|
|
- Not contain whitespace |
|
|
|
|
|
''' |
|
|
|
|
|
|
|
|
pass |
|
|
pass |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Zone(object): |
|
|
class Zone(object): |
|
|
|
|
|
''' |
|
|
|
|
|
Container for DNS records belonging to a single DNS zone. |
|
|
|
|
|
|
|
|
|
|
|
A Zone represents a DNS zone and manages all the records within it. It |
|
|
|
|
|
provides methods for adding, removing, and querying records, as well as |
|
|
|
|
|
computing changes between zones and applying those changes. |
|
|
|
|
|
|
|
|
|
|
|
Zones support copy-on-write semantics via the :meth:`copy` method, which |
|
|
|
|
|
creates shallow copies that are hydrated on first modification. This allows |
|
|
|
|
|
for efficient processing of zones through multiple stages without |
|
|
|
|
|
unnecessary copying. |
|
|
|
|
|
|
|
|
|
|
|
Key features: |
|
|
|
|
|
|
|
|
|
|
|
- **Record management**: Add, remove, and query DNS records |
|
|
|
|
|
- **Validation**: Enforce DNS standards (CNAME restrictions, sub-zone rules) |
|
|
|
|
|
- **IDNA support**: Handle internationalized domain names |
|
|
|
|
|
- **Sub-zone awareness**: Respect configured sub-zone boundaries |
|
|
|
|
|
- **Change tracking**: Compute differences between desired and existing state |
|
|
|
|
|
- **Copy-on-write**: Efficient shallow copying with lazy hydration |
|
|
|
|
|
|
|
|
|
|
|
Example usage:: |
|
|
|
|
|
|
|
|
|
|
|
from octodns.zone import Zone |
|
|
|
|
|
from octodns.record import Record |
|
|
|
|
|
|
|
|
|
|
|
zone = Zone('example.com.', []) |
|
|
|
|
|
record = Record.new(zone, 'www', {'type': 'A', 'ttl': 300, 'value': '1.2.3.4'}) |
|
|
|
|
|
zone.add_record(record) |
|
|
|
|
|
|
|
|
|
|
|
# Create a shallow copy |
|
|
|
|
|
copy = zone.copy() |
|
|
|
|
|
# Modifications to copy don't affect the original until hydrated |
|
|
|
|
|
|
|
|
|
|
|
See Also: |
|
|
|
|
|
- :doc:`/zone_lifecycle` for details on zone processing workflow |
|
|
|
|
|
- :class:`octodns.record.base.Record` |
|
|
|
|
|
- :class:`octodns.provider.base.BaseProvider` |
|
|
|
|
|
''' |
|
|
|
|
|
|
|
|
log = getLogger('Zone') |
|
|
log = getLogger('Zone') |
|
|
|
|
|
|
|
|
def __init__( |
|
|
def __init__( |
|
|
@ -67,6 +151,31 @@ class Zone(object): |
|
|
update_pcent_threshold=None, |
|
|
update_pcent_threshold=None, |
|
|
delete_pcent_threshold=None, |
|
|
delete_pcent_threshold=None, |
|
|
): |
|
|
): |
|
|
|
|
|
''' |
|
|
|
|
|
Initialize a DNS zone. |
|
|
|
|
|
|
|
|
|
|
|
:param name: The zone name (must end with a dot). Internationalized |
|
|
|
|
|
domain names (IDN) are automatically encoded to IDNA format. |
|
|
|
|
|
:type name: str |
|
|
|
|
|
:param sub_zones: List of sub-zone names managed separately. Records |
|
|
|
|
|
belonging to sub-zones will be rejected (except NS/DS |
|
|
|
|
|
at the boundary). |
|
|
|
|
|
:type sub_zones: list[str] |
|
|
|
|
|
:param update_pcent_threshold: Override for maximum update percentage |
|
|
|
|
|
threshold. If None, uses provider default. |
|
|
|
|
|
:type update_pcent_threshold: float or None |
|
|
|
|
|
:param delete_pcent_threshold: Override for maximum delete percentage |
|
|
|
|
|
threshold. If None, uses provider default. |
|
|
|
|
|
:type delete_pcent_threshold: float or None |
|
|
|
|
|
|
|
|
|
|
|
:raises InvalidNameError: If the zone name is invalid (missing trailing |
|
|
|
|
|
dot, contains double dots, or has whitespace). |
|
|
|
|
|
|
|
|
|
|
|
.. important:: |
|
|
|
|
|
- Zone names must end with a dot (.) |
|
|
|
|
|
- Zone names are automatically encoded to IDNA format internally |
|
|
|
|
|
- Sub-zones prevent records from being added to the parent zone |
|
|
|
|
|
''' |
|
|
if not name[-1] == '.': |
|
|
if not name[-1] == '.': |
|
|
raise InvalidNameError( |
|
|
raise InvalidNameError( |
|
|
f'Invalid zone name {name}, missing ending dot' |
|
|
f'Invalid zone name {name}, missing ending dot' |
|
|
@ -108,17 +217,53 @@ class Zone(object): |
|
|
|
|
|
|
|
|
@property |
|
|
@property |
|
|
def records(self): |
|
|
def records(self): |
|
|
|
|
|
''' |
|
|
|
|
|
Get all records in this zone. |
|
|
|
|
|
|
|
|
|
|
|
Returns a set of all DNS records in the zone. If this is a shallow copy |
|
|
|
|
|
(not yet hydrated), returns records from the origin zone. |
|
|
|
|
|
|
|
|
|
|
|
:return: Set of all records in the zone. |
|
|
|
|
|
:rtype: set[octodns.record.base.Record] |
|
|
|
|
|
''' |
|
|
if self._origin: |
|
|
if self._origin: |
|
|
return self._origin.records |
|
|
return self._origin.records |
|
|
return set([r for _, node in self._records.items() for r in node]) |
|
|
return set([r for _, node in self._records.items() for r in node]) |
|
|
|
|
|
|
|
|
@property |
|
|
@property |
|
|
def root_ns(self): |
|
|
def root_ns(self): |
|
|
|
|
|
''' |
|
|
|
|
|
Get the root NS record for this zone. |
|
|
|
|
|
|
|
|
|
|
|
The root NS record is the NS record at the zone apex (empty hostname). |
|
|
|
|
|
Returns None if no root NS record exists. |
|
|
|
|
|
|
|
|
|
|
|
:return: The root NS record, or None if not present. |
|
|
|
|
|
:rtype: octodns.record.ns.NsRecord or None |
|
|
|
|
|
''' |
|
|
if self._origin: |
|
|
if self._origin: |
|
|
return self._origin.root_ns |
|
|
return self._origin.root_ns |
|
|
return self._root_ns |
|
|
return self._root_ns |
|
|
|
|
|
|
|
|
def hostname_from_fqdn(self, fqdn): |
|
|
def hostname_from_fqdn(self, fqdn): |
|
|
|
|
|
''' |
|
|
|
|
|
Extract the hostname portion from a fully qualified domain name. |
|
|
|
|
|
|
|
|
|
|
|
Strips the zone name from the FQDN to get just the hostname portion. |
|
|
|
|
|
Handles both IDNA-encoded and UTF-8 domain names correctly. |
|
|
|
|
|
|
|
|
|
|
|
:param fqdn: Fully qualified domain name. |
|
|
|
|
|
:type fqdn: str |
|
|
|
|
|
|
|
|
|
|
|
:return: The hostname portion (without the zone name). |
|
|
|
|
|
:rtype: str |
|
|
|
|
|
|
|
|
|
|
|
Example:: |
|
|
|
|
|
|
|
|
|
|
|
zone = Zone('example.com.', []) |
|
|
|
|
|
zone.hostname_from_fqdn('www.example.com.') # Returns 'www' |
|
|
|
|
|
zone.hostname_from_fqdn('example.com.') # Returns '' |
|
|
|
|
|
''' |
|
|
try: |
|
|
try: |
|
|
fqdn.encode('ascii') |
|
|
fqdn.encode('ascii') |
|
|
# it's non-idna or idna encoded |
|
|
# it's non-idna or idna encoded |
|
|
@ -128,6 +273,27 @@ class Zone(object): |
|
|
return self._utf8_name_re.sub('', fqdn) |
|
|
return self._utf8_name_re.sub('', fqdn) |
|
|
|
|
|
|
|
|
def owns(self, _type, fqdn): |
|
|
def owns(self, _type, fqdn): |
|
|
|
|
|
''' |
|
|
|
|
|
Determine if this zone owns a given FQDN for a specific record type. |
|
|
|
|
|
|
|
|
|
|
|
Checks whether a record with the given FQDN and type should be managed |
|
|
|
|
|
by this zone, taking into account sub-zone boundaries. Records under |
|
|
|
|
|
sub-zones are not owned by the parent (except NS records at the exact |
|
|
|
|
|
sub-zone boundary). |
|
|
|
|
|
|
|
|
|
|
|
:param _type: The DNS record type (e.g., 'A', 'CNAME', 'NS'). |
|
|
|
|
|
:type _type: str |
|
|
|
|
|
:param fqdn: Fully qualified domain name to check. |
|
|
|
|
|
:type fqdn: str |
|
|
|
|
|
|
|
|
|
|
|
:return: True if this zone owns the FQDN for this type, False otherwise. |
|
|
|
|
|
:rtype: bool |
|
|
|
|
|
|
|
|
|
|
|
.. important:: |
|
|
|
|
|
- NS records at sub-zone boundaries are owned by the parent zone |
|
|
|
|
|
- All other records under sub-zones are not owned by the parent |
|
|
|
|
|
- FQDNs are automatically normalized (trailing dot added if missing) |
|
|
|
|
|
''' |
|
|
if fqdn[-1] != '.': |
|
|
if fqdn[-1] != '.': |
|
|
fqdn = f'{fqdn}.' |
|
|
fqdn = f'{fqdn}.' |
|
|
|
|
|
|
|
|
@ -154,6 +320,38 @@ class Zone(object): |
|
|
return True |
|
|
return True |
|
|
|
|
|
|
|
|
def add_record(self, record, replace=False, lenient=False): |
|
|
def add_record(self, record, replace=False, lenient=False): |
|
|
|
|
|
''' |
|
|
|
|
|
Add a DNS record to this zone. |
|
|
|
|
|
|
|
|
|
|
|
Adds the provided record to the zone with validation. If this is a |
|
|
|
|
|
shallow copy (has an origin), it will be hydrated before adding. |
|
|
|
|
|
|
|
|
|
|
|
:param record: The DNS record to add to the zone. |
|
|
|
|
|
:type record: octodns.record.base.Record |
|
|
|
|
|
:param replace: If True, replace any existing record with the same name |
|
|
|
|
|
and type. If False, raise an exception if a duplicate |
|
|
|
|
|
exists. |
|
|
|
|
|
:type replace: bool |
|
|
|
|
|
:param lenient: If True, skip some validation checks (sub-zone checks, |
|
|
|
|
|
CNAME coexistence checks). Useful when loading existing |
|
|
|
|
|
data that may not be standards-compliant. |
|
|
|
|
|
:type lenient: bool |
|
|
|
|
|
|
|
|
|
|
|
:raises SubzoneRecordException: If the record belongs in a configured |
|
|
|
|
|
sub-zone (unless it's an NS/DS record |
|
|
|
|
|
at the boundary). |
|
|
|
|
|
:raises DuplicateRecordException: If a record with the same name and type |
|
|
|
|
|
already exists and ``replace=False``. |
|
|
|
|
|
:raises InvalidNodeException: If adding the record would create an |
|
|
|
|
|
invalid CNAME coexistence situation. |
|
|
|
|
|
|
|
|
|
|
|
.. important:: |
|
|
|
|
|
- Automatically hydrates shallow copies on first modification |
|
|
|
|
|
- NS/DS records are allowed at sub-zone boundaries |
|
|
|
|
|
- CNAME records cannot coexist with other records at the same node |
|
|
|
|
|
- Use ``replace=True`` to update existing records |
|
|
|
|
|
- Use ``lenient=True`` when loading potentially non-compliant data |
|
|
|
|
|
''' |
|
|
if self._origin: |
|
|
if self._origin: |
|
|
self.hydrate() |
|
|
self.hydrate() |
|
|
|
|
|
|
|
|
@ -218,6 +416,21 @@ class Zone(object): |
|
|
node.add(record) |
|
|
node.add(record) |
|
|
|
|
|
|
|
|
def remove_record(self, record): |
|
|
def remove_record(self, record): |
|
|
|
|
|
''' |
|
|
|
|
|
Remove a DNS record from this zone. |
|
|
|
|
|
|
|
|
|
|
|
Removes the provided record from the zone. If this is a shallow copy |
|
|
|
|
|
(has an origin), it will be hydrated before removing. |
|
|
|
|
|
|
|
|
|
|
|
:param record: The DNS record to remove from the zone. |
|
|
|
|
|
:type record: octodns.record.base.Record |
|
|
|
|
|
|
|
|
|
|
|
.. important:: |
|
|
|
|
|
- Automatically hydrates shallow copies on first modification |
|
|
|
|
|
- Clearing the root NS record (empty name) also clears the cached |
|
|
|
|
|
``root_ns`` property |
|
|
|
|
|
- Silently succeeds if the record doesn't exist in the zone |
|
|
|
|
|
''' |
|
|
if self._origin: |
|
|
if self._origin: |
|
|
self.hydrate() |
|
|
self.hydrate() |
|
|
|
|
|
|
|
|
@ -235,6 +448,29 @@ class Zone(object): |
|
|
return self.remove_record(record) |
|
|
return self.remove_record(record) |
|
|
|
|
|
|
|
|
def changes(self, desired, target): |
|
|
def changes(self, desired, target): |
|
|
|
|
|
''' |
|
|
|
|
|
Compute the changes needed to transform this zone into the desired state. |
|
|
|
|
|
|
|
|
|
|
|
Compares this zone (existing state) with the desired zone and returns |
|
|
|
|
|
a list of changes (Creates, Updates, Deletes) required to make this |
|
|
|
|
|
zone match the desired state. Respects record-level include/exclude |
|
|
|
|
|
filtering and provider support. |
|
|
|
|
|
|
|
|
|
|
|
:param desired: The desired zone state to compare against. |
|
|
|
|
|
:type desired: Zone |
|
|
|
|
|
:param target: The target provider that will apply these changes. Used |
|
|
|
|
|
to check record support and apply include/exclude rules. |
|
|
|
|
|
:type target: octodns.provider.base.BaseProvider |
|
|
|
|
|
|
|
|
|
|
|
:return: List of changes needed to transform this zone to the desired state. |
|
|
|
|
|
:rtype: list[octodns.record.change.Change] |
|
|
|
|
|
|
|
|
|
|
|
.. important:: |
|
|
|
|
|
- Skips records marked as ``ignored`` |
|
|
|
|
|
- Respects record-level ``included`` and ``excluded`` lists |
|
|
|
|
|
- Only includes changes for record types the target supports |
|
|
|
|
|
- Returns Creates, Updates (via record.changes), and Deletes |
|
|
|
|
|
''' |
|
|
self.log.debug('changes: zone=%s, target=%s', self, target) |
|
|
self.log.debug('changes: zone=%s, target=%s', self, target) |
|
|
|
|
|
|
|
|
# Build up a hash of the desired records, thanks to our special |
|
|
# Build up a hash of the desired records, thanks to our special |
|
|
@ -351,7 +587,20 @@ class Zone(object): |
|
|
|
|
|
|
|
|
def apply(self, changes): |
|
|
def apply(self, changes): |
|
|
''' |
|
|
''' |
|
|
Apply the provided changes to the zone. |
|
|
|
|
|
|
|
|
Apply a list of changes to this zone. |
|
|
|
|
|
|
|
|
|
|
|
Applies the provided changes by adding new/updated records and removing |
|
|
|
|
|
deleted records. Uses ``replace=True`` and ``lenient=True`` to handle |
|
|
|
|
|
updates and non-standard records gracefully. |
|
|
|
|
|
|
|
|
|
|
|
:param changes: List of changes to apply to the zone. |
|
|
|
|
|
:type changes: list[octodns.record.change.Change] |
|
|
|
|
|
|
|
|
|
|
|
.. important:: |
|
|
|
|
|
- Delete changes remove the existing record |
|
|
|
|
|
- Create and Update changes add the new record with ``replace=True`` |
|
|
|
|
|
- All adds use ``lenient=True`` to skip validation |
|
|
|
|
|
- Changes are applied in the order provided |
|
|
''' |
|
|
''' |
|
|
for change in changes: |
|
|
for change in changes: |
|
|
if isinstance(change, Delete): |
|
|
if isinstance(change, Delete): |
|
|
@ -361,14 +610,25 @@ class Zone(object): |
|
|
|
|
|
|
|
|
def hydrate(self): |
|
|
def hydrate(self): |
|
|
''' |
|
|
''' |
|
|
Take a shallow copy Zone and make it a deeper copy holding its own |
|
|
|
|
|
reference to records. These records will still be the originals and |
|
|
|
|
|
they should not be modified. Changes should be made by calling |
|
|
|
|
|
`add_record`, often with `replace=True`, and/or `remove_record`. |
|
|
|
|
|
|
|
|
|
|
|
Note: This method does not need to be called under normal circumstances |
|
|
|
|
|
as `add_record` and `remove_record` will automatically call it when |
|
|
|
|
|
appropriate. |
|
|
|
|
|
|
|
|
Convert a shallow copy into a hydrated copy with its own record references. |
|
|
|
|
|
|
|
|
|
|
|
Hydration copies all records from the origin zone into this zone, |
|
|
|
|
|
making it independent. The records themselves are still the original |
|
|
|
|
|
objects and should not be modified directly. Use :meth:`add_record` |
|
|
|
|
|
with ``replace=True`` or :meth:`remove_record` to make changes. |
|
|
|
|
|
|
|
|
|
|
|
:return: True if hydration occurred, False if already hydrated. |
|
|
|
|
|
:rtype: bool |
|
|
|
|
|
|
|
|
|
|
|
.. note:: |
|
|
|
|
|
This method is automatically called by :meth:`add_record` and |
|
|
|
|
|
:meth:`remove_record` when needed, so manual calls are rarely necessary. |
|
|
|
|
|
|
|
|
|
|
|
.. important:: |
|
|
|
|
|
- Only hydrates if this is a shallow copy (has an ``_origin``) |
|
|
|
|
|
- Clears the ``_origin`` reference after hydration |
|
|
|
|
|
- Uses ``lenient=True`` when adding records from origin |
|
|
|
|
|
- Records are still shared with the origin (not deep copied) |
|
|
''' |
|
|
''' |
|
|
origin = self._origin |
|
|
origin = self._origin |
|
|
if origin is None: |
|
|
if origin is None: |
|
|
@ -383,14 +643,33 @@ class Zone(object): |
|
|
|
|
|
|
|
|
def copy(self): |
|
|
def copy(self): |
|
|
''' |
|
|
''' |
|
|
Copy-on-write semantics support. This method will create a shallow |
|
|
|
|
|
clone of the zone which will be hydrated the first time `add_record` or |
|
|
|
|
|
`remove_record` is called. |
|
|
|
|
|
|
|
|
|
|
|
This allows low-cost copies of things to be made in situations where |
|
|
|
|
|
changes are unlikely and only incurs the "expense" of actually |
|
|
|
|
|
copying the records when required. The actual record copy will not be |
|
|
|
|
|
"deep" meaning that records should not be modified directly. |
|
|
|
|
|
|
|
|
Create a shallow copy of this zone using copy-on-write semantics. |
|
|
|
|
|
|
|
|
|
|
|
Creates a new zone that shares records with this zone until the copy |
|
|
|
|
|
is modified. When :meth:`add_record` or :meth:`remove_record` is called |
|
|
|
|
|
on the copy, it will be automatically hydrated with its own record |
|
|
|
|
|
references. |
|
|
|
|
|
|
|
|
|
|
|
:return: A shallow copy of this zone. |
|
|
|
|
|
:rtype: Zone |
|
|
|
|
|
|
|
|
|
|
|
.. important:: |
|
|
|
|
|
- The copy shares records with the original until hydrated |
|
|
|
|
|
- Hydration happens automatically on first modification |
|
|
|
|
|
- Records in the hydrated copy are still the same objects (not deep copied) |
|
|
|
|
|
- Modifying records directly affects both zones; use ``record.copy()`` |
|
|
|
|
|
and ``add_record(..., replace=True)`` instead |
|
|
|
|
|
|
|
|
|
|
|
Example:: |
|
|
|
|
|
|
|
|
|
|
|
original = Zone('example.com.', []) |
|
|
|
|
|
# ... add records to original ... |
|
|
|
|
|
|
|
|
|
|
|
copy = original.copy() # Shallow copy, shares records |
|
|
|
|
|
# No copying has occurred yet |
|
|
|
|
|
|
|
|
|
|
|
copy.add_record(new_record) # Triggers hydration, copies record refs |
|
|
|
|
|
# Now copy has its own record references |
|
|
''' |
|
|
''' |
|
|
copy = Zone( |
|
|
copy = Zone( |
|
|
self.name, |
|
|
self.name, |
|
|
@ -402,4 +681,10 @@ class Zone(object): |
|
|
return copy |
|
|
return copy |
|
|
|
|
|
|
|
|
def __repr__(self): |
|
|
def __repr__(self): |
|
|
|
|
|
''' |
|
|
|
|
|
Return a string representation of this zone. |
|
|
|
|
|
|
|
|
|
|
|
:return: String in the format ``Zone<zone_name>`` using the decoded name. |
|
|
|
|
|
:rtype: str |
|
|
|
|
|
''' |
|
|
return f'Zone<{self.decoded_name}>' |
|
|
return f'Zone<{self.decoded_name}>' |