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
@@ -0,0 +1,223 @@
1
+ """
2
+ Pyjoyment.Plugin.Mount - Application mount plugin
3
+ =================================================
4
+ ::
5
+
6
+ # Pyjoyment
7
+ route = app.plugin('Mount', {'/prefix': '/home/sri/foo/script/foo'})
8
+
9
+ # Pyjoyment.Lite
10
+ route = plugin('Mount', {'/prefix': '/home/sri/myapp.py'})
11
+
12
+ # Adjust the generated route and mounted application
13
+ example = plugin('Mount', {'/example': '/home/sri/example.py'})
14
+ example.to(message='It works great!')
15
+ embedded = example.pattern.defaults['app']
16
+ embedded.config(foo='bar')
17
+ embedded.log = app.log
18
+
19
+ # Mount application with host
20
+ plugin('Mount', {'example.com': '/home/sri/myapp.py'})
21
+
22
+ # Host and path
23
+ plugin('Mount', {'example.com/myapp': '/home/sri/myapp.py'})
24
+
25
+ # Or even hosts with wildcard subdomains
26
+ plugin('Mount', {'*.example.com/myapp': '/home/sri/myapp.py'})
27
+
28
+ # Application object
29
+ plugin('Mount', {'/prefix': MyApp.new()})
30
+
31
+ # WSGI application, as an object or as the name of its module and its
32
+ # attribute
33
+ plugin('Mount', {'/flask': flask_app})
34
+ plugin('Mount', {'/flask': 'myflask:app'})
35
+
36
+ # ASGI application, as an object or as the name of its module and its
37
+ # attribute
38
+ plugin('Mount', {'/starlette': starlette_app})
39
+ plugin('Mount', {'/starlette': 'mystarlette:app'})
40
+
41
+ # FastCGI application, as PHP on php-fpm, by its location, with the
42
+ # directory of its scripts and the one entry of every other path
43
+ plugin('Mount', {'/php': 'fcgi://127.0.0.1:9000'})
44
+ plugin('Mount', {'/php': 'fcgi+unix://%2Frun%2Fphp%2Ffpm.sock'})
45
+ plugin('Mount', {'/blog': 'fcgi://127.0.0.1:9000?document_root=%2Fvar%2Fwww&script=index.php'})
46
+
47
+ # CGI application, as an object, as a string is the path of a script
48
+ plugin('Mount', {'/cgi-bin': Pyjo.App.CGI.new(document_root='/usr/lib/cgi-bin')})
49
+
50
+ # Event function of AWS Lambda, as an object, in a process of its own
51
+ plugin('Mount', {'/function': Pyjo.App.Lambda.new(function='myfunction.handler')})
52
+
53
+ # A regular expression of the whole path, whose group path is the rest of
54
+ # the path for the application, and whose other named groups are captures
55
+ plugin('Mount', {re.compile(r'/php(?P<path>/.*)?'): flask_app})
56
+ plugin('Mount', {re.compile(r'/(?P<user>\\w+)/blog(?P<path>/.*)?'): flask_app})
57
+
58
+ :mod:`Pyjoyment.Plugin.Mount` is a plugin that allows you to mount whole
59
+ :mod:`Pyjoyment` applications, other applications of :mod:`Pyjo`, ``WSGI``
60
+ applications, which :mod:`Pyjo.App.WSGI` runs, and ``ASGI`` applications,
61
+ whose ``__call__`` is a coroutine function, which :mod:`Pyjo.App.ASGI` runs,
62
+ as an application of :mod:`Pyjo` from a script, an object, or the name of a
63
+ module and its attribute, and ``FastCGI`` applications, which
64
+ :mod:`Pyjo.App.FastCGI` connects to, by their location ``fcgi://`` or
65
+ ``fcgi+unix://``, ``CGI`` programs, which :mod:`Pyjo.App.CGI` runs, and
66
+ event functions of AWS Lambda, which :mod:`Pyjo.App.Lambda` runs, as an
67
+ object of them. An application that is not one of :mod:`Pyjoyment` gets the
68
+ server that runs it with :meth:`~Pyjo.App.Pyjo_App.server` before the server
69
+ starts, as the application that mounts it does.
70
+
71
+ The path is a pattern of a route that matches the beginning of the path of a
72
+ request, as in Mojolicious, so that ``/php`` matches ``/php.css`` too, which
73
+ an application of another framework does not get, as its ``SCRIPT_NAME`` or
74
+ ``root_path`` ends at a slash. A regular expression matches the whole path, as
75
+ the ones of PromQL do, with the rest of it for the application in its group
76
+ ``path``.
77
+
78
+ The code of this plugin is a good example for learning to build new plugins,
79
+ you're welcome to fork it.
80
+
81
+ See :mod:`Pyjoyment.Plugins` for a list of plugins that are available by
82
+ default.
83
+
84
+ See also
85
+ --------
86
+
87
+ :mod:`Pyjoyment`, https://mojolicious.org.
88
+
89
+ Ported from `Mojolicious::Plugin::Mount
90
+ <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojolicious/Plugin/Mount.pm>`_ of Mojolicious 9.49.
91
+
92
+ Classes
93
+ -------
94
+ """
95
+
96
+ import Pyjo.App
97
+ import Pyjo.App.ASGI
98
+ import Pyjo.App.FastCGI
99
+ import Pyjo.App.WSGI
100
+ import Pyjo.Exception
101
+ import Pyjo.Server
102
+ import Pyjoyment.Plugin
103
+ import Pyjoyment.Routes.Pattern
104
+
105
+ import importlib
106
+ import inspect
107
+ import os
108
+ import re
109
+
110
+ from typing import TYPE_CHECKING, Any, cast
111
+
112
+
113
+ if TYPE_CHECKING:
114
+ import Pyjoyment
115
+ import Pyjoyment.Routes.Route
116
+
117
+
118
+ class Pyjoyment_Plugin_Mount(Pyjoyment.Plugin.object):
119
+ """
120
+ :mod:`Pyjoyment.Plugin.Mount` inherits all methods from
121
+ :mod:`Pyjoyment.Plugin` and implements the following new ones.
122
+ """
123
+
124
+ def register(self, app: 'Pyjoyment.object', conf: dict[Any, Any]) -> 'Pyjoyment.Routes.Route.object':
125
+ """::
126
+
127
+ route = plugin.register(Pyjoyment.new(), {'/foo': '/some/app.py'})
128
+ route = plugin.register(Pyjoyment.new(), {'/foo': 'some_app:app'})
129
+
130
+ Mount application and return the generated route, which is usually a
131
+ :mod:`Pyjoyment.Routes.Route` object.
132
+ """
133
+ import Pyjoyment
134
+
135
+ key: str | re.Pattern[str] = next(iter(conf))
136
+ embed: Any = _load(conf[key])
137
+ if isinstance(embed, Pyjoyment.object):
138
+ embed.secrets = app.secrets
139
+
140
+ # Not in Mojolicious: another application of Pyjo, as one of Pyjo.App.WSGI, gets the server that runs it before it
141
+ # starts and after it stops, as the application that mounts it gets it
142
+ else:
143
+
144
+ def before_server_start(server: Pyjo.Server.object, app: Any) -> None:
145
+ embed.server(server)
146
+
147
+ def after_server_stop(server: Pyjo.Server.object, app: Any) -> Any:
148
+ return embed.server_stopped(server)
149
+
150
+ app.hook('before_server_start', before_server_start)
151
+ app.hook('after_server_stop', after_server_stop)
152
+ embed.log = app.log
153
+
154
+ # Not in Mojolicious: a regular expression that matches the whole path, as the ones of PromQL do, whose group
155
+ # path is the rest of the path for the application, as partial routes have it, and whose other named groups are
156
+ # captures
157
+ if isinstance(key, re.Pattern):
158
+ route = app.routes.any()
159
+ route.pattern = _Whole(key)
160
+ route.partial = True
161
+ return route.to(app=embed)
162
+
163
+ # Extract host
164
+ path: str | None = key
165
+ host = None
166
+ if m := re.match(r'^(\*\.)?([^/]+)(/.*)?$', path):
167
+ host = re.compile(rf'^(?:.*\.)?{re.escape(m[2])}$' if m[1] else rf'^{re.escape(m[2])}$', re.I)
168
+ path = m[3]
169
+
170
+ route = app.routes.any(path)
171
+ route.partial = True
172
+ route.to(app=embed)
173
+ return route.requires(host=host) if host else route
174
+
175
+
176
+ # Not in Mojolicious: an application as an object, or as the name of a module and its attribute, as ASGI servers take
177
+ # it, an ASGI or a WSGI application, which Pyjo.App.ASGI or Pyjo.App.WSGI runs, and the location of a FastCGI
178
+ # application, which Pyjo.App.FastCGI connects to, where Mojolicious loads a script
179
+ def _load(value: Any) -> Pyjo.Server.Application:
180
+ if isinstance(value, str) and re.match(r'fcgi(\+unix)?://', value):
181
+ return Pyjo.App.FastCGI.new(url=value)
182
+ if isinstance(value, str) and re.fullmatch(r'[\w.]+:[\w.]+', value) and not os.path.exists(value):
183
+ module, _, name = value.partition(':')
184
+ try:
185
+ value = importlib.import_module(module)
186
+ for attr in name.split('.'):
187
+ value = getattr(value, attr)
188
+ except Exception as e:
189
+ raise Pyjo.Exception.new(f'Can\'t load application "{module}:{name}": {e}') from e
190
+ elif isinstance(value, str | os.PathLike):
191
+ return Pyjo.Server.new().load_app(cast(str, value))
192
+
193
+ if isinstance(value, Pyjo.App.object):
194
+ return value
195
+ if inspect.iscoroutinefunction(value) or inspect.iscoroutinefunction(type(value).__call__):
196
+ return Pyjo.App.ASGI.new(app=value)
197
+ if not callable(value):
198
+ raise Pyjo.Exception.new(f'"{value!r}" is not an application')
199
+ return Pyjo.App.WSGI.new(app=value)
200
+
201
+
202
+ # Not in Mojolicious: the pattern of a regular expression that matches the whole path, whose named groups are captures,
203
+ # and whose group path, from its start to the end of the path, is the rest of the path
204
+ class _Whole(Pyjoyment.Routes.Pattern.object):
205
+ def __init__(self, whole: re.Pattern[str]) -> None:
206
+ super().__init__()
207
+ self.whole = whole
208
+ # The regular expression and its source, as the pages of exceptions and of not found and the command routes show
209
+ # them
210
+ self.regex = whole
211
+ self.unparsed = whole.pattern
212
+
213
+ def match_partial(self, path: str, detect: bool = False) -> tuple[dict[str, Any], str] | None:
214
+ if (m := self.whole.fullmatch(path)) is None:
215
+ return None
216
+ captures = {**self.defaults, **{name: value for name, value in m.groupdict().items() if value is not None}}
217
+ rest = path[m.start('path') :] if captures.pop('path', None) is not None else ''
218
+ return captures, rest
219
+
220
+
221
+ new = Pyjoyment_Plugin_Mount.new
222
+ object = Pyjoyment_Plugin_Mount
223
+ Mount = Pyjoyment_Plugin_Mount
@@ -0,0 +1,209 @@
1
+ """
2
+ Pyjoyment.Plugin.PyRenderer - Template string renderer plugin
3
+ =============================================================
4
+ ::
5
+
6
+ # Pyjoyment
7
+ app.plugin('PyRenderer')
8
+
9
+ # A template of a function
10
+ @app.template('index.html')
11
+ def index(c, name):
12
+ return t'<p>Hello {name}!</p>'
13
+
14
+ c.render('index', name='Joe')
15
+
16
+ # An inline t-string of Python 3.14
17
+ c.render(inline=t'<p>Hello {name}!</p>')
18
+
19
+ # The same on any Python
20
+ c.render(inline=t('<p>Hello {name}!</p>', name=name))
21
+
22
+ :mod:`Pyjoyment.Plugin.PyRenderer` is a renderer for ``py`` templates, which
23
+ are template strings, t-strings of
24
+ `PEP 750 <https://peps.python.org/pep-0750/>`_ or of
25
+ :func:`Pyjo.String.Template.t`, rendered by :mod:`Pyjo.String.Template`, which
26
+ escapes the values that they interpolate, unless they are markup, as the
27
+ helpers of :mod:`Pyjoyment.Plugin.TagHelpers` return. ::
28
+
29
+ from Pyjo.String.Template import t
30
+
31
+ # "<p>Hello &lt;script&gt;!</p>"
32
+ name = '<script>'
33
+ c.render(inline=t('<p>Hello {name}!</p>', name=name))
34
+
35
+ # "<p><a href="/">Home</a></p>"
36
+ c.render(inline=t('<p>{link}</p>', link=c.link_to('Home', '/')))
37
+
38
+ A template string is code, which cannot be read from a file of data as a
39
+ template of :mod:`Pyjoyment.Plugin.EPRenderer` is, so a template is a function
40
+ that returns one, registered with :meth:`Pyjoyment.Pyjoyment_App.template`,
41
+ or the function ``template`` of :mod:`Pyjoyment.Lite`, by its name with the
42
+ format, as ``index.html``. The function gets the controller and the values
43
+ of the stash that it names as its parameters, as the variables of an ``ep``
44
+ template, or all of them with ``**``, and it can set the values of the stash
45
+ that the renderer reads, as a layout. ::
46
+
47
+ # A template with a layout
48
+ @app.template('hello.html')
49
+ def hello(c, name):
50
+ c.layout('default')
51
+ c.title('Hello')
52
+ return t'<p>Hello {name}!</p>'
53
+
54
+ # The layout, which gets the content and the title
55
+ @app.template('layouts/default.html')
56
+ def default(c, **stash):
57
+ return t'<title>{c.title()}</title><body>{c.content()}</body>'
58
+
59
+ A template can also be a file in
60
+ :attr:`Pyjoyment.Renderer.Pyjoyment_Renderer.paths` with the extension ``py``,
61
+ as ``templates/index.html.py``, a module of Python whose function ``render``
62
+ is the function of the template. The file is
63
+ compiled once and kept in the cache of the renderer, without the bytecode
64
+ that an import writes next to it, and an exception in it shows the lines of
65
+ the file. ::
66
+
67
+ # templates/hello.html.py
68
+ def render(c, name):
69
+ return t'<p>Hello {name}!</p>'
70
+
71
+ An inline template that is a template string is rendered by it, unless
72
+ another handler is given. Its values are evaluated where it is written, so it
73
+ gets the values of the stash as the code around it does, ``c.stash('name')``,
74
+ and a layout does not apply to it, as to any inline template.
75
+
76
+ This is a core plugin, that means it is always enabled and its code a good
77
+ example for learning to build new plugins, you're welcome to fork it.
78
+
79
+ See :mod:`Pyjoyment.Plugins` for a list of plugins that are available by
80
+ default.
81
+
82
+ See also
83
+ --------
84
+
85
+ :mod:`Pyjoyment`, https://mojolicious.org.
86
+
87
+ Inspired by `Mojolicious::Plugin::EPLRenderer
88
+ <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojolicious/Plugin/EPLRenderer.pm>`_ of Mojolicious 9.49.
89
+ """
90
+
91
+ import Pyjo.Exception
92
+ import Pyjo.File
93
+ import Pyjo.String.Template
94
+ import Pyjoyment.Plugin
95
+
96
+ from Pyjo.Util import decode, encode, md5_sum
97
+
98
+ import inspect
99
+ import io
100
+ import types
101
+
102
+ from collections.abc import Callable
103
+ from typing import TYPE_CHECKING, Any
104
+
105
+
106
+ if TYPE_CHECKING:
107
+ import Pyjoyment
108
+ import Pyjoyment.Controller
109
+ import Pyjoyment.Renderer
110
+
111
+
112
+ class Pyjoyment_Plugin_PyRenderer(Pyjoyment.Plugin.object):
113
+ """
114
+ :mod:`Pyjoyment.Plugin.PyRenderer` inherits all methods from
115
+ :mod:`Pyjoyment.Plugin` and implements the following new ones.
116
+ """
117
+
118
+ def register(self, app: 'Pyjoyment.object', conf: dict[str, Any] | None = None) -> None:
119
+ """::
120
+
121
+ plugin.register(Pyjoyment.new())
122
+
123
+ Register renderer in :mod:`Pyjoyment` application.
124
+ """
125
+ app.renderer.add_handler('py', _handler)
126
+
127
+
128
+ def _handler(
129
+ renderer: 'Pyjoyment.Renderer.object',
130
+ c: 'Pyjoyment.Controller.object',
131
+ output: io.StringIO,
132
+ options: dict[str, Any],
133
+ ) -> None:
134
+ log = c.helpers.log()
135
+
136
+ # Inline
137
+ inline = options.get('inline')
138
+ if inline is not None:
139
+ if not Pyjo.String.Template.is_template(inline):
140
+ return
141
+ template = Pyjo.String.Template.t(inline)
142
+ name = md5_sum(encode('UTF-8', '{}'.join(template.strings)))
143
+ log.trace(f'Rendering inline template "{name}"')
144
+ output.write(template.render())
145
+ return
146
+
147
+ if (name := renderer.template_name(options)) is None:
148
+ return
149
+
150
+ # Function
151
+ if (cb := renderer.templates.get(name.removesuffix('.py'))) is not None:
152
+ log.trace(f'Rendering template "{name}" of a function')
153
+
154
+ # File
155
+ elif (path := renderer.template_path(options)) is not None:
156
+ key = md5_sum(encode('UTF-8', f'py:{path}'))
157
+ if (cb := renderer.cache.get(key)) is not None:
158
+ log.trace(f'Rendering cached template "{name}"')
159
+ else:
160
+ log.trace(f'Rendering template "{name}"')
161
+ renderer.cache.set(key, cb := _load(path, key, renderer.encoding))
162
+
163
+ # No template
164
+ else:
165
+ log.debug(f'Template "{name}" not found')
166
+ return
167
+
168
+ output.write(Pyjo.String.Template.escape(_call(cb, c, c.stash())))
169
+
170
+
171
+ # The function "render" of a file, which is compiled from its source, as importlib would write its bytecode next to it,
172
+ # with the path of the file, from which a traceback reads the lines of the template
173
+ def _load(path: str, key: str, encoding: str | None) -> Callable[..., Any]:
174
+ data = Pyjo.File.path(path).slurp()
175
+ assert isinstance(data, bytes)
176
+ if encoding is None:
177
+ source = data.decode('latin-1')
178
+ elif (source := decode(encoding, data)) is None:
179
+ raise Pyjo.Exception.new(f'Template "{path}" has invalid encoding')
180
+ try:
181
+ code = compile(source, path, 'exec', dont_inherit=True)
182
+ except SyntaxError as e:
183
+ raise Pyjo.Exception.new(f'{e.msg} at {path} line {e.lineno}.\n') from None
184
+
185
+ module = types.ModuleType(f'{__name__}.{key}')
186
+ module.__file__ = path
187
+
188
+ # No spec, as a spec without a loader makes linecache warn, which then reads the lines from the file
189
+ del module.__spec__
190
+ exec(code, module.__dict__)
191
+ if not callable(cb := module.__dict__.get('render')):
192
+ raise Pyjo.Exception.new(f'Template "{path}" has no function "render"')
193
+ return cb
194
+
195
+
196
+ # The controller and the values of the stash that the function names, or all of them for **
197
+ def _call(cb: Callable[..., Any], c: 'Pyjoyment.Controller.object', stash: dict[str, Any]) -> Any:
198
+ parameters = list(inspect.signature(cb).parameters.values())
199
+ rest = parameters[1:]
200
+ if any(p.kind is inspect.Parameter.VAR_KEYWORD for p in rest):
201
+ values = {k: v for k, v in stash.items() if k.isidentifier() and k != parameters[0].name}
202
+ else:
203
+ values = {p.name: stash[p.name] for p in rest if p.name in stash}
204
+ return cb(c, **values)
205
+
206
+
207
+ new = Pyjoyment_Plugin_PyRenderer.new
208
+ object = Pyjoyment_Plugin_PyRenderer
209
+ PyRenderer = Pyjoyment_Plugin_PyRenderer
@@ -0,0 +1,141 @@
1
+ """
2
+ Pyjoyment.Plugin.TOMLConfig - TOML configuration plugin
3
+ =======================================================
4
+ ::
5
+
6
+ # myapp.toml (it's just TOML with embedded Python)
7
+ %# Just a value
8
+ foo = "bar"
9
+
10
+ %# Nested data structures are fine too
11
+ baz = ["♥"]
12
+
13
+ %# You have full access to the application
14
+ music_dir = '<%= app.home.child('music') %>'
15
+
16
+ [db]
17
+ host = "localhost"
18
+
19
+ # Pyjoyment
20
+ config = app.plugin('TOMLConfig')
21
+ print(config['foo'])
22
+
23
+ # Pyjoyment.Lite
24
+ config = plugin('TOMLConfig')
25
+ print(config['foo'])
26
+
27
+ # foo.html.ep
28
+ %= config()['foo']
29
+
30
+ # The configuration is available application-wide
31
+ config = app.config()
32
+ print(config['foo'])
33
+
34
+ # Everything can be customized with options
35
+ config = plugin('TOMLConfig', file='/etc/myapp.conf')
36
+
37
+ :mod:`Pyjoyment.Plugin.TOMLConfig` is a TOML configuration plugin that
38
+ preprocesses its input with :mod:`Pyjo.Template` and parses it with
39
+ :mod:`tomllib` of the standard library.
40
+
41
+ The application object can be accessed via ``app``. A default configuration
42
+ filename in the application home directory will be generated from the value
43
+ of :attr:`Pyjoyment.Pyjoyment_App.moniker` (``$moniker.toml``). You can extend
44
+ the normal configuration file ``$moniker.toml`` with ``mode`` specific ones
45
+ like ``$moniker.$mode.toml``, which will be detected automatically.
46
+
47
+ A path goes into a literal string of TOML, in single quotes, which keeps
48
+ the backslashes of a path of Windows, where a basic string in double quotes
49
+ takes them for escapes.
50
+
51
+ These configuration values are currently reserved:
52
+
53
+ ``config_override``
54
+ If this configuration value has been set in
55
+ :meth:`Pyjoyment.Pyjoyment_App.config` when this plugin is loaded, it will
56
+ not do anything besides loading deployment specific plugins.
57
+
58
+ ``plugins`` ::
59
+
60
+ plugins = [{ SetUserGroup = { user = "sri", group = "staff" } }]
61
+
62
+ One or more deployment specific plugins that should be loaded right after
63
+ this plugin has been loaded.
64
+
65
+ The code of this plugin is a good example for learning to build new plugins,
66
+ you're welcome to fork it.
67
+
68
+ See :mod:`Pyjoyment.Plugins` for a list of plugins that are available by
69
+ default.
70
+
71
+ Options
72
+ -------
73
+
74
+ :mod:`Pyjoyment.Plugin.TOMLConfig` inherits all options from
75
+ :mod:`Pyjoyment.Plugin.JSONConfig`.
76
+
77
+ See also
78
+ --------
79
+
80
+ :mod:`Pyjoyment`, https://mojolicious.org.
81
+
82
+ Inspired by `Mojolicious::Plugin::NotYAMLConfig
83
+ <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojolicious/Plugin/NotYAMLConfig.pm>`_ of Mojolicious 9.49.
84
+
85
+ Classes
86
+ -------
87
+ """
88
+
89
+ import Pyjo.Exception
90
+ import Pyjoyment.Plugin.JSONConfig
91
+
92
+ import tomllib
93
+
94
+ from typing import TYPE_CHECKING, Any
95
+
96
+
97
+ if TYPE_CHECKING:
98
+ import Pyjoyment
99
+
100
+
101
+ class Pyjoyment_Plugin_TOMLConfig(Pyjoyment.Plugin.JSONConfig.object):
102
+ """
103
+ :mod:`Pyjoyment.Plugin.TOMLConfig` inherits all methods from
104
+ :mod:`Pyjoyment.Plugin.JSONConfig` and implements the following new ones.
105
+ """
106
+
107
+ def parse(self, content: str, file: str, conf: dict[str, Any], app: 'Pyjoyment.object') -> dict[str, Any]:
108
+ """::
109
+
110
+ plugin.parse(content, file, conf, app)
111
+
112
+ Process content with
113
+ :meth:`~Pyjoyment.Plugin.JSONConfig.Pyjoyment_Plugin_JSONConfig.render`
114
+ and parse it with :mod:`tomllib`. ::
115
+
116
+ def parse(self, content, file, conf, app):
117
+ ...
118
+ content = self.render(content, file, conf, app)
119
+ ...
120
+ return config
121
+ """
122
+ try:
123
+ return tomllib.loads(self.render(content, file, conf, app))
124
+ except Exception as e:
125
+ raise Pyjo.Exception.new(f'Can\'t load configuration from file "{file}": {e}') from e
126
+
127
+ def register(self, app: 'Pyjoyment.object', conf: dict[str, Any] | None = None) -> dict[str, Any]:
128
+ """::
129
+
130
+ config = plugin.register(Pyjoyment.new())
131
+ config = plugin.register(Pyjoyment.new(), {'file': '/etc/foo.conf'})
132
+
133
+ Register plugin in :mod:`Pyjoyment` application and merge
134
+ configuration.
135
+ """
136
+ return super().register(app, {'ext': 'toml', **(conf or {})})
137
+
138
+
139
+ new = Pyjoyment_Plugin_TOMLConfig.new
140
+ object = Pyjoyment_Plugin_TOMLConfig
141
+ TOMLConfig = Pyjoyment_Plugin_TOMLConfig