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.
Files changed (200) hide show
  1. Pyjo/App/ASGI.py +735 -0
  2. Pyjo/App/CGI.py +773 -0
  3. Pyjo/App/FastCGI.py +631 -0
  4. Pyjo/App/Lambda.py +987 -0
  5. Pyjo/App/WSGI.py +427 -0
  6. Pyjo/App/__init__.py +409 -0
  7. Pyjo/Asset/File.py +377 -0
  8. Pyjo/Asset/Memory.py +235 -0
  9. Pyjo/Asset/__init__.py +200 -0
  10. Pyjo/Base.py +215 -0
  11. Pyjo/Cache.py +119 -0
  12. Pyjo/Collection.py +570 -0
  13. Pyjo/Content/MultiPart.py +325 -0
  14. Pyjo/Content/Single.py +193 -0
  15. Pyjo/Content/__init__.py +806 -0
  16. Pyjo/Cookie/Request.py +90 -0
  17. Pyjo/Cookie/Response.py +294 -0
  18. Pyjo/Cookie/__init__.py +143 -0
  19. Pyjo/DOM/CSS.py +1019 -0
  20. Pyjo/DOM/HTML.py +550 -0
  21. Pyjo/DOM/__init__.py +1329 -0
  22. Pyjo/Date.py +294 -0
  23. Pyjo/EventEmitter.py +522 -0
  24. Pyjo/Exception.py +494 -0
  25. Pyjo/File.py +959 -0
  26. Pyjo/GetOpt.py +605 -0
  27. Pyjo/Headers.py +760 -0
  28. Pyjo/HelloWorld.py +77 -0
  29. Pyjo/Home.py +109 -0
  30. Pyjo/IOLoop/ChildProcess.py +591 -0
  31. Pyjo/IOLoop/Client.py +736 -0
  32. Pyjo/IOLoop/Datagram.py +982 -0
  33. Pyjo/IOLoop/Messages.py +664 -0
  34. Pyjo/IOLoop/Server.py +458 -0
  35. Pyjo/IOLoop/Stdio.py +446 -0
  36. Pyjo/IOLoop/Stream.py +644 -0
  37. Pyjo/IOLoop/Subprocess.py +419 -0
  38. Pyjo/IOLoop/TLS.py +360 -0
  39. Pyjo/IOLoop/__init__.py +1036 -0
  40. Pyjo/IOLoop/certs/README.md +41 -0
  41. Pyjo/IOLoop/certs/ca.crt +20 -0
  42. Pyjo/IOLoop/certs/server.crt +21 -0
  43. Pyjo/IOLoop/certs/server.key +28 -0
  44. Pyjo/JSON/Pointer.py +135 -0
  45. Pyjo/JSON/RPC.py +772 -0
  46. Pyjo/JSON/Schema.py +2506 -0
  47. Pyjo/JSON/__init__.py +573 -0
  48. Pyjo/Loader.py +342 -0
  49. Pyjo/Log.py +723 -0
  50. Pyjo/Message/Request.py +725 -0
  51. Pyjo/Message/Response.py +476 -0
  52. Pyjo/Message/__init__.py +830 -0
  53. Pyjo/Parameters.py +472 -0
  54. Pyjo/Path.py +419 -0
  55. Pyjo/Promise.py +894 -0
  56. Pyjo/Prove.py +1305 -0
  57. Pyjo/Reactor/Asyncio.py +854 -0
  58. Pyjo/Reactor/Poll.py +439 -0
  59. Pyjo/Reactor/__init__.py +525 -0
  60. Pyjo/SSE.py +119 -0
  61. Pyjo/Server/ASGI.py +486 -0
  62. Pyjo/Server/CGI.py +259 -0
  63. Pyjo/Server/Daemon.py +1007 -0
  64. Pyjo/Server/FastCGI.py +517 -0
  65. Pyjo/Server/Hot/__init__.py +687 -0
  66. Pyjo/Server/Hot/__main__.py +5 -0
  67. Pyjo/Server/Lambda.py +641 -0
  68. Pyjo/Server/Prefork.py +917 -0
  69. Pyjo/Server/Prethread.py +394 -0
  70. Pyjo/Server/Reload/Backend/Poll.py +114 -0
  71. Pyjo/Server/Reload/Backend/__init__.py +126 -0
  72. Pyjo/Server/Reload/__init__.py +482 -0
  73. Pyjo/Server/Reload/__main__.py +5 -0
  74. Pyjo/Server/Stdio.py +228 -0
  75. Pyjo/Server/WSGI.py +349 -0
  76. Pyjo/Server/__init__.py +396 -0
  77. Pyjo/String/Bytes.py +532 -0
  78. Pyjo/String/HTML.py +210 -0
  79. Pyjo/String/Mixin.py +333 -0
  80. Pyjo/String/Template.py +470 -0
  81. Pyjo/String/Unicode.py +553 -0
  82. Pyjo/String/__init__.py +0 -0
  83. Pyjo/TOML.py +232 -0
  84. Pyjo/Template.py +1290 -0
  85. Pyjo/Test/Pyjo.py +1454 -0
  86. Pyjo/Test/__init__.py +1233 -0
  87. Pyjo/Test/pytest_plugin.py +305 -0
  88. Pyjo/Transaction/HTTP.py +374 -0
  89. Pyjo/Transaction/WebSocket.py +709 -0
  90. Pyjo/Transaction/__init__.py +429 -0
  91. Pyjo/URL/Template.py +459 -0
  92. Pyjo/URL/__init__.py +651 -0
  93. Pyjo/Upload.py +139 -0
  94. Pyjo/UserAgent/CookieJar.py +391 -0
  95. Pyjo/UserAgent/Proxy.py +138 -0
  96. Pyjo/UserAgent/Server.py +239 -0
  97. Pyjo/UserAgent/Transactor.py +766 -0
  98. Pyjo/UserAgent/__init__.py +1243 -0
  99. Pyjo/Util.py +3567 -0
  100. Pyjo/WebSocket.py +332 -0
  101. Pyjo/YAML.py +1167 -0
  102. Pyjo/__init__.py +441 -0
  103. Pyjo/__main__.py +35 -0
  104. Pyjo/py.typed +0 -0
  105. Pyjoyment/Command/Author/__init__.py +20 -0
  106. Pyjoyment/Command/Author/generate/__init__.py +106 -0
  107. Pyjoyment/Command/Author/generate/app.py +299 -0
  108. Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
  109. Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
  110. Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
  111. Pyjoyment/Command/Author/generate/lite_app.py +113 -0
  112. Pyjoyment/Command/Author/generate/plugin.py +267 -0
  113. Pyjoyment/Command/Author/generate/pyproject.py +123 -0
  114. Pyjoyment/Command/Author/inflate.py +101 -0
  115. Pyjoyment/Command/__init__.py +332 -0
  116. Pyjoyment/Command/asgi.py +92 -0
  117. Pyjoyment/Command/cgi.py +90 -0
  118. Pyjoyment/Command/daemon.py +160 -0
  119. Pyjoyment/Command/eval.py +128 -0
  120. Pyjoyment/Command/fastcgi.py +99 -0
  121. Pyjoyment/Command/get.py +305 -0
  122. Pyjoyment/Command/lambda.py +117 -0
  123. Pyjoyment/Command/prefork.py +181 -0
  124. Pyjoyment/Command/prethread.py +126 -0
  125. Pyjoyment/Command/routes.py +133 -0
  126. Pyjoyment/Command/stdio.py +92 -0
  127. Pyjoyment/Command/version.py +141 -0
  128. Pyjoyment/Command/wsgi.py +93 -0
  129. Pyjoyment/Commands.py +546 -0
  130. Pyjoyment/Controller.py +1307 -0
  131. Pyjoyment/HelloWorld.py +56 -0
  132. Pyjoyment/Lite.py +494 -0
  133. Pyjoyment/Plugin/AllowedHosts.py +205 -0
  134. Pyjoyment/Plugin/Config.py +248 -0
  135. Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
  136. Pyjoyment/Plugin/EPLRenderer.py +134 -0
  137. Pyjoyment/Plugin/EPRenderer.py +212 -0
  138. Pyjoyment/Plugin/HeaderCondition.py +98 -0
  139. Pyjoyment/Plugin/Health.py +325 -0
  140. Pyjoyment/Plugin/JSONConfig.py +172 -0
  141. Pyjoyment/Plugin/Mount.py +223 -0
  142. Pyjoyment/Plugin/PyRenderer.py +209 -0
  143. Pyjoyment/Plugin/TOMLConfig.py +141 -0
  144. Pyjoyment/Plugin/TagHelpers.py +959 -0
  145. Pyjoyment/Plugin/YAMLConfig.py +178 -0
  146. Pyjoyment/Plugin/__init__.py +80 -0
  147. Pyjoyment/Plugins.py +215 -0
  148. Pyjoyment/Renderer.py +673 -0
  149. Pyjoyment/Routes/Match.py +251 -0
  150. Pyjoyment/Routes/Pattern.py +443 -0
  151. Pyjoyment/Routes/Route.py +827 -0
  152. Pyjoyment/Routes/__init__.py +419 -0
  153. Pyjoyment/Sessions.py +255 -0
  154. Pyjoyment/Static.py +421 -0
  155. Pyjoyment/Types.py +238 -0
  156. Pyjoyment/Validator/Validation.py +395 -0
  157. Pyjoyment/Validator/__init__.py +304 -0
  158. Pyjoyment/__init__.py +1083 -0
  159. Pyjoyment/__main__.py +5 -0
  160. Pyjoyment/py.typed +0 -0
  161. Pyjoyment/resources/public/favicon.ico +0 -0
  162. Pyjoyment/resources/public/favicon.svg +345 -0
  163. Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
  164. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
  165. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
  166. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
  167. Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
  168. Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
  169. Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
  170. Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
  171. Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
  172. Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
  173. Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
  174. Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
  175. Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
  176. Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
  177. Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
  178. Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
  179. Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
  180. Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
  181. Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
  182. Pyjoyment/resources/public/pyjo/logo.png +0 -0
  183. Pyjoyment/resources/public/pyjo/logo.svg +346 -0
  184. Pyjoyment/resources/public/pyjo/notfound.png +0 -0
  185. Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
  186. Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
  187. Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
  188. Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
  189. Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
  190. Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
  191. Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
  192. Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
  193. Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
  194. Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
  195. Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
  196. pyjoyment-0.1.0.dist-info/METADATA +376 -0
  197. pyjoyment-0.1.0.dist-info/RECORD +200 -0
  198. pyjoyment-0.1.0.dist-info/WHEEL +4 -0
  199. pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
  200. 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