Pyjoyment 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.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
Pyjo/Log.py
ADDED
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pyjo.Log - Simple logger
|
|
3
|
+
========================
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
from Pyjo.Log import Log
|
|
7
|
+
|
|
8
|
+
# Log to STDERR
|
|
9
|
+
log = Log()
|
|
10
|
+
|
|
11
|
+
# Or import the module, as in Google Python Style Guide
|
|
12
|
+
|
|
13
|
+
import Pyjo.Log
|
|
14
|
+
|
|
15
|
+
# Log to STDERR
|
|
16
|
+
log = Pyjo.Log.new()
|
|
17
|
+
|
|
18
|
+
# Customize log file location and minimum log level
|
|
19
|
+
log = Pyjo.Log.new(path='/var/log/pyjo.log', level='warn')
|
|
20
|
+
|
|
21
|
+
# Log messages
|
|
22
|
+
log.trace('Doing stuff')
|
|
23
|
+
log.debug('Not sure what is happening here')
|
|
24
|
+
log.info('FYI: it happened again')
|
|
25
|
+
log.warn('This might be a problem')
|
|
26
|
+
log.error('Garden variety error')
|
|
27
|
+
log.fatal('Boom')
|
|
28
|
+
|
|
29
|
+
:mod:`Pyjo.Log` is a simple logger for :mod:`Pyjo` projects.
|
|
30
|
+
|
|
31
|
+
The end of a ``with`` block closes the file that the log opened for
|
|
32
|
+
:attr:`~Pyjo_Log.path`.
|
|
33
|
+
|
|
34
|
+
Events
|
|
35
|
+
------
|
|
36
|
+
|
|
37
|
+
:mod:`Pyjo.Log` inherits all events from :mod:`Pyjo.EventEmitter` and can
|
|
38
|
+
emit the following new ones.
|
|
39
|
+
|
|
40
|
+
message
|
|
41
|
+
~~~~~~~
|
|
42
|
+
::
|
|
43
|
+
|
|
44
|
+
log.on('message', lambda log, level, *lines: ...)
|
|
45
|
+
|
|
46
|
+
Emitted when a new message gets logged. ::
|
|
47
|
+
|
|
48
|
+
log.on('message', lambda log, level, *lines: print(f'{level}: ', *lines, sep=''))
|
|
49
|
+
|
|
50
|
+
JSON
|
|
51
|
+
----
|
|
52
|
+
|
|
53
|
+
With :attr:`~Pyjo_Log.json`, or the ``PYJO_LOG_FORMAT`` environment variable
|
|
54
|
+
set to ``json``, which the option ``--log-format`` of the commands of
|
|
55
|
+
:mod:`Pyjoyment.Commands` sets, every message is a line of JSON, with the
|
|
56
|
+
fields that log collectors, as
|
|
57
|
+
Cloud Logging, Datadog and Elastic, recognize: ``time`` in UTC, as of
|
|
58
|
+
:rfc:`3339`, ``level``, ``severity`` with the names of Cloud Logging and
|
|
59
|
+
syslog, ``message``, ``pid``, and the fields of the message and its context,
|
|
60
|
+
as :class:`Field` objects. A message of several lines, as a traceback, stays
|
|
61
|
+
one line of JSON. ::
|
|
62
|
+
|
|
63
|
+
{"time":"2026-10-19T08:15:42.123450Z","level":"warn","severity":"WARNING","message":"Disk is 95% full","pid":31841}
|
|
64
|
+
|
|
65
|
+
The levels ``trace`` and ``debug`` are of the severity ``DEBUG``, ``info`` of
|
|
66
|
+
``INFO``, ``warn`` of ``WARNING``, ``error`` of ``ERROR`` and ``fatal`` of
|
|
67
|
+
``CRITICAL``.
|
|
68
|
+
|
|
69
|
+
The logging module
|
|
70
|
+
------------------
|
|
71
|
+
|
|
72
|
+
:class:`Pyjo.Log.Handler` is a :class:`logging.Handler` that passes the records
|
|
73
|
+
of the standard library's :mod:`logging` to a :mod:`Pyjo.Log`. ::
|
|
74
|
+
|
|
75
|
+
import logging
|
|
76
|
+
|
|
77
|
+
logging.getLogger().addHandler(Pyjo.Log.Handler(log))
|
|
78
|
+
logging.getLogger().warning('Disk is %d%% full', 95)
|
|
79
|
+
|
|
80
|
+
The levels of :mod:`logging` are mapped to the closest level of the log:
|
|
81
|
+
``CRITICAL`` is ``fatal``, ``ERROR`` is ``error``, ``WARNING`` is ``warn``,
|
|
82
|
+
``INFO`` is ``info``, ``DEBUG`` is ``debug``, and a level below ``DEBUG`` is
|
|
83
|
+
``trace``.
|
|
84
|
+
|
|
85
|
+
See also
|
|
86
|
+
--------
|
|
87
|
+
|
|
88
|
+
:mod:`Pyjoyment`, https://mojolicious.org.
|
|
89
|
+
|
|
90
|
+
Ported from `Mojo::Log <https://github.com/mojolicious/mojo/blob/v9.52/lib/Mojo/Log.pm>`_
|
|
91
|
+
of Mojolicious 9.52.
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
import Pyjo.EventEmitter
|
|
95
|
+
import Pyjo.Exception
|
|
96
|
+
import Pyjo.File
|
|
97
|
+
|
|
98
|
+
import builtins
|
|
99
|
+
import datetime
|
|
100
|
+
import json
|
|
101
|
+
import logging
|
|
102
|
+
import os
|
|
103
|
+
import sys
|
|
104
|
+
import time
|
|
105
|
+
import weakref
|
|
106
|
+
|
|
107
|
+
from collections.abc import Callable, Iterable, Iterator, Mapping
|
|
108
|
+
from types import TracebackType
|
|
109
|
+
from typing import IO, Any, Self, cast
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
type Anything = builtins.object
|
|
113
|
+
|
|
114
|
+
# A callback for formatting log messages, it gets the time, the level and the lines
|
|
115
|
+
type Format = Callable[..., str]
|
|
116
|
+
|
|
117
|
+
# Supported log levels
|
|
118
|
+
LEVEL = {'trace': 1, 'debug': 2, 'info': 3, 'warn': 4, 'error': 5, 'fatal': 6}
|
|
119
|
+
|
|
120
|
+
# The severities of Cloud Logging and syslog, which Datadog recognizes too
|
|
121
|
+
SEVERITY = {
|
|
122
|
+
'trace': 'DEBUG',
|
|
123
|
+
'debug': 'DEBUG',
|
|
124
|
+
'info': 'INFO',
|
|
125
|
+
'warn': 'WARNING',
|
|
126
|
+
'error': 'ERROR',
|
|
127
|
+
'fatal': 'CRITICAL',
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# Systemd magic numbers
|
|
131
|
+
MAGIC = {'trace': 7, 'debug': 6, 'info': 5, 'warn': 4, 'error': 3, 'fatal': 2}
|
|
132
|
+
|
|
133
|
+
# Colors, as Term::ANSIColor has yellow, red and white on red
|
|
134
|
+
COLORS = {'warn': '33', 'error': '31', 'fatal': '37;41'}
|
|
135
|
+
|
|
136
|
+
# A handle that is not set yet, as None is no handle
|
|
137
|
+
_UNSET: Any = builtins.object()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class Pyjo_Log(Pyjo.EventEmitter.object):
|
|
141
|
+
"""
|
|
142
|
+
:mod:`Pyjo.Log` inherits all methods from :mod:`Pyjo.EventEmitter` and
|
|
143
|
+
implements the following new ones.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
def __init__(
|
|
147
|
+
self,
|
|
148
|
+
*,
|
|
149
|
+
color: bool | None = None,
|
|
150
|
+
format: Format | None = None,
|
|
151
|
+
handle: IO[str] | None = _UNSET,
|
|
152
|
+
history: list[list[Any]] | None = None,
|
|
153
|
+
json: bool | None = None,
|
|
154
|
+
level: str = 'trace',
|
|
155
|
+
max_history_size: int = 10,
|
|
156
|
+
path: str | os.PathLike[str] | None = None,
|
|
157
|
+
short: bool | None = None,
|
|
158
|
+
) -> None:
|
|
159
|
+
"""::
|
|
160
|
+
|
|
161
|
+
log = Pyjo.Log.new()
|
|
162
|
+
log = Pyjo.Log.new(level='warn')
|
|
163
|
+
|
|
164
|
+
Construct a new :mod:`Pyjo.Log` object and subscribe to the
|
|
165
|
+
``message`` event with the default logger.
|
|
166
|
+
"""
|
|
167
|
+
super().__init__()
|
|
168
|
+
|
|
169
|
+
self._color: bool | None = color
|
|
170
|
+
self._json: bool | None = json
|
|
171
|
+
|
|
172
|
+
self.history: list[list[Any]] = history if history is not None else []
|
|
173
|
+
"""::
|
|
174
|
+
|
|
175
|
+
history = log.history
|
|
176
|
+
log.history = [[time.time(), 'debug', 'That went wrong']]
|
|
177
|
+
|
|
178
|
+
The last few logged messages.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
self.level = level
|
|
182
|
+
"""::
|
|
183
|
+
|
|
184
|
+
level = log.level
|
|
185
|
+
log.level = 'debug'
|
|
186
|
+
|
|
187
|
+
Active log level, defaults to ``trace``. Available log levels are
|
|
188
|
+
``trace``, ``debug``, ``info``, ``warn``, ``error`` and ``fatal``, in
|
|
189
|
+
that order.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
self.max_history_size = max_history_size
|
|
193
|
+
"""::
|
|
194
|
+
|
|
195
|
+
size = log.max_history_size
|
|
196
|
+
log.max_history_size = 5
|
|
197
|
+
|
|
198
|
+
Maximum number of logged messages to store in :attr:`history`,
|
|
199
|
+
defaults to ``10``.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
self.path = path
|
|
203
|
+
"""::
|
|
204
|
+
|
|
205
|
+
path = log.path
|
|
206
|
+
log.path = '/var/log/pyjo.log'
|
|
207
|
+
|
|
208
|
+
Log file path used by :attr:`handle`.
|
|
209
|
+
"""
|
|
210
|
+
|
|
211
|
+
self._short: bool | None = short
|
|
212
|
+
|
|
213
|
+
self._capturing = False
|
|
214
|
+
self._context: list[Any] = []
|
|
215
|
+
self._format = format
|
|
216
|
+
self._handle = handle
|
|
217
|
+
self._finalizer: weakref.finalize[[], Any] | None = None
|
|
218
|
+
self._parent: Pyjo_Log | None = None
|
|
219
|
+
|
|
220
|
+
self.on('message', _message)
|
|
221
|
+
|
|
222
|
+
# Not in Mojolicious: a context manager, which closes the file that the log opened for path where the block ends, as
|
|
223
|
+
# PyPy frees the object only when its garbage collector runs
|
|
224
|
+
def __enter__(self) -> Self:
|
|
225
|
+
return self
|
|
226
|
+
|
|
227
|
+
def __exit__(
|
|
228
|
+
self, type: type[BaseException] | None, value: BaseException | None, traceback: TracebackType | None
|
|
229
|
+
) -> None:
|
|
230
|
+
self.close()
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def color(self) -> bool:
|
|
234
|
+
"""::
|
|
235
|
+
|
|
236
|
+
boolean = log.color
|
|
237
|
+
log.color = boolean
|
|
238
|
+
|
|
239
|
+
Colorize log messages with the levels ``warn``, ``error`` and
|
|
240
|
+
``fatal`` using ANSI escape sequences, defaults to the value of the
|
|
241
|
+
``PYJO_LOG_COLOR`` environment variable.
|
|
242
|
+
"""
|
|
243
|
+
if self._color is None:
|
|
244
|
+
self._color = bool(os.environ.get('PYJO_LOG_COLOR'))
|
|
245
|
+
return self._color
|
|
246
|
+
|
|
247
|
+
@color.setter
|
|
248
|
+
def color(self, value: bool) -> None:
|
|
249
|
+
self._color = value
|
|
250
|
+
|
|
251
|
+
# Not in Mojolicious: JSON Lines for log collectors
|
|
252
|
+
@property
|
|
253
|
+
def json(self) -> bool:
|
|
254
|
+
"""::
|
|
255
|
+
|
|
256
|
+
boolean = log.json
|
|
257
|
+
log.json = boolean
|
|
258
|
+
|
|
259
|
+
Generate log messages as lines of JSON, see `JSON`_, defaults to true
|
|
260
|
+
when the ``PYJO_LOG_FORMAT`` environment variable is ``json``, and
|
|
261
|
+
false when it is ``text`` or not set. It takes precedence over
|
|
262
|
+
:attr:`short` and :attr:`color`.
|
|
263
|
+
"""
|
|
264
|
+
if self._json is None:
|
|
265
|
+
self._json = (os.environ.get('PYJO_LOG_FORMAT') or '').lower() == 'json'
|
|
266
|
+
return self._json
|
|
267
|
+
|
|
268
|
+
@json.setter
|
|
269
|
+
def json(self, value: bool) -> None:
|
|
270
|
+
self._json = value
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def short(self) -> bool:
|
|
274
|
+
"""::
|
|
275
|
+
|
|
276
|
+
boolean = log.short
|
|
277
|
+
log.short = boolean
|
|
278
|
+
|
|
279
|
+
Generate short log messages without a timestamp but with journald log
|
|
280
|
+
level prefix, suitable for systemd environments, defaults to the value
|
|
281
|
+
of the ``PYJO_LOG_SHORT`` environment variable.
|
|
282
|
+
"""
|
|
283
|
+
if self._short is None:
|
|
284
|
+
self._short = bool(os.environ.get('PYJO_LOG_SHORT'))
|
|
285
|
+
return self._short
|
|
286
|
+
|
|
287
|
+
@short.setter
|
|
288
|
+
def short(self, value: bool) -> None:
|
|
289
|
+
self._short = value
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def format(self) -> Format:
|
|
293
|
+
"""::
|
|
294
|
+
|
|
295
|
+
cb = log.format
|
|
296
|
+
log.format = lambda time, level, *lines: ...
|
|
297
|
+
|
|
298
|
+
A callback for formatting log messages. ::
|
|
299
|
+
|
|
300
|
+
log.format = lambda time, level, *lines: '[2018-11-08 14:20:13.77168] [28320] [info] I ♥ Pyjoyment\\n'
|
|
301
|
+
"""
|
|
302
|
+
if self._format is None:
|
|
303
|
+
self._format = _json if self.json else _short if self.short else _color if self.color else _default
|
|
304
|
+
return self._format
|
|
305
|
+
|
|
306
|
+
@format.setter
|
|
307
|
+
def format(self, value: Format) -> None:
|
|
308
|
+
self._format = value
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def handle(self) -> IO[str] | None:
|
|
312
|
+
"""::
|
|
313
|
+
|
|
314
|
+
handle = log.handle
|
|
315
|
+
log.handle = io.StringIO()
|
|
316
|
+
|
|
317
|
+
Log text stream used by the default ``message`` event, defaults to
|
|
318
|
+
opening :attr:`path` for appending in UTF-8 or :data:`sys.stderr`.
|
|
319
|
+
"""
|
|
320
|
+
if self._handle is _UNSET:
|
|
321
|
+
# STDERR, or the file
|
|
322
|
+
path = self.path
|
|
323
|
+
if path is None:
|
|
324
|
+
self._handle = sys.stderr
|
|
325
|
+
else:
|
|
326
|
+
handle = self._handle = Pyjo.File.new(path).open('a', encoding='UTF-8')
|
|
327
|
+
|
|
328
|
+
# Not in Mojolicious: the file is closed with the object, as Perl closes it with the last reference
|
|
329
|
+
self._finalizer = weakref.finalize(self, handle.close)
|
|
330
|
+
return self._handle
|
|
331
|
+
|
|
332
|
+
@handle.setter
|
|
333
|
+
def handle(self, value: IO[str] | None) -> None:
|
|
334
|
+
self._handle = value
|
|
335
|
+
|
|
336
|
+
def _release(self) -> None:
|
|
337
|
+
# The file that the log opened for path, which it opens again when it is used again
|
|
338
|
+
if (finalizer := self._finalizer) is not None:
|
|
339
|
+
self._finalizer = None
|
|
340
|
+
self._handle = _UNSET
|
|
341
|
+
finalizer()
|
|
342
|
+
|
|
343
|
+
def append(self, msg: str) -> None:
|
|
344
|
+
"""::
|
|
345
|
+
|
|
346
|
+
log.append('[2018-11-08 14:20:13.77168] [28320] [info] I ♥ Pyjoyment\\n')
|
|
347
|
+
|
|
348
|
+
Append message to :attr:`handle`.
|
|
349
|
+
"""
|
|
350
|
+
if (handle := self.handle) is None:
|
|
351
|
+
return
|
|
352
|
+
fd = _fileno(handle)
|
|
353
|
+
_flock(fd, True)
|
|
354
|
+
try:
|
|
355
|
+
handle.write(msg)
|
|
356
|
+
handle.flush()
|
|
357
|
+
except OSError as e:
|
|
358
|
+
error = type(e)(f"Can't write to log: {e.strerror}" if e.strerror else "Can't write to log")
|
|
359
|
+
error.errno = e.errno
|
|
360
|
+
raise error from e
|
|
361
|
+
finally:
|
|
362
|
+
_flock(fd, False)
|
|
363
|
+
|
|
364
|
+
def capture(self, level: str | None = None) -> '_Capture':
|
|
365
|
+
"""::
|
|
366
|
+
|
|
367
|
+
with log.capture() as messages:
|
|
368
|
+
...
|
|
369
|
+
with log.capture('debug') as messages:
|
|
370
|
+
...
|
|
371
|
+
|
|
372
|
+
Capture log messages until the end of the ``with`` block, useful for
|
|
373
|
+
testing log messages. The messages stay in the returned object after
|
|
374
|
+
the block. ::
|
|
375
|
+
|
|
376
|
+
# Test your log messages
|
|
377
|
+
with log.capture('trace') as messages:
|
|
378
|
+
log.fatal('Something very bad happened')
|
|
379
|
+
log.trace('Just some debug information')
|
|
380
|
+
like_ok(str(messages), r'Something very bad happened', 'logs contain fatal message')
|
|
381
|
+
like_ok(messages[-1], r'Just some debug information', 'trace message was last')
|
|
382
|
+
"""
|
|
383
|
+
if self._capturing:
|
|
384
|
+
Pyjo.Exception.object.throw('Log messages are already being captured')
|
|
385
|
+
self._capturing = True
|
|
386
|
+
|
|
387
|
+
original = self.level
|
|
388
|
+
self.level = level or original
|
|
389
|
+
subscribers = self.subscribers('message')
|
|
390
|
+
self.unsubscribe('message')
|
|
391
|
+
|
|
392
|
+
def restore() -> None:
|
|
393
|
+
self.level = original
|
|
394
|
+
self.unsubscribe('message')
|
|
395
|
+
self._capturing = False
|
|
396
|
+
for cb in subscribers:
|
|
397
|
+
self.on('message', cb)
|
|
398
|
+
|
|
399
|
+
capture = _Capture(restore)
|
|
400
|
+
messages = capture.messages
|
|
401
|
+
|
|
402
|
+
def message(log: Pyjo_Log, level: str, *lines: Any) -> None:
|
|
403
|
+
messages.append(log.format(time.time(), level, *lines))
|
|
404
|
+
|
|
405
|
+
self.on('message', message)
|
|
406
|
+
|
|
407
|
+
return capture
|
|
408
|
+
|
|
409
|
+
# Not in Mojolicious: the release of the file of path, which Perl does in DESTROY, also at the end of a with block
|
|
410
|
+
def close(self) -> None:
|
|
411
|
+
"""::
|
|
412
|
+
|
|
413
|
+
log.close()
|
|
414
|
+
|
|
415
|
+
Close the file that the log opened for :attr:`path`, as the end of a
|
|
416
|
+
``with`` block does, which it opens again when it is used again.
|
|
417
|
+
"""
|
|
418
|
+
self._release()
|
|
419
|
+
|
|
420
|
+
def context(self, *context: Any) -> Self:
|
|
421
|
+
"""::
|
|
422
|
+
|
|
423
|
+
new = log.context('[extra]', '[information]')
|
|
424
|
+
|
|
425
|
+
Construct a new child :mod:`Pyjo.Log` object that will include context
|
|
426
|
+
information with every log message. ::
|
|
427
|
+
|
|
428
|
+
# Log with context
|
|
429
|
+
log = Pyjo.Log.new()
|
|
430
|
+
context = log.context('[17a60115]')
|
|
431
|
+
context.debug('This is a log message with context information')
|
|
432
|
+
context.info('And another')
|
|
433
|
+
|
|
434
|
+
The items of a dict are :class:`Field` objects, which a message of
|
|
435
|
+
JSON has as fields of its own, and the other formats as
|
|
436
|
+
``name=value``. ::
|
|
437
|
+
|
|
438
|
+
# Log with the fields of a context
|
|
439
|
+
context = log.context({'user': 'sri'})
|
|
440
|
+
"""
|
|
441
|
+
log = self.new(level=self.level)
|
|
442
|
+
log._parent = self._parent or self
|
|
443
|
+
# Not in Mojolicious: the items of a dict as fields
|
|
444
|
+
log._context = [
|
|
445
|
+
field
|
|
446
|
+
for item in context
|
|
447
|
+
for field in (
|
|
448
|
+
[Field(name, value) for name, value in cast(Mapping[str, Any], item).items()]
|
|
449
|
+
if isinstance(item, Mapping)
|
|
450
|
+
else [item]
|
|
451
|
+
)
|
|
452
|
+
]
|
|
453
|
+
return log
|
|
454
|
+
|
|
455
|
+
def debug(self, *msgs: Any) -> Self:
|
|
456
|
+
"""::
|
|
457
|
+
|
|
458
|
+
log = log.debug('You screwed up, but that is ok')
|
|
459
|
+
log = log.debug('All', 'cool')
|
|
460
|
+
log = log.debug(lambda: ...)
|
|
461
|
+
|
|
462
|
+
Emit ``message`` event and log ``debug`` message.
|
|
463
|
+
"""
|
|
464
|
+
return self._log('debug', *msgs) if 2 >= LEVEL[self.level] else self
|
|
465
|
+
|
|
466
|
+
def error(self, *msgs: Any) -> Self:
|
|
467
|
+
"""::
|
|
468
|
+
|
|
469
|
+
log = log.error('You really screwed up this time')
|
|
470
|
+
log = log.error('Wow', 'seriously')
|
|
471
|
+
log = log.error(lambda: ...)
|
|
472
|
+
|
|
473
|
+
Emit ``message`` event and log ``error`` message.
|
|
474
|
+
"""
|
|
475
|
+
return self._log('error', *msgs) if 5 >= LEVEL[self.level] else self
|
|
476
|
+
|
|
477
|
+
def fatal(self, *msgs: Any) -> Self:
|
|
478
|
+
"""::
|
|
479
|
+
|
|
480
|
+
log = log.fatal('Its over...')
|
|
481
|
+
log = log.fatal('Bye', 'bye')
|
|
482
|
+
log = log.fatal(lambda: ...)
|
|
483
|
+
|
|
484
|
+
Emit ``message`` event and log ``fatal`` message.
|
|
485
|
+
"""
|
|
486
|
+
return self._log('fatal', *msgs) if 6 >= LEVEL[self.level] else self
|
|
487
|
+
|
|
488
|
+
def info(self, *msgs: Any) -> Self:
|
|
489
|
+
"""::
|
|
490
|
+
|
|
491
|
+
log = log.info('You are bad, but you prolly know already')
|
|
492
|
+
log = log.info('Ok', 'then')
|
|
493
|
+
log = log.info(lambda: ...)
|
|
494
|
+
|
|
495
|
+
Emit ``message`` event and log ``info`` message.
|
|
496
|
+
"""
|
|
497
|
+
return self._log('info', *msgs) if 3 >= LEVEL[self.level] else self
|
|
498
|
+
|
|
499
|
+
def is_level(self, level: str) -> bool:
|
|
500
|
+
"""::
|
|
501
|
+
|
|
502
|
+
boolean = log.is_level('debug')
|
|
503
|
+
|
|
504
|
+
Check active log :attr:`level`. ::
|
|
505
|
+
|
|
506
|
+
# True
|
|
507
|
+
log.set(level='debug').is_level('debug')
|
|
508
|
+
log.set(level='debug').is_level('info')
|
|
509
|
+
|
|
510
|
+
# False
|
|
511
|
+
log.set(level='info').is_level('debug')
|
|
512
|
+
log.set(level='fatal').is_level('warn')
|
|
513
|
+
"""
|
|
514
|
+
return LEVEL[level] >= LEVEL[self.level]
|
|
515
|
+
|
|
516
|
+
def trace(self, *msgs: Any) -> Self:
|
|
517
|
+
"""::
|
|
518
|
+
|
|
519
|
+
log = log.trace('Whatever')
|
|
520
|
+
log = log.trace('Who', 'cares')
|
|
521
|
+
log = log.trace(lambda: ...)
|
|
522
|
+
|
|
523
|
+
Emit ``message`` event and log ``trace`` message.
|
|
524
|
+
"""
|
|
525
|
+
return self._log('trace', *msgs) if 1 >= LEVEL[self.level] else self
|
|
526
|
+
|
|
527
|
+
def warn(self, *msgs: Any) -> Self:
|
|
528
|
+
"""::
|
|
529
|
+
|
|
530
|
+
log = log.warn('Dont do that Dave...')
|
|
531
|
+
log = log.warn('No', 'really')
|
|
532
|
+
log = log.warn(lambda: ...)
|
|
533
|
+
|
|
534
|
+
Emit ``message`` event and log ``warn`` message.
|
|
535
|
+
"""
|
|
536
|
+
return self._log('warn', *msgs) if 4 >= LEVEL[self.level] else self
|
|
537
|
+
|
|
538
|
+
def _log(self, level: str, *msgs: Any) -> Self:
|
|
539
|
+
if msgs and callable(msgs[0]):
|
|
540
|
+
result: Any = msgs[0]()
|
|
541
|
+
msgs = tuple(cast(Iterable[Any], result)) if isinstance(result, (tuple, list)) else (result,)
|
|
542
|
+
msgs = (*self._context, *msgs)
|
|
543
|
+
(self._parent or self).emit('message', level, *msgs)
|
|
544
|
+
# Unlike Mojolicious: Mojo returns what emit() returns, the parent of a context, where the POD says the log
|
|
545
|
+
return self
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
# Not in Mojolicious: a field of a JSON message
|
|
549
|
+
class Field(str):
|
|
550
|
+
"""::
|
|
551
|
+
|
|
552
|
+
field = Pyjo.Log.Field('user', 'sri')
|
|
553
|
+
field = Pyjo.Log.Field('request_id', 'abc123', '[abc123]')
|
|
554
|
+
|
|
555
|
+
A field of a message of JSON, as a line of a message or of a context, see
|
|
556
|
+
`JSON`_, which the other formats have as its text, ``name=value`` by
|
|
557
|
+
default. A field does not replace the fields that every message of JSON
|
|
558
|
+
has, and a value that JSON has no type for is a string. ::
|
|
559
|
+
|
|
560
|
+
# Log with a field
|
|
561
|
+
log.info('Disk is full', Pyjo.Log.Field('disk', '/dev/sda1'))
|
|
562
|
+
"""
|
|
563
|
+
|
|
564
|
+
name: str
|
|
565
|
+
"""::
|
|
566
|
+
|
|
567
|
+
name = field.name
|
|
568
|
+
|
|
569
|
+
Name of the field.
|
|
570
|
+
"""
|
|
571
|
+
|
|
572
|
+
value: Any
|
|
573
|
+
"""::
|
|
574
|
+
|
|
575
|
+
value = field.value
|
|
576
|
+
|
|
577
|
+
Value of the field.
|
|
578
|
+
"""
|
|
579
|
+
|
|
580
|
+
def __new__(cls, name: str, value: Any, text: str | None = None) -> Self:
|
|
581
|
+
field = super().__new__(cls, f'{name}={value}' if text is None else text)
|
|
582
|
+
field.name, field.value = name, value
|
|
583
|
+
return field
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
class _Capture:
|
|
587
|
+
def __init__(self, restore: Callable[[], None]) -> None:
|
|
588
|
+
self.messages: list[str] = []
|
|
589
|
+
self._restore: Callable[[], None] | None = restore
|
|
590
|
+
|
|
591
|
+
def __enter__(self) -> Self:
|
|
592
|
+
return self
|
|
593
|
+
|
|
594
|
+
def __exit__(
|
|
595
|
+
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
|
|
596
|
+
) -> None:
|
|
597
|
+
if (restore := self._restore) is not None:
|
|
598
|
+
self._restore = None
|
|
599
|
+
restore()
|
|
600
|
+
|
|
601
|
+
# Left out from Mojolicious: the bool overload, an empty capture is false as an empty container is
|
|
602
|
+
|
|
603
|
+
def __getitem__(self, index: int) -> str:
|
|
604
|
+
return self.messages[index]
|
|
605
|
+
|
|
606
|
+
def __iter__(self) -> Iterator[str]:
|
|
607
|
+
return iter(self.messages)
|
|
608
|
+
|
|
609
|
+
def __len__(self) -> int:
|
|
610
|
+
return len(self.messages)
|
|
611
|
+
|
|
612
|
+
def __str__(self) -> str:
|
|
613
|
+
return ''.join(self.messages)
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
# Not in Mojolicious: a handler of the logging module
|
|
617
|
+
class Handler(logging.Handler):
|
|
618
|
+
"""::
|
|
619
|
+
|
|
620
|
+
handler = Pyjo.Log.Handler(log)
|
|
621
|
+
handler = Pyjo.Log.Handler(log, logging.INFO)
|
|
622
|
+
|
|
623
|
+
A :class:`logging.Handler` that formats the records of :mod:`logging` and
|
|
624
|
+
logs them with the given :mod:`Pyjo.Log`, at the closest level.
|
|
625
|
+
"""
|
|
626
|
+
|
|
627
|
+
def __init__(self, log: Pyjo_Log, level: int = logging.NOTSET) -> None:
|
|
628
|
+
super().__init__(level)
|
|
629
|
+
self.log = log
|
|
630
|
+
|
|
631
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
632
|
+
try:
|
|
633
|
+
msg = self.format(record)
|
|
634
|
+
log = self.log
|
|
635
|
+
level = _level(record.levelno)
|
|
636
|
+
if log.is_level(level):
|
|
637
|
+
getattr(log, level)(msg)
|
|
638
|
+
except Exception:
|
|
639
|
+
self.handleError(record)
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def _color(time: float, level: str, *lines: Any) -> str:
|
|
643
|
+
msg = _default(time, level, *lines)
|
|
644
|
+
return f'\x1b[{COLORS[level]}m{msg}\x1b[0m' if level in COLORS else msg
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _default(time_: float, level: str, *lines: Any) -> str:
|
|
648
|
+
t = time.localtime(time_)
|
|
649
|
+
# The fraction of the seconds is the one of the time as a string
|
|
650
|
+
fraction = str(time_).split('.')[1] if '.' in str(time_) else '0'
|
|
651
|
+
seconds = float(f'{t.tm_sec}.{fraction}')
|
|
652
|
+
timestamp = f'{t.tm_year:04d}-{t.tm_mon:02d}-{t.tm_mday:02d} {t.tm_hour:02d}:{t.tm_min:02d}:{seconds:08.5f}'
|
|
653
|
+
return f'[{timestamp}] [{os.getpid()}] [{level}] {" ".join(map(str, lines))}\n'
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
def _fileno(handle: IO[str]) -> int | None:
|
|
657
|
+
try:
|
|
658
|
+
return handle.fileno()
|
|
659
|
+
except (AttributeError, OSError, ValueError):
|
|
660
|
+
return None
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
# Perl ignores what flock returns, as on a handle that cannot be locked
|
|
664
|
+
def _flock(fd: int | None, lock: bool) -> None:
|
|
665
|
+
if fd is None or sys.platform == 'win32':
|
|
666
|
+
return
|
|
667
|
+
import fcntl
|
|
668
|
+
|
|
669
|
+
try:
|
|
670
|
+
fcntl.flock(fd, fcntl.LOCK_EX if lock else fcntl.LOCK_UN)
|
|
671
|
+
except OSError:
|
|
672
|
+
pass
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
# The closest level of Pyjo.Log for a level of the logging module
|
|
676
|
+
def _json(time_: float, level: str, *lines: Any) -> str:
|
|
677
|
+
timestamp = datetime.datetime.fromtimestamp(time_, datetime.UTC).isoformat(timespec='microseconds')
|
|
678
|
+
record: dict[str, Any] = {
|
|
679
|
+
'time': timestamp.removesuffix('+00:00') + 'Z',
|
|
680
|
+
'level': level,
|
|
681
|
+
'severity': SEVERITY[level],
|
|
682
|
+
'message': ' '.join(str(line) for line in lines if not isinstance(line, Field)),
|
|
683
|
+
'pid': os.getpid(),
|
|
684
|
+
}
|
|
685
|
+
for line in lines:
|
|
686
|
+
if isinstance(line, Field):
|
|
687
|
+
record.setdefault(line.name, line.value)
|
|
688
|
+
return json.dumps(record, ensure_ascii=False, separators=(',', ':'), default=str) + '\n'
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
def _level(levelno: int) -> str:
|
|
692
|
+
if levelno >= logging.CRITICAL:
|
|
693
|
+
return 'fatal'
|
|
694
|
+
if levelno >= logging.ERROR:
|
|
695
|
+
return 'error'
|
|
696
|
+
if levelno >= logging.WARNING:
|
|
697
|
+
return 'warn'
|
|
698
|
+
if levelno >= logging.INFO:
|
|
699
|
+
return 'info'
|
|
700
|
+
if levelno >= logging.DEBUG:
|
|
701
|
+
return 'debug'
|
|
702
|
+
return 'trace'
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def _message(log: Pyjo_Log, level: str, *lines: Any) -> None:
|
|
706
|
+
max = log.max_history_size
|
|
707
|
+
history = log.history
|
|
708
|
+
msg = [time.time(), level, *lines]
|
|
709
|
+
history.append(msg)
|
|
710
|
+
while len(history) > max:
|
|
711
|
+
history.pop(0)
|
|
712
|
+
|
|
713
|
+
log.append(log.format(*msg))
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
def _short(time: float, level: str, *lines: Any) -> str:
|
|
717
|
+
magic, short = f'<{MAGIC[level]}>', level[0]
|
|
718
|
+
return f'{magic}[{os.getpid()}] [{short}] {" ".join(map(str, lines))}\n'
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
new = Pyjo_Log.new
|
|
722
|
+
object = Pyjo_Log
|
|
723
|
+
Log = Pyjo_Log
|