File: prettyprint.py

package info (click to toggle)
python-eliot 1.16.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 964 kB
  • sloc: python: 8,641; makefile: 151
file content (173 lines) | stat: -rw-r--r-- 5,050 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
"""
API and command-line support for human-readable Eliot messages.
"""

import pprint
import argparse
from datetime import datetime
from sys import stdin, stdout
from collections import OrderedDict
from json import dumps

from json import loads

from ._message import (
    TIMESTAMP_FIELD,
    TASK_UUID_FIELD,
    TASK_LEVEL_FIELD,
    MESSAGE_TYPE_FIELD,
)
from ._action import ACTION_TYPE_FIELD, ACTION_STATUS_FIELD


# Ensure binary stdin, since we expect specifically UTF-8 encoded
# messages, not platform-encoding messages.
stdin = stdin.buffer


# Fields that all Eliot messages are expected to have:
REQUIRED_FIELDS = {TASK_LEVEL_FIELD, TASK_UUID_FIELD, TIMESTAMP_FIELD}

# Fields that get treated specially when formatting.
_skip_fields = {
    TIMESTAMP_FIELD,
    TASK_UUID_FIELD,
    TASK_LEVEL_FIELD,
    MESSAGE_TYPE_FIELD,
    ACTION_TYPE_FIELD,
    ACTION_STATUS_FIELD,
}

# First fields to render:
_first_fields = [ACTION_TYPE_FIELD, MESSAGE_TYPE_FIELD, ACTION_STATUS_FIELD]


def _render_timestamp(message: dict, local_timezone: bool) -> str:
    """Convert a message's timestamp to a string."""
    # If we were returning or storing the datetime we'd want to use an
    # explicit timezone instead of a naive datetime, but since we're
    # just using it for formatting we needn't bother.
    if local_timezone:
        dt = datetime.fromtimestamp(message[TIMESTAMP_FIELD])
    else:
        dt = datetime.utcfromtimestamp(message[TIMESTAMP_FIELD])
    result = dt.isoformat(sep="T")
    if not local_timezone:
        result += "Z"
    return result


def pretty_format(message: dict, local_timezone: bool = False) -> str:
    """
    Convert a message dictionary into a human-readable string.

    @param message: Message to parse, as dictionary.

    @return: Unicode string.
    """

    def add_field(previous, key, value):
        value = (
            pprint.pformat(value, width=40).replace("\\n", "\n ").replace("\\t", "\t")
        )
        # Reindent second line and later to match up with first line's
        # indentation:
        lines = value.split("\n")
        # indent lines are "  <key length>|  <value>"
        indent = "{}| ".format(" " * (2 + len(key)))
        value = "\n".join([lines[0]] + [indent + l for l in lines[1:]])
        return "  %s: %s\n" % (key, value)

    remaining = ""
    for field in _first_fields:
        if field in message:
            remaining += add_field(remaining, field, message[field])
    for key, value in sorted(message.items()):
        if key not in _skip_fields:
            remaining += add_field(remaining, key, value)

    level = "/" + "/".join(map(str, message[TASK_LEVEL_FIELD]))
    return "%s -> %s\n%s\n%s" % (
        message[TASK_UUID_FIELD],
        level,
        _render_timestamp(message, local_timezone),
        remaining,
    )


def compact_format(message: dict, local_timezone: bool = False) -> str:
    """Format an Eliot message into a single line.

    The message is presumed to be JSON-serializable.
    """
    ordered_message = OrderedDict()
    for field in _first_fields:
        if field in message:
            ordered_message[field] = message[field]
    for key, value in sorted(message.items()):
        if key not in _skip_fields:
            ordered_message[key] = value
    # drop { and } from JSON:
    rendered = " ".join(
        "{}={}".format(key, dumps(value, separators=(",", ":")))
        for (key, value) in ordered_message.items()
    )

    return "%s%s %s %s" % (
        message[TASK_UUID_FIELD],
        "/" + "/".join(map(str, message[TASK_LEVEL_FIELD])),
        _render_timestamp(message, local_timezone),
        rendered,
    )


_CLI_HELP = """\
Convert Eliot messages into more readable format.

Reads JSON lines from stdin, write out pretty-printed results on stdout.
"""


def _main():
    """
    Command-line program that reads in JSON from stdin and writes out
    pretty-printed messages to stdout.
    """
    parser = argparse.ArgumentParser(
        description=_CLI_HELP, usage="cat messages | %(prog)s [options]"
    )
    parser.add_argument(
        "-c",
        "--compact",
        action="store_true",
        dest="compact",
        help="Compact format, one message per line.",
    )
    parser.add_argument(
        "-l",
        "--local-timezone",
        action="store_true",
        dest="local_timezone",
        help="Use local timezone instead of UTC.",
    )

    args = parser.parse_args()
    if args.compact:
        formatter = compact_format
    else:
        formatter = pretty_format

    for line in stdin:
        try:
            message = loads(line)
        except ValueError:
            stdout.write("Not JSON: {}\n\n".format(line.rstrip(b"\n")))
            continue
        if REQUIRED_FIELDS - set(message.keys()):
            stdout.write("Not an Eliot message: {}\n\n".format(line.rstrip(b"\n")))
            continue
        result = formatter(message, args.local_timezone) + "\n"
        stdout.write(result)


__all__ = ["pretty_format", "compact_format"]