Skip to content

Commit d93f906

Browse files
authored
Merge pull request #12 from Code-Partners/feat-text-protocol
Feat text protocol
2 parents 9e859f5 + c6a9c6a commit d93f906

File tree

16 files changed

+518
-4
lines changed

16 files changed

+518
-4
lines changed

common/level.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,7 @@ class Level(Enum):
99
ERROR = 4
1010
FATAL = 5
1111
CONTROL = 6
12+
13+
def __str__(self):
14+
return "%s" % self._name_
15+

common/pattern_parser.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import logging
2+
import typing
3+
4+
from typing import List
5+
6+
from common.tokens.token_abc import Token
7+
from common.tokens.token_factory import TokenFactory
8+
from packets.log_entry.log_entry import LogEntry, LogEntryType
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
class PatternParser:
14+
_SPACES: str = " " * 3
15+
16+
def __init__(self) -> None:
17+
self._tokens: List[Token] = list()
18+
self._buffer: list = []
19+
self._pattern: str = str()
20+
self._indent_level: int = 0
21+
self._indent: bool = False
22+
self._position = 0
23+
24+
def expand(self, log_entry: LogEntry) -> str:
25+
size = len(self._tokens)
26+
27+
if size == 0:
28+
return ""
29+
30+
self._buffer.clear()
31+
if log_entry.log_entry_type == LogEntryType.LEAVE_METHOD:
32+
if self._indent_level > 0:
33+
self._indent_level -= 1
34+
logger.debug("Decreased indent level when leaving method, new indent level is %d" % self._indent_level)
35+
36+
for token in self._tokens:
37+
token: Token
38+
if self.indent and token.indent:
39+
for i in range(self._indent_level):
40+
self._buffer.append(self._SPACES)
41+
42+
expanded = token.expand(log_entry)
43+
width = token.width
44+
45+
if width < 0:
46+
# left-aligned
47+
self._buffer.append(expanded)
48+
pad = -width - len(expanded)
49+
for i in range(pad):
50+
self._buffer.append(" ")
51+
elif width > 0:
52+
pad = width - len(expanded)
53+
for i in range(pad):
54+
self._buffer.append(" ")
55+
# right-aligned
56+
self._buffer.append(expanded)
57+
else:
58+
self._buffer.append(expanded)
59+
if log_entry.log_entry_type == LogEntryType.ENTER_METHOD:
60+
self._indent_level += 1
61+
logger.debug("Added indent level when entering method, new indent level is %d" % self._indent_level)
62+
63+
return "".join(self._buffer)
64+
65+
def _next(self) -> typing.Optional[Token]:
66+
length = len(self._pattern)
67+
if self._position < length:
68+
is_variable = False
69+
pos: int = self._position
70+
71+
if self._pattern[pos] == "$":
72+
is_variable = True
73+
pos += 1
74+
75+
while pos < length:
76+
if self._pattern[pos] == "$":
77+
if is_variable:
78+
pos += 1
79+
break
80+
pos += 1
81+
82+
value = self._pattern[self._position: pos]
83+
self._position = pos
84+
85+
return TokenFactory.get_token(value)
86+
else:
87+
return None
88+
89+
def _parse(self) -> None:
90+
self._tokens.clear()
91+
token = self._next()
92+
while token is not None:
93+
self._tokens.append(token)
94+
token = self._next()
95+
96+
@property
97+
def pattern(self) -> str:
98+
return self._pattern
99+
100+
@pattern.setter
101+
def pattern(self, pattern: str) -> None:
102+
if not isinstance(pattern, str):
103+
raise TypeError("pattern must be an str")
104+
self._position = 0
105+
self._pattern = pattern.strip() if pattern else ""
106+
self._parse()
107+
108+
@property
109+
def indent(self) -> bool:
110+
return self._indent
111+
112+
@indent.setter
113+
def indent(self, indent: bool) -> None:
114+
if not isinstance(indent, bool):
115+
raise TypeError("indent must be a bool")
116+
117+
self._indent = indent

common/tokens/__init__.py

Whitespace-only changes.

common/tokens/token_abc.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from abc import ABC, abstractmethod
2+
3+
from packets.log_entry.log_entry import LogEntry
4+
5+
6+
class Token(ABC):
7+
_value: str
8+
_options: str
9+
_width: int
10+
11+
@staticmethod
12+
@abstractmethod
13+
def expand(log_entry: LogEntry) -> str:
14+
...
15+
16+
@property
17+
def value(self) -> str:
18+
return self._value
19+
20+
@value.setter
21+
def value(self, value: str) -> None:
22+
if not isinstance(value, str):
23+
raise TypeError("value must be an str")
24+
self._value = value
25+
26+
@property
27+
def options(self) -> str:
28+
return self._options
29+
30+
@options.setter
31+
def options(self, options: str) -> None:
32+
if not isinstance(options, str):
33+
raise TypeError("options must be an str")
34+
self._options = options
35+
36+
@property
37+
def indent(self) -> bool:
38+
return False
39+
40+
@property
41+
def width(self) -> int:
42+
return self._width
43+
44+
@width.setter
45+
def width(self, width: int) -> None:
46+
if not isinstance(width, int):
47+
raise TypeError("width must be an int")
48+
self._width = width

common/tokens/token_factory.py

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from .tokens import *
2+
from .token_abc import Token
3+
4+
5+
class TokenFactory:
6+
tokens = {
7+
"$appname$": AppNameToken,
8+
"$session$": SessionToken,
9+
"$hostname$": HostNameToken,
10+
"$title$": TitleToken,
11+
"$timestamp$": TimestampToken,
12+
"$level$": LevelToken,
13+
"$color$": ColorToken,
14+
"$logentrytype$": LogEntryTypeToken,
15+
"$viewerid$": ViewerIdToken,
16+
"$thread$": ThreadIdToken,
17+
"$process$": ProcessIdToken,
18+
}
19+
20+
@staticmethod
21+
def _create_literal(value: str) -> Token:
22+
if not isinstance(value, str):
23+
raise TypeError("value must be an str")
24+
token = LiteralToken()
25+
token.options = ""
26+
token.value = value
27+
token.width = 0
28+
return token
29+
30+
@classmethod
31+
def get_token(cls, value: str) -> Token:
32+
if not isinstance(value, str):
33+
raise TypeError("value must be an str")
34+
35+
length = len(value)
36+
37+
if length <= 2:
38+
return cls._create_literal(value)
39+
40+
if value[0] != "$" or value[-1] != "$":
41+
return cls._create_literal(value)
42+
43+
original = value
44+
options = ""
45+
46+
# extract the token options: $token{options}$
47+
if value[-2] == "}":
48+
idx = value.find("{")
49+
50+
if idx > -1:
51+
idx += 1
52+
options = value[idx: -2]
53+
value = value[:idx - 1] + value[-1]
54+
55+
width = ""
56+
idx = value.find(",")
57+
58+
# extract the token width: $token, width$
59+
if idx > -1:
60+
idx += 1
61+
width = value[idx: -1]
62+
value = value[: idx - 1] + value[-1]
63+
64+
value = value.lower()
65+
impl = cls.tokens.get(value)
66+
if impl is None:
67+
return cls._create_literal(original)
68+
69+
# noinspection PyBroadException
70+
try:
71+
token: Token = impl()
72+
token.options = options
73+
token.value = original
74+
token.width = cls._parse_width(width)
75+
except Exception:
76+
return cls._create_literal(original)
77+
78+
return token
79+
80+
@staticmethod
81+
def _parse_width(value: str) -> int:
82+
if not isinstance(value, str):
83+
raise TypeError("value must be an str")
84+
85+
value = value.strip()
86+
if len(value) == 0:
87+
return 0
88+
89+
try:
90+
width = int(value)
91+
except ValueError:
92+
width = 0
93+
94+
return width

common/tokens/tokens.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import logging
2+
from datetime import datetime
3+
4+
from common.tokens.token_abc import Token
5+
from packets.log_entry.log_entry import LogEntry
6+
from session.session import Session
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
class AppNameToken(Token):
12+
@staticmethod
13+
def expand(log_entry: LogEntry) -> str:
14+
return log_entry.appname
15+
16+
17+
class SessionToken(Token):
18+
@staticmethod
19+
def expand(log_entry: LogEntry) -> str:
20+
return log_entry.session_name
21+
22+
23+
class HostNameToken(Token):
24+
@staticmethod
25+
def expand(log_entry: LogEntry) -> str:
26+
return log_entry.hostname
27+
28+
29+
class TitleToken(Token):
30+
@staticmethod
31+
def expand(log_entry: LogEntry) -> str:
32+
return log_entry.title
33+
34+
@property
35+
def indent(self) -> bool:
36+
return True
37+
38+
39+
class TimestampToken(Token):
40+
_FORMAT: str = "%Y-%m-%d %H:%M:%S.%f"
41+
42+
@staticmethod
43+
def _get_timestamp(log_entry: LogEntry) -> datetime:
44+
# convert timestamp value stored in LogEntry to seconds
45+
timestamp = log_entry.timestamp / 1_000_000
46+
47+
# convert time to utc
48+
offset_from_utc = datetime.fromtimestamp(timestamp).astimezone().utcoffset().total_seconds()
49+
timestamp -= offset_from_utc
50+
return datetime.fromtimestamp(timestamp)
51+
52+
def expand(self, log_entry: LogEntry) -> str:
53+
timestamp = self._get_timestamp(log_entry)
54+
55+
options = self.options
56+
if options is not None and len(options) > 0:
57+
fmt = options
58+
logger.debug("Timestamp format is set by options string: %s " % fmt)
59+
return timestamp.strftime(fmt)
60+
61+
logger.debug("Timestamp format is default: %s " % self._FORMAT)
62+
return timestamp.strftime(self._FORMAT)
63+
64+
65+
class LevelToken(Token):
66+
67+
@staticmethod
68+
def expand(log_entry: LogEntry) -> str:
69+
return str(log_entry.level)
70+
71+
72+
class ColorToken(Token):
73+
_CHAR_MAP: str = "0123456789ABCDEF"
74+
75+
def _append_hex(self, string_buffer: list, value: int) -> list:
76+
value &= 0xff
77+
string_buffer.append(self._CHAR_MAP[value & 0xf])
78+
string_buffer.append(self._CHAR_MAP[value >> 4])
79+
return string_buffer
80+
81+
def expand(self, log_entry: LogEntry) -> str:
82+
color = log_entry.color
83+
84+
if color is not None and color != Session.DEFAULT_COLOR:
85+
string_buffer = ["0x"]
86+
self._append_hex(string_buffer, color.get_red())
87+
self._append_hex(string_buffer, color.get_green())
88+
self._append_hex(string_buffer, color.get_blue())
89+
90+
return "".join(string_buffer)
91+
else:
92+
return "<default>"
93+
94+
95+
class LogEntryTypeToken(Token):
96+
@staticmethod
97+
def expand(log_entry: LogEntry) -> str:
98+
return str(log_entry.log_entry_type)
99+
100+
101+
class ViewerIdToken(Token):
102+
@staticmethod
103+
def expand(log_entry: LogEntry) -> str:
104+
return str(log_entry.viewer_id)
105+
106+
107+
class ThreadIdToken(Token):
108+
@staticmethod
109+
def expand(log_entry: LogEntry) -> str:
110+
return str(log_entry.thread_id)
111+
112+
113+
class ProcessIdToken(Token):
114+
@staticmethod
115+
def expand(log_entry: LogEntry) -> str:
116+
return str(log_entry.process_id)
117+
118+
119+
class LiteralToken(Token):
120+
def expand(self, log_entry: LogEntry) -> str:
121+
return self.value

common/viewer_id.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,6 @@ class ViewerId(Enum):
2323
JPEG = 401
2424
ICON = 402
2525
METAFILE = 403
26+
27+
def __str__(self):
28+
return "%s" % self._name_

0 commit comments

Comments
 (0)