logxpy 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. logxpy/__init__.py +126 -0
  2. logxpy/_action.py +958 -0
  3. logxpy/_async.py +186 -0
  4. logxpy/_base.py +80 -0
  5. logxpy/_compat.py +71 -0
  6. logxpy/_config.py +45 -0
  7. logxpy/_dest.py +88 -0
  8. logxpy/_errors.py +58 -0
  9. logxpy/_fmt.py +68 -0
  10. logxpy/_generators.py +136 -0
  11. logxpy/_mask.py +23 -0
  12. logxpy/_message.py +195 -0
  13. logxpy/_output.py +517 -0
  14. logxpy/_pool.py +93 -0
  15. logxpy/_traceback.py +126 -0
  16. logxpy/_types.py +71 -0
  17. logxpy/_util.py +56 -0
  18. logxpy/_validation.py +486 -0
  19. logxpy/_version.py +21 -0
  20. logxpy/cli.py +61 -0
  21. logxpy/dask.py +172 -0
  22. logxpy/decorators.py +268 -0
  23. logxpy/filter.py +124 -0
  24. logxpy/journald.py +88 -0
  25. logxpy/json.py +149 -0
  26. logxpy/loggerx.py +253 -0
  27. logxpy/logwriter.py +84 -0
  28. logxpy/parse.py +191 -0
  29. logxpy/prettyprint.py +173 -0
  30. logxpy/serializers.py +36 -0
  31. logxpy/stdlib.py +23 -0
  32. logxpy/tai64n.py +45 -0
  33. logxpy/testing.py +472 -0
  34. logxpy/tests/__init__.py +9 -0
  35. logxpy/tests/common.py +36 -0
  36. logxpy/tests/strategies.py +231 -0
  37. logxpy/tests/test_action.py +1751 -0
  38. logxpy/tests/test_api.py +86 -0
  39. logxpy/tests/test_async.py +67 -0
  40. logxpy/tests/test_compat.py +13 -0
  41. logxpy/tests/test_config.py +21 -0
  42. logxpy/tests/test_coroutines.py +105 -0
  43. logxpy/tests/test_dask.py +211 -0
  44. logxpy/tests/test_decorators.py +54 -0
  45. logxpy/tests/test_filter.py +122 -0
  46. logxpy/tests/test_fmt.py +42 -0
  47. logxpy/tests/test_generators.py +292 -0
  48. logxpy/tests/test_journald.py +246 -0
  49. logxpy/tests/test_json.py +208 -0
  50. logxpy/tests/test_loggerx.py +44 -0
  51. logxpy/tests/test_logwriter.py +262 -0
  52. logxpy/tests/test_message.py +334 -0
  53. logxpy/tests/test_output.py +921 -0
  54. logxpy/tests/test_parse.py +309 -0
  55. logxpy/tests/test_pool.py +55 -0
  56. logxpy/tests/test_prettyprint.py +303 -0
  57. logxpy/tests/test_pyinstaller.py +35 -0
  58. logxpy/tests/test_serializers.py +36 -0
  59. logxpy/tests/test_stdlib.py +73 -0
  60. logxpy/tests/test_tai64n.py +66 -0
  61. logxpy/tests/test_testing.py +1051 -0
  62. logxpy/tests/test_traceback.py +251 -0
  63. logxpy/tests/test_twisted.py +814 -0
  64. logxpy/tests/test_util.py +45 -0
  65. logxpy/tests/test_validation.py +989 -0
  66. logxpy/twisted.py +265 -0
  67. logxpy-0.1.0.dist-info/METADATA +100 -0
  68. logxpy-0.1.0.dist-info/RECORD +72 -0
  69. logxpy-0.1.0.dist-info/WHEEL +5 -0
  70. logxpy-0.1.0.dist-info/entry_points.txt +2 -0
  71. logxpy-0.1.0.dist-info/licenses/LICENSE +201 -0
  72. logxpy-0.1.0.dist-info/top_level.txt +1 -0
logxpy/_generators.py ADDED
@@ -0,0 +1,136 @@
1
+ """
2
+ Support for maintaining an action context across generator suspension.
3
+ """
4
+
5
+ from sys import exc_info
6
+ from functools import wraps
7
+ from contextlib import contextmanager
8
+ from contextvars import copy_context
9
+ from weakref import WeakKeyDictionary
10
+
11
+ from . import log_message
12
+
13
+
14
+ class _GeneratorContext(object):
15
+ """Generator sub-context for C{_ExecutionContext}."""
16
+
17
+ def __init__(self, execution_context):
18
+ self._execution_context = execution_context
19
+ self._contexts = WeakKeyDictionary()
20
+ self._current_generator = None
21
+
22
+ def init_stack(self, generator):
23
+ """Create a new stack for the given generator."""
24
+ self._contexts[generator] = copy_context()
25
+
26
+ @contextmanager
27
+ def in_generator(self, generator):
28
+ """Context manager: set the given generator as the current generator."""
29
+ previous_generator = self._current_generator
30
+ try:
31
+ self._current_generator = generator
32
+ yield
33
+ finally:
34
+ self._current_generator = previous_generator
35
+
36
+
37
+ class GeneratorSupportNotEnabled(Exception):
38
+ """
39
+ An attempt was made to use a decorated generator without first turning on
40
+ the generator context manager.
41
+ """
42
+
43
+
44
+ def eliot_friendly_generator_function(original):
45
+ """
46
+ Decorate a generator function so that the Eliot action context is
47
+ preserved across ``yield`` expressions.
48
+ """
49
+
50
+ @wraps(original)
51
+ def wrapper(*a, **kw):
52
+ # Keep track of whether the next value to deliver to the generator is
53
+ # a non-exception or an exception.
54
+ ok = True
55
+
56
+ # Keep track of the next value to deliver to the generator.
57
+ value_in = None
58
+
59
+ # Create the generator with a call to the generator function. This
60
+ # happens with whatever Eliot action context happens to be active,
61
+ # which is fine and correct and also irrelevant because no code in the
62
+ # generator function can run until we call send or throw on it.
63
+ gen = original(*a, **kw)
64
+
65
+ # Initialize the per-generator context to a copy of the current context.
66
+ context = copy_context()
67
+ while True:
68
+ try:
69
+ # Whichever way we invoke the generator, we will do it
70
+ # with the Eliot action context stack we've saved for it.
71
+ # Then the context manager will re-save it and restore the
72
+ # "outside" stack for us.
73
+ #
74
+ # Regarding the support of Twisted's inlineCallbacks-like
75
+ # functionality (see eliot.twisted.inline_callbacks):
76
+ #
77
+ # The invocation may raise the inlineCallbacks internal
78
+ # control flow exception _DefGen_Return. It is not wrong to
79
+ # just let that propagate upwards here but inlineCallbacks
80
+ # does think it is wrong. The behavior triggers a
81
+ # DeprecationWarning to try to get us to fix our code. We
82
+ # could explicitly handle and re-raise the _DefGen_Return but
83
+ # only at the expense of depending on a private Twisted API.
84
+ # For now, I'm opting to try to encourage Twisted to fix the
85
+ # situation (or at least not worsen it):
86
+ # https://twistedmatrix.com/trac/ticket/9590
87
+ #
88
+ # Alternatively, _DefGen_Return is only required on Python 2.
89
+ # When Python 2 support is dropped, this concern can be
90
+ # eliminated by always using `return value` instead of
91
+ # `returnValue(value)` (and adding the necessary logic to the
92
+ # StopIteration handler below).
93
+ def go():
94
+ if ok:
95
+ value_out = gen.send(value_in)
96
+ else:
97
+ value_out = gen.throw(*value_in)
98
+ # We have obtained a value from the generator. In
99
+ # giving it to us, it has given up control. Note this
100
+ # fact here. Importantly, this is within the
101
+ # generator's action context so that we get a good
102
+ # indication of where the yield occurred.
103
+ #
104
+ # This is noisy, enable only for debugging:
105
+ if wrapper.debug:
106
+ log_message(message_type="yielded")
107
+ return value_out
108
+
109
+ value_out = context.run(go)
110
+ except StopIteration:
111
+ # When the generator raises this, it is signaling
112
+ # completion. Leave the loop.
113
+ break
114
+ else:
115
+ try:
116
+ # Pass the generator's result along to whoever is
117
+ # driving. Capture the result as the next value to
118
+ # send inward.
119
+ value_in = yield value_out
120
+ except:
121
+ # Or capture the exception if that's the flavor of the
122
+ # next value. This could possibly include GeneratorExit
123
+ # which turns out to be just fine because throwing it into
124
+ # the inner generator effectively propagates the close
125
+ # (and with the right context!) just as you would want.
126
+ # True, the GeneratorExit does get re-throwing out of the
127
+ # gen.throw call and hits _the_generator_context's
128
+ # contextmanager. But @contextmanager extremely
129
+ # conveniently eats it for us! Thanks, @contextmanager!
130
+ ok = False
131
+ value_in = exc_info()
132
+ else:
133
+ ok = True
134
+
135
+ wrapper.debug = False
136
+ return wrapper
logxpy/_mask.py ADDED
@@ -0,0 +1,23 @@
1
+ """Field masking."""
2
+ from __future__ import annotations
3
+ import re
4
+ from typing import Any
5
+
6
+ class Masker:
7
+ def __init__(self, fields: list[str], patterns: list[str]):
8
+ self._fields = {f.lower() for f in fields}
9
+ self._patterns = [re.compile(p) for p in patterns]
10
+
11
+ def mask(self, data: dict[str, Any]) -> dict[str, Any]:
12
+ return {k: self._mask(k, v) for k, v in data.items()}
13
+
14
+ def _mask(self, key: str, val: Any) -> Any:
15
+ if key.lower() in self._fields: return "***"
16
+ if isinstance(val, str):
17
+ for p in self._patterns:
18
+ val = p.sub("***", val)
19
+ elif isinstance(val, dict):
20
+ return self.mask(val)
21
+ elif isinstance(val, list):
22
+ return [self._mask(key, v) for v in val]
23
+ return val
logxpy/_message.py ADDED
@@ -0,0 +1,195 @@
1
+ """
2
+ Log messages and related utilities.
3
+ """
4
+
5
+ import time
6
+ from warnings import warn
7
+
8
+ from pyrsistent import PClass, pmap_field
9
+
10
+ MESSAGE_TYPE_FIELD = "message_type"
11
+ TASK_UUID_FIELD = "task_uuid"
12
+ TASK_LEVEL_FIELD = "task_level"
13
+ TIMESTAMP_FIELD = "timestamp"
14
+
15
+ EXCEPTION_FIELD = "exception"
16
+ REASON_FIELD = "reason"
17
+
18
+
19
+ class Message(object):
20
+ """
21
+ A log message.
22
+
23
+ Messages are basically dictionaries, mapping "fields" to "values". Field
24
+ names should not start with C{'_'}, as those are reserved for system use
25
+ (e.g. C{"_id"} is used by Elasticsearch for unique message identifiers and
26
+ may be auto-populated by logstash).
27
+ """
28
+
29
+ # Overrideable for testing purposes:
30
+ _time = time.time
31
+
32
+ @classmethod
33
+ def new(_class, _serializer=None, **fields):
34
+ """
35
+ Create a new L{Message}.
36
+
37
+ The keyword arguments will become the initial contents of the L{Message}.
38
+
39
+ @param _serializer: A positional argument, either C{None} or a
40
+ L{eliot._validation._MessageSerializer} with which a
41
+ L{eliot.ILogger} may choose to serialize the message. If you're
42
+ using L{eliot.MessageType} this will be populated for you.
43
+
44
+ @return: The new L{Message}
45
+ """
46
+ warn(
47
+ "Message.new() is deprecated since 1.11.0, "
48
+ "use eliot.log_message() instead.",
49
+ DeprecationWarning,
50
+ stacklevel=2,
51
+ )
52
+ return _class(fields, _serializer)
53
+
54
+ @classmethod
55
+ def log(_class, **fields):
56
+ """
57
+ Write a new L{Message} to the default L{Logger}.
58
+
59
+ The keyword arguments will become contents of the L{Message}.
60
+ """
61
+ warn(
62
+ "Message.log() is deprecated since 1.11.0, "
63
+ "use Action.log() or eliot.log_message() instead.",
64
+ DeprecationWarning,
65
+ stacklevel=2,
66
+ )
67
+ _class(fields).write()
68
+
69
+ def __init__(self, contents, serializer=None):
70
+ """
71
+ You can also use L{Message.new} to create L{Message} objects.
72
+
73
+ @param contents: The contents of this L{Message}, a C{dict} whose keys
74
+ must be C{str}, or text that has been UTF-8 encoded to
75
+ C{bytes}.
76
+
77
+ @param serializer: Either C{None}, or
78
+ L{eliot._validation._MessageSerializer} with which a
79
+ L{eliot.Logger} may choose to serialize the message. If you're
80
+ using L{eliot.MessageType} this will be populated for you.
81
+ """
82
+ self._contents = contents.copy()
83
+ self._serializer = serializer
84
+
85
+ def bind(self, **fields):
86
+ """
87
+ Return a new L{Message} with this message's contents plus the
88
+ additional given bindings.
89
+ """
90
+ contents = self._contents.copy()
91
+ contents.update(fields)
92
+ return Message(contents, self._serializer)
93
+
94
+ def contents(self):
95
+ """
96
+ Return a copy of L{Message} contents.
97
+ """
98
+ return self._contents.copy()
99
+
100
+ def _timestamp(self):
101
+ """
102
+ Return the current time.
103
+ """
104
+ return self._time()
105
+
106
+ def write(self, logger=None, action=None):
107
+ """
108
+ Write the message to the given logger.
109
+
110
+ This will additionally include a timestamp, the action context if any,
111
+ and any other fields.
112
+
113
+ Byte field names will be converted to Unicode.
114
+
115
+ @type logger: L{eliot.ILogger} or C{None} indicating the default one.
116
+ Should not be set if the action is also set.
117
+
118
+ @param action: The L{Action} which is the context for this message. If
119
+ C{None}, the L{Action} will be deduced from the current call stack.
120
+ """
121
+ fields = dict(self._contents)
122
+ if "message_type" not in fields:
123
+ fields["message_type"] = ""
124
+ if self._serializer is not None:
125
+ fields["__eliot_serializer__"] = self._serializer
126
+ if action is None:
127
+ if logger is not None:
128
+ fields["__eliot_logger__"] = logger
129
+ log_message(**fields)
130
+ else:
131
+ action.log(**fields)
132
+
133
+
134
+ class WrittenMessage(PClass):
135
+ """
136
+ A L{Message} that has been logged.
137
+
138
+ @ivar _logged_dict: The originally logged dictionary.
139
+ """
140
+
141
+ _logged_dict = pmap_field((str, str), object)
142
+
143
+ @property
144
+ def timestamp(self):
145
+ """
146
+ The Unix timestamp of when the message was logged.
147
+ """
148
+ return self._logged_dict[TIMESTAMP_FIELD]
149
+
150
+ @property
151
+ def task_uuid(self):
152
+ """
153
+ The UUID of the task in which the message was logged.
154
+ """
155
+ return self._logged_dict[TASK_UUID_FIELD]
156
+
157
+ @property
158
+ def task_level(self):
159
+ """
160
+ The L{TaskLevel} of this message appears within the task.
161
+ """
162
+ return TaskLevel(level=self._logged_dict[TASK_LEVEL_FIELD])
163
+
164
+ @property
165
+ def contents(self):
166
+ """
167
+ A C{PMap}, the message contents without Eliot metadata.
168
+ """
169
+ return (
170
+ self._logged_dict.discard(TIMESTAMP_FIELD)
171
+ .discard(TASK_UUID_FIELD)
172
+ .discard(TASK_LEVEL_FIELD)
173
+ )
174
+
175
+ @classmethod
176
+ def from_dict(cls, logged_dictionary):
177
+ """
178
+ Reconstruct a L{WrittenMessage} from a logged dictionary.
179
+
180
+ @param logged_dictionary: A C{PMap} representing a parsed log entry.
181
+ @return: A L{WrittenMessage} for that dictionary.
182
+ """
183
+ return cls(_logged_dict=logged_dictionary)
184
+
185
+ def as_dict(self):
186
+ """
187
+ Return the dictionary that was used to write this message.
188
+
189
+ @return: A C{dict}, as might be logged by Eliot.
190
+ """
191
+ return self._logged_dict
192
+
193
+
194
+ # Import at end to deal with circular imports:
195
+ from ._action import log_message, TaskLevel