"""
Demonstration of Parnas' Information Hiding Principle
Design Decision Hidden: How events/telemetry are processed and stored
- The Events class hides the details of which sinks are registered
- Each sink implementation hides its specific processing strategy
- Changes to logging format, storage, or filtering don't affect clients
"""
from typing import Any, Tuple
import logging
from abc import ABC, abstractmethod
class TelemetrySink(ABC):
"""Interface that hides implementation details of telemetry handling"""
@abstractmethod
def handle(self, *args):
# type: (Tuple[str, Any]) -> None
"""Process telemetry with key-value pairs"""
pass
class StructuredPythonLogger(TelemetrySink):
"""Implementation that hides Python logger integration details"""
class _KeyValueFormatter(logging.Formatter):
"""Internal formatter that outputs structured key=value pairs"""
def format(self, record):
# type: (logging.LogRecord) -> str
# The structured data is passed via extra
if hasattr(record, 'structured_data'):
parts = ["{0}={1}".format(k, v) for k, v in record.structured_data.items()]
structured_msg = " ".join(parts)
record.msg = structured_msg
return super(StructuredPythonLogger._KeyValueFormatter, self).format(record)
def __init__(self, logger):
# type: (logging.Logger) -> None
self._logger = logger # Hidden implementation detail
def handle(self, *args):
# type: (Tuple[str, Any]) -> None
event_data = dict(args)
# Use log level if provided, otherwise INFO
level = str(event_data.get('level', 'INFO')).upper()
# Pass structured data to logger via extra
if level == 'ERROR':
self._logger.error('', extra={'structured_data': event_data})
elif level == 'WARNING':
self._logger.warning('', extra={'structured_data': event_data})
elif level == 'DEBUG':
self._logger.debug('', extra={'structured_data': event_data})
else:
self._logger.info('', extra={'structured_data': event_data})
class StructuredMetricsCollector(TelemetrySink):
"""Implementation that hides metrics aggregation strategy"""
def __init__(self):
self._metrics = {} # Hidden implementation detail
def handle(self, *args):
# type: (Tuple[str, Any]) -> None
event_data = dict(args)
# Hidden decision: only track events with 'metric' key
if 'metric' in event_data:
metric_name = event_data['metric']
self._metrics[metric_name] = self._metrics.get(metric_name, 0) + 1
def get_metrics(self):
# type: () -> dict
"""Expose metrics without revealing internal storage"""
return self._metrics. copy()
class Events:
"""
Facade that hides the complexity of multi-sink telemetry processing
Design Decisions Hidden:
- Which sinks are registered
- How many sinks exist
- Order of sink execution
- Sink lifecycle management
"""
def __init__(self):
self._sinks = [] # type: list # Hidden detail
def register(self, sink):
# type: (TelemetrySink) -> None
"""Add sink without exposing internal collection"""
self._sinks.append(sink)
def emit(self, *args):
# type: (Tuple[str, Any]) -> None
"""
Delegate event to all sinks
Client doesn't need to know:
- How many sinks exist
- What each sink does
- If sinks can be added/removed at runtime
"""
for sink in self._sinks:
sink.handle(*args)
def main():
"""
Runnable example demonstrating information hiding
"""
# Configure logging system with custom structured formatter
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
handler.setFormatter(StructuredPythonLogger._KeyValueFormatter('%(levelname)s - %(message)s'))
# Configure root logger
root_logger = logging.getLogger()
root_logger.setLevel(logging.DEBUG)
root_logger.addHandler(handler)
# Get logger for the telemetry system
logger = logging.getLogger('app.telemetry')
# Setup telemetry system (design decisions hidden)
events = Events()
events.register(StructuredPythonLogger(logger))
metrics = StructuredMetricsCollector()
events.register(metrics)
# Use logger for demo headers
demo_logger = logging.getLogger('demo')
demo_logger.info("=== Telemetry Events Demo ===")
# Client code: doesn't know or care about sink implementations
events.emit(
('level', 'INFO'),
('event', 'app_start'),
('user', 'd-led'),
('version', '1.0.0'),
('metric', 'app_start')
)
events.emit(
('level', 'DEBUG'),
('event', 'db_query'),
('query_time_ms', 45),
('table', 'users'),
('rows', 150),
('metric', 'db_query')
)
events.emit(
('level', 'ERROR'),
('event', 'connection_failed'),
('service', 'database'),
('retry_count', 3),
('error_code', 'CONN_TIMEOUT'),
('metric', 'db_error')
)
events.emit(
('level', 'WARNING'),
('event', 'cache_miss'),
('key', 'user: 12345'),
('fallback', 'database'),
('metric', 'cache_miss')
)
events.emit(
('level', 'INFO'),
('event', 'request_completed'),
('endpoint', '/api/users'),
('duration_ms', 234),
('status', 200),
('metric', 'request_completed')
)
# More events to make metrics interesting
events. emit(
('level', 'ERROR'),
('event', 'connection_failed'),
('service', 'cache'),
('retry_count', 2),
('metric', 'db_error')
)
events.emit(
('level', 'DEBUG'),
('event', 'db_query'),
('query_time_ms', 23),
('table', 'products'),
('rows', 50),
('metric', 'db_query')
)
events.emit(
('level', 'INFO'),
('event', 'request_completed'),
('endpoint', '/api/products'),
('duration_ms', 156),
('status', 200),
('metric', 'request_completed')
)
# Show collected metrics
demo_logger.info("=== Metrics Summary ===")
for metric, count in metrics.get_metrics().items():
demo_logger.info("{0}: {1}".format(metric, count))
if __name__ == "__main__":
main()
IiIiCkRlbW9uc3RyYXRpb24gb2YgUGFybmFzJyBJbmZvcm1hdGlvbiBIaWRpbmcgUHJpbmNpcGxlCgpEZXNpZ24gRGVjaXNpb24gSGlkZGVuOiAgICBIb3cgZXZlbnRzL3RlbGVtZXRyeSBhcmUgcHJvY2Vzc2VkIGFuZCBzdG9yZWQKLSBUaGUgRXZlbnRzIGNsYXNzIGhpZGVzIHRoZSBkZXRhaWxzIG9mIHdoaWNoIHNpbmtzIGFyZSByZWdpc3RlcmVkCi0gRWFjaCBzaW5rIGltcGxlbWVudGF0aW9uIGhpZGVzIGl0cyBzcGVjaWZpYyBwcm9jZXNzaW5nIHN0cmF0ZWd5Ci0gQ2hhbmdlcyB0byBsb2dnaW5nIGZvcm1hdCwgc3RvcmFnZSwgb3IgZmlsdGVyaW5nIGRvbid0IGFmZmVjdCBjbGllbnRzCiIiIgoKZnJvbSB0eXBpbmcgaW1wb3J0IEFueSwgVHVwbGUKaW1wb3J0IGxvZ2dpbmcKZnJvbSBhYmMgaW1wb3J0IEFCQywgYWJzdHJhY3RtZXRob2QKCgpjbGFzcyBUZWxlbWV0cnlTaW5rKEFCQyk6CiAgICAiIiJJbnRlcmZhY2UgdGhhdCBoaWRlcyBpbXBsZW1lbnRhdGlvbiBkZXRhaWxzIG9mIHRlbGVtZXRyeSBoYW5kbGluZyIiIgogICAgCiAgICBAYWJzdHJhY3RtZXRob2QKICAgIGRlZiBoYW5kbGUoc2VsZiwgKmFyZ3MpOgogICAgICAgICMgdHlwZTogKFR1cGxlW3N0ciwgQW55XSkgLT4gTm9uZQogICAgICAgICIiIlByb2Nlc3MgdGVsZW1ldHJ5IHdpdGgga2V5LXZhbHVlIHBhaXJzIiIiCiAgICAgICAgcGFzcwoKCmNsYXNzIFN0cnVjdHVyZWRQeXRob25Mb2dnZXIoVGVsZW1ldHJ5U2luayk6CiAgICAiIiJJbXBsZW1lbnRhdGlvbiB0aGF0IGhpZGVzIFB5dGhvbiBsb2dnZXIgaW50ZWdyYXRpb24gZGV0YWlscyIiIgogICAgCiAgICBjbGFzcyBfS2V5VmFsdWVGb3JtYXR0ZXIobG9nZ2luZy5Gb3JtYXR0ZXIpOgogICAgICAgICIiIkludGVybmFsIGZvcm1hdHRlciB0aGF0IG91dHB1dHMgc3RydWN0dXJlZCBrZXk9dmFsdWUgcGFpcnMiIiIKICAgICAgICAKICAgICAgICBkZWYgZm9ybWF0KHNlbGYsIHJlY29yZCk6CiAgICAgICAgICAgICMgdHlwZTogKGxvZ2dpbmcuTG9nUmVjb3JkKSAtPiBzdHIKICAgICAgICAgICAgIyBUaGUgc3RydWN0dXJlZCBkYXRhIGlzIHBhc3NlZCB2aWEgZXh0cmEKICAgICAgICAgICAgaWYgaGFzYXR0cihyZWNvcmQsICdzdHJ1Y3R1cmVkX2RhdGEnKToKICAgICAgICAgICAgICAgIHBhcnRzID0gWyJ7MH09ezF9Ii5mb3JtYXQoaywgdikgZm9yIGssIHYgaW4gcmVjb3JkLnN0cnVjdHVyZWRfZGF0YS5pdGVtcygpXQogICAgICAgICAgICAgICAgc3RydWN0dXJlZF9tc2cgPSAiICIuam9pbihwYXJ0cykKICAgICAgICAgICAgICAgIHJlY29yZC5tc2cgPSBzdHJ1Y3R1cmVkX21zZwogICAgICAgICAgICByZXR1cm4gc3VwZXIoU3RydWN0dXJlZFB5dGhvbkxvZ2dlci5fS2V5VmFsdWVGb3JtYXR0ZXIsIHNlbGYpLmZvcm1hdChyZWNvcmQpCiAgICAKICAgIGRlZiBfX2luaXRfXyhzZWxmLCBsb2dnZXIpOgogICAgICAgICMgdHlwZTogKGxvZ2dpbmcuTG9nZ2VyKSAtPiBOb25lCiAgICAgICAgc2VsZi5fbG9nZ2VyID0gbG9nZ2VyICAjIEhpZGRlbiBpbXBsZW1lbnRhdGlvbiBkZXRhaWwKICAgIAogICAgZGVmIGhhbmRsZShzZWxmLCAqYXJncyk6CiAgICAgICAgIyB0eXBlOiAoVHVwbGVbc3RyLCBBbnldKSAtPiBOb25lCiAgICAgICAgZXZlbnRfZGF0YSA9IGRpY3QoYXJncykKICAgICAgICAKICAgICAgICAjIFVzZSBsb2cgbGV2ZWwgaWYgcHJvdmlkZWQsIG90aGVyd2lzZSBJTkZPCiAgICAgICAgbGV2ZWwgPSBzdHIoZXZlbnRfZGF0YS5nZXQoJ2xldmVsJywgJ0lORk8nKSkudXBwZXIoKQogICAgICAgIAogICAgICAgICMgUGFzcyBzdHJ1Y3R1cmVkIGRhdGEgdG8gbG9nZ2VyIHZpYSBleHRyYQogICAgICAgIGlmIGxldmVsID09ICdFUlJPUic6CiAgICAgICAgICAgIHNlbGYuX2xvZ2dlci5lcnJvcignJywgZXh0cmE9eydzdHJ1Y3R1cmVkX2RhdGEnOiBldmVudF9kYXRhfSkKICAgICAgICBlbGlmIGxldmVsID09ICdXQVJOSU5HJzoKICAgICAgICAgICAgc2VsZi5fbG9nZ2VyLndhcm5pbmcoJycsIGV4dHJhPXsnc3RydWN0dXJlZF9kYXRhJzogIGV2ZW50X2RhdGF9KQogICAgICAgIGVsaWYgbGV2ZWwgPT0gJ0RFQlVHJzoKICAgICAgICAgICAgc2VsZi5fbG9nZ2VyLmRlYnVnKCcnLCBleHRyYT17J3N0cnVjdHVyZWRfZGF0YSc6ICBldmVudF9kYXRhfSkKICAgICAgICBlbHNlOgogICAgICAgICAgICBzZWxmLl9sb2dnZXIuaW5mbygnJywgZXh0cmE9eydzdHJ1Y3R1cmVkX2RhdGEnOiBldmVudF9kYXRhfSkKCgpjbGFzcyBTdHJ1Y3R1cmVkTWV0cmljc0NvbGxlY3RvcihUZWxlbWV0cnlTaW5rKToKICAgICIiIkltcGxlbWVudGF0aW9uIHRoYXQgaGlkZXMgbWV0cmljcyBhZ2dyZWdhdGlvbiBzdHJhdGVneSIiIgogICAgCiAgICBkZWYgX19pbml0X18oc2VsZik6CiAgICAgICAgc2VsZi5fbWV0cmljcyA9IHt9ICAjIEhpZGRlbiBpbXBsZW1lbnRhdGlvbiBkZXRhaWwKICAgIAogICAgZGVmIGhhbmRsZShzZWxmLCAqYXJncyk6CiAgICAgICAgIyB0eXBlOiAoVHVwbGVbc3RyLCBBbnldKSAtPiBOb25lCiAgICAgICAgZXZlbnRfZGF0YSA9IGRpY3QoYXJncykKICAgICAgICAKICAgICAgICAjIEhpZGRlbiBkZWNpc2lvbjogb25seSB0cmFjayBldmVudHMgd2l0aCAnbWV0cmljJyBrZXkKICAgICAgICBpZiAnbWV0cmljJyBpbiBldmVudF9kYXRhOgogICAgICAgICAgICBtZXRyaWNfbmFtZSA9IGV2ZW50X2RhdGFbJ21ldHJpYyddCiAgICAgICAgICAgIHNlbGYuX21ldHJpY3NbbWV0cmljX25hbWVdID0gc2VsZi5fbWV0cmljcy5nZXQobWV0cmljX25hbWUsIDApICsgMQogICAgCiAgICBkZWYgZ2V0X21ldHJpY3Moc2VsZik6CiAgICAgICAgIyB0eXBlOiAoKSAtPiBkaWN0CiAgICAgICAgIiIiRXhwb3NlIG1ldHJpY3Mgd2l0aG91dCByZXZlYWxpbmcgaW50ZXJuYWwgc3RvcmFnZSIiIgogICAgICAgIHJldHVybiBzZWxmLl9tZXRyaWNzLiBjb3B5KCkKCgpjbGFzcyBFdmVudHM6CiAgICAiIiIKICAgIEZhY2FkZSB0aGF0IGhpZGVzIHRoZSBjb21wbGV4aXR5IG9mIG11bHRpLXNpbmsgdGVsZW1ldHJ5IHByb2Nlc3NpbmcKICAgIAogICAgRGVzaWduIERlY2lzaW9ucyBIaWRkZW46CiAgICAtIFdoaWNoIHNpbmtzIGFyZSByZWdpc3RlcmVkCiAgICAtIEhvdyBtYW55IHNpbmtzIGV4aXN0CiAgICAtIE9yZGVyIG9mIHNpbmsgZXhlY3V0aW9uCiAgICAtIFNpbmsgbGlmZWN5Y2xlIG1hbmFnZW1lbnQKICAgICIiIgogICAgCiAgICBkZWYgX19pbml0X18oc2VsZik6CiAgICAgICAgc2VsZi5fc2lua3MgPSBbXSAgIyB0eXBlOiBsaXN0ICAjIEhpZGRlbiBkZXRhaWwKICAgIAogICAgZGVmIHJlZ2lzdGVyKHNlbGYsIHNpbmspOgogICAgICAgICMgdHlwZTogKFRlbGVtZXRyeVNpbmspIC0+IE5vbmUKICAgICAgICAiIiJBZGQgc2luayB3aXRob3V0IGV4cG9zaW5nIGludGVybmFsIGNvbGxlY3Rpb24iIiIKICAgICAgICBzZWxmLl9zaW5rcy5hcHBlbmQoc2luaykKICAgIAogICAgZGVmIGVtaXQoc2VsZiwgKmFyZ3MpOgogICAgICAgICMgdHlwZTogKFR1cGxlW3N0ciwgQW55XSkgLT4gTm9uZQogICAgICAgICIiIgogICAgICAgIERlbGVnYXRlIGV2ZW50IHRvIGFsbCBzaW5rcwogICAgICAgIAogICAgICAgIENsaWVudCBkb2Vzbid0IG5lZWQgdG8ga25vdzogCiAgICAgICAgLSBIb3cgbWFueSBzaW5rcyBleGlzdAogICAgICAgIC0gV2hhdCBlYWNoIHNpbmsgZG9lcwogICAgICAgIC0gSWYgc2lua3MgY2FuIGJlIGFkZGVkL3JlbW92ZWQgYXQgcnVudGltZQogICAgICAgICIiIgogICAgICAgIGZvciBzaW5rIGluIHNlbGYuX3NpbmtzOgogICAgICAgICAgICBzaW5rLmhhbmRsZSgqYXJncykKCgpkZWYgbWFpbigpOgogICAgIiIiCiAgICBSdW5uYWJsZSBleGFtcGxlIGRlbW9uc3RyYXRpbmcgaW5mb3JtYXRpb24gaGlkaW5nCiAgICAiIiIKICAgICMgQ29uZmlndXJlIGxvZ2dpbmcgc3lzdGVtIHdpdGggY3VzdG9tIHN0cnVjdHVyZWQgZm9ybWF0dGVyCiAgICBoYW5kbGVyID0gbG9nZ2luZy5TdHJlYW1IYW5kbGVyKCkKICAgIGhhbmRsZXIuc2V0TGV2ZWwobG9nZ2luZy5ERUJVRykKICAgIGhhbmRsZXIuc2V0Rm9ybWF0dGVyKFN0cnVjdHVyZWRQeXRob25Mb2dnZXIuX0tleVZhbHVlRm9ybWF0dGVyKCclKGxldmVsbmFtZSlzIC0gJShtZXNzYWdlKXMnKSkKICAgIAogICAgIyBDb25maWd1cmUgcm9vdCBsb2dnZXIKICAgIHJvb3RfbG9nZ2VyID0gbG9nZ2luZy5nZXRMb2dnZXIoKQogICAgcm9vdF9sb2dnZXIuc2V0TGV2ZWwobG9nZ2luZy5ERUJVRykKICAgIHJvb3RfbG9nZ2VyLmFkZEhhbmRsZXIoaGFuZGxlcikKICAgIAogICAgIyBHZXQgbG9nZ2VyIGZvciB0aGUgdGVsZW1ldHJ5IHN5c3RlbQogICAgbG9nZ2VyID0gbG9nZ2luZy5nZXRMb2dnZXIoJ2FwcC50ZWxlbWV0cnknKQogICAgCiAgICAjIFNldHVwIHRlbGVtZXRyeSBzeXN0ZW0gKGRlc2lnbiBkZWNpc2lvbnMgaGlkZGVuKQogICAgZXZlbnRzID0gRXZlbnRzKCkKICAgIGV2ZW50cy5yZWdpc3RlcihTdHJ1Y3R1cmVkUHl0aG9uTG9nZ2VyKGxvZ2dlcikpCiAgICAKICAgIG1ldHJpY3MgPSBTdHJ1Y3R1cmVkTWV0cmljc0NvbGxlY3RvcigpCiAgICBldmVudHMucmVnaXN0ZXIobWV0cmljcykKICAgIAogICAgIyBVc2UgbG9nZ2VyIGZvciBkZW1vIGhlYWRlcnMKICAgIGRlbW9fbG9nZ2VyID0gbG9nZ2luZy5nZXRMb2dnZXIoJ2RlbW8nKQogICAgZGVtb19sb2dnZXIuaW5mbygiPT09IFRlbGVtZXRyeSBFdmVudHMgRGVtbyA9PT0iKQogICAgCiAgICAjIENsaWVudCBjb2RlOiAgZG9lc24ndCBrbm93IG9yIGNhcmUgYWJvdXQgc2luayBpbXBsZW1lbnRhdGlvbnMKICAgIGV2ZW50cy5lbWl0KAogICAgICAgICgnbGV2ZWwnLCAnSU5GTycpLAogICAgICAgICgnZXZlbnQnLCAnYXBwX3N0YXJ0JyksCiAgICAgICAgKCd1c2VyJywgJ2QtbGVkJyksCiAgICAgICAgKCd2ZXJzaW9uJywgJzEuMC4wJyksCiAgICAgICAgKCdtZXRyaWMnLCAnYXBwX3N0YXJ0JykKICAgICkKICAgIAogICAgZXZlbnRzLmVtaXQoCiAgICAgICAgKCdsZXZlbCcsICdERUJVRycpLAogICAgICAgICgnZXZlbnQnLCAnZGJfcXVlcnknKSwKICAgICAgICAoJ3F1ZXJ5X3RpbWVfbXMnLCA0NSksCiAgICAgICAgKCd0YWJsZScsICd1c2VycycpLAogICAgICAgICgncm93cycsIDE1MCksCiAgICAgICAgKCdtZXRyaWMnLCAnZGJfcXVlcnknKQogICAgKQogICAgCiAgICBldmVudHMuZW1pdCgKICAgICAgICAoJ2xldmVsJywgJ0VSUk9SJyksCiAgICAgICAgKCdldmVudCcsICdjb25uZWN0aW9uX2ZhaWxlZCcpLAogICAgICAgICgnc2VydmljZScsICdkYXRhYmFzZScpLAogICAgICAgICgncmV0cnlfY291bnQnLCAzKSwKICAgICAgICAoJ2Vycm9yX2NvZGUnLCAnQ09OTl9USU1FT1VUJyksCiAgICAgICAgKCdtZXRyaWMnLCAnZGJfZXJyb3InKQogICAgKQogICAgCiAgICBldmVudHMuZW1pdCgKICAgICAgICAoJ2xldmVsJywgJ1dBUk5JTkcnKSwKICAgICAgICAoJ2V2ZW50JywgJ2NhY2hlX21pc3MnKSwKICAgICAgICAoJ2tleScsICd1c2VyOiAxMjM0NScpLAogICAgICAgICgnZmFsbGJhY2snLCAnZGF0YWJhc2UnKSwKICAgICAgICAoJ21ldHJpYycsICdjYWNoZV9taXNzJykKICAgICkKICAgIAogICAgZXZlbnRzLmVtaXQoCiAgICAgICAgKCdsZXZlbCcsICdJTkZPJyksCiAgICAgICAgKCdldmVudCcsICdyZXF1ZXN0X2NvbXBsZXRlZCcpLAogICAgICAgICgnZW5kcG9pbnQnLCAnL2FwaS91c2VycycpLAogICAgICAgICgnZHVyYXRpb25fbXMnLCAyMzQpLAogICAgICAgICgnc3RhdHVzJywgMjAwKSwKICAgICAgICAoJ21ldHJpYycsICdyZXF1ZXN0X2NvbXBsZXRlZCcpCiAgICApCiAgICAKICAgICMgTW9yZSBldmVudHMgdG8gbWFrZSBtZXRyaWNzIGludGVyZXN0aW5nCiAgICBldmVudHMuIGVtaXQoCiAgICAgICAgKCdsZXZlbCcsICdFUlJPUicpLAogICAgICAgICgnZXZlbnQnLCAnY29ubmVjdGlvbl9mYWlsZWQnKSwKICAgICAgICAoJ3NlcnZpY2UnLCAnY2FjaGUnKSwKICAgICAgICAoJ3JldHJ5X2NvdW50JywgMiksCiAgICAgICAgKCdtZXRyaWMnLCAnZGJfZXJyb3InKQogICAgKQogICAgCiAgICBldmVudHMuZW1pdCgKICAgICAgICAoJ2xldmVsJywgJ0RFQlVHJyksCiAgICAgICAgKCdldmVudCcsICdkYl9xdWVyeScpLAogICAgICAgICgncXVlcnlfdGltZV9tcycsIDIzKSwKICAgICAgICAoJ3RhYmxlJywgJ3Byb2R1Y3RzJyksCiAgICAgICAgKCdyb3dzJywgNTApLAogICAgICAgICgnbWV0cmljJywgJ2RiX3F1ZXJ5JykKICAgICkKICAgIAogICAgZXZlbnRzLmVtaXQoCiAgICAgICAgKCdsZXZlbCcsICdJTkZPJyksCiAgICAgICAgKCdldmVudCcsICdyZXF1ZXN0X2NvbXBsZXRlZCcpLAogICAgICAgICgnZW5kcG9pbnQnLCAnL2FwaS9wcm9kdWN0cycpLAogICAgICAgICgnZHVyYXRpb25fbXMnLCAxNTYpLAogICAgICAgICgnc3RhdHVzJywgMjAwKSwKICAgICAgICAoJ21ldHJpYycsICdyZXF1ZXN0X2NvbXBsZXRlZCcpCiAgICApCiAgICAKICAgICMgU2hvdyBjb2xsZWN0ZWQgbWV0cmljcwogICAgZGVtb19sb2dnZXIuaW5mbygiPT09IE1ldHJpY3MgU3VtbWFyeSA9PT0iKQogICAgZm9yIG1ldHJpYywgY291bnQgaW4gbWV0cmljcy5nZXRfbWV0cmljcygpLml0ZW1zKCk6CiAgICAgICAgZGVtb19sb2dnZXIuaW5mbygiezB9OiAgezF9Ii5mb3JtYXQobWV0cmljLCBjb3VudCkpCgoKaWYgX19uYW1lX18gPT0gIl9fbWFpbl9fIjoKICAgIG1haW4oKQ==