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,299 @@
1
+ """
2
+ Pyjoyment.Command.Author.generate.app - App generator command
3
+ =============================================================
4
+ ::
5
+
6
+ Usage: APPLICATION generate app [OPTIONS] [NAME]
7
+
8
+ pyjo generate app
9
+ pyjo generate app TestApp
10
+ pyjo generate app My.TestApp
11
+
12
+ Options:
13
+ -h, --help Show this summary of available options
14
+
15
+ :mod:`Pyjoyment.Command.Author.generate.app` generates application directory
16
+ structures for fully functional :mod:`Pyjoyment` applications: the package of
17
+ the application in the directory of the project, which is its home, with
18
+ ``pyproject.toml`` for its dependencies, so that
19
+ ``uv run python -m MyApp daemon`` starts it and
20
+ ``uv run python -m Pyjo.Prove -l -r t`` tests it, as does ``uv run pytest``,
21
+ with pytest of the dependency group ``dev``, and ``AGENTS.md``, which
22
+ ``CLAUDE.md`` imports, tells a coding agent how the application is made, run
23
+ and tested, and where the skills of Pyjoyment are.
24
+
25
+ This is a core command, that means it is always enabled and its code a good
26
+ example for learning to build new commands, you're welcome to fork it.
27
+
28
+ See :mod:`Pyjoyment.Commands` for a list of commands that are available by
29
+ default.
30
+
31
+ See also
32
+ --------
33
+
34
+ :mod:`Pyjoyment`, https://mojolicious.org.
35
+
36
+ Ported from `Mojolicious::Command::Author::generate::app
37
+ <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojolicious/Command/Author/generate/app.pm>`_ of
38
+ Mojolicious 9.49.
39
+
40
+ Classes
41
+ -------
42
+ """
43
+
44
+ import Pyjoyment.Command
45
+ import Pyjoyment.Command.Author.generate.pyproject
46
+
47
+ from Pyjo.Util import class_to_file, class_to_path, decamelize, extract_usage
48
+ from Pyjoyment.Command.Author.generate.pyproject import _requirement # pyright: ignore[reportPrivateUsage]
49
+
50
+
51
+ class Pyjoyment_Command_Author_generate_app(Pyjoyment.Command.object):
52
+ """
53
+ :mod:`Pyjoyment.Command.Author.generate.app` inherits all attributes and
54
+ methods from :mod:`Pyjoyment.Command` and implements the following new
55
+ ones.
56
+ """
57
+
58
+ description = 'Generate Pyjoyment application directory structure'
59
+ """::
60
+
61
+ description = app.description
62
+ app.description = 'Foo'
63
+
64
+ Short description of this command, used for the command list.
65
+ """
66
+
67
+ usage = extract_usage()
68
+ """::
69
+
70
+ usage = app.usage
71
+ app.usage = 'Foo'
72
+
73
+ Usage information for this command, used for the help screen.
74
+ """
75
+
76
+ def run(self, class_: str | None = None, *args: str) -> None:
77
+ """::
78
+
79
+ app.run(*sys.argv[1:])
80
+
81
+ Run this command.
82
+ """
83
+ class_ = class_ or 'MyApp'
84
+
85
+ # Script
86
+ # Renamed from Mojolicious: the __main__ module of the package, for python -m, where Mojo has script/my_app
87
+ name = class_to_file(class_)
88
+ package = class_.replace('.', '/')
89
+ self.render_to_rel_file('main', f'{name}/{package}/__main__.py', {'class_': class_})
90
+
91
+ # Application class
92
+ # Unlike Mojolicious: a package in the directory of the project, as the current directory is on sys.path, where
93
+ # Mojo has the lib directory
94
+ self.render_to_rel_file(
95
+ 'appclass', f'{name}/{package}/__init__.py', {'class_': _identifier(class_), 'alias': _alias(class_)}
96
+ )
97
+
98
+ # Config file (using the default moniker)
99
+ self.render_to_rel_file('config', f'{name}/{decamelize(class_.replace(".", "::"))}.yml')
100
+
101
+ # Controller
102
+ controller = f'{class_}.Controller.Example'
103
+ path = class_to_path(controller)
104
+ self.render_to_rel_file(
105
+ 'controller', f'{name}/{path}', {'class_': _identifier(controller), 'alias': _alias(controller)}
106
+ )
107
+
108
+ # Test
109
+ # Renamed from Mojolicious: t/basic.py, where Mojo has t/basic.t
110
+ self.render_to_rel_file('test', f'{name}/t/basic.py', {'class_': class_})
111
+
112
+ # Not in Mojolicious: pyproject.toml, with the dependencies, as Mojo leaves Makefile.PL to generate makefile
113
+ pyproject = Pyjoyment.Command.Author.generate.pyproject.new(app=self.app, quiet=self.quiet)
114
+ pyproject.render_to_rel_file(
115
+ 'pyproject', f'{name}/pyproject.toml', {'name': name, 'requirement': _requirement(), 'tests': True}
116
+ )
117
+
118
+ # Static file
119
+ self.render_to_rel_file('static', f'{name}/public/index.html')
120
+ self.create_rel_dir(f'{name}/public/assets')
121
+
122
+ # Templates
123
+ self.render_to_rel_file('layout', f'{name}/templates/layouts/default.html.ep')
124
+ self.render_to_rel_file('welcome', f'{name}/templates/example/welcome.html.ep')
125
+
126
+ # Not in Mojolicious: the guide of the coding agents, AGENTS.md, which CLAUDE.md imports for Claude Code
127
+ config = f'{decamelize(class_.replace(".", "::"))}.yml'
128
+ self.render_to_rel_file('agents', f'{name}/AGENTS.md', {'class_': class_, 'config': config, 'package': package})
129
+ self.render_to_rel_file('claude', f'{name}/CLAUDE.md')
130
+
131
+
132
+ # Not in Mojolicious: the name of the class in its module, with underscores, as Pyjo_URL is of Pyjo.URL
133
+ def _identifier(class_: str) -> str:
134
+ return class_.replace('.', '_')
135
+
136
+
137
+ # Not in Mojolicious: the name of the class for the constructor of Python, the last part of its module, as Path is of
138
+ # Pyjo.Path
139
+ def _alias(class_: str) -> str:
140
+ return class_.rsplit('.', 1)[-1]
141
+
142
+
143
+ new = Pyjoyment_Command_Author_generate_app.new
144
+ object = Pyjoyment_Command_Author_generate_app
145
+
146
+ __DATA__ = r"""
147
+ @@ main
148
+ import Pyjoyment.Commands
149
+
150
+
151
+ # Start command line interface for application
152
+ Pyjoyment.Commands.start_app('<%= class_ %>')
153
+
154
+ @@ appclass
155
+ import Pyjoyment
156
+
157
+
158
+ class <%= class_ %>(Pyjoyment.object):
159
+ # This method will run once at server start
160
+ def startup(self):
161
+ # Load configuration from config file
162
+ config = self.plugin('YAMLConfig')
163
+
164
+ # Configure the application
165
+ self.secrets = config.get('secrets')
166
+
167
+ %# Not in Mojolicious: the plugin Health, whose endpoints the probes of containers and orchestrators request
168
+ # Liveness and readiness for containers and orchestrators
169
+ self.plugin('Health')
170
+
171
+ # Router
172
+ r = self.routes
173
+
174
+ # Normal route to controller
175
+ r.get('/').to('Example#welcome')
176
+
177
+
178
+ new = <%= class_ %>.new
179
+ object = <%= class_ %>
180
+ % if alias != class_:
181
+ <%= alias %> = <%= class_ %>
182
+ % end
183
+
184
+ @@ controller
185
+ import Pyjoyment.Controller
186
+
187
+
188
+ class <%= class_ %>(Pyjoyment.Controller.object):
189
+ # This action will render a template
190
+ def welcome(self):
191
+ # Render template "example/welcome.html.ep" with message
192
+ self.render(msg='Welcome to the Pyjoyment real-time web framework!')
193
+
194
+
195
+ object = <%= class_ %>
196
+ <%= alias %> = <%= class_ %>
197
+
198
+ @@ static
199
+ <!DOCTYPE html>
200
+ <html>
201
+ <head>
202
+ <title>Welcome to the Pyjoyment real-time web framework!</title>
203
+ </head>
204
+ <body>
205
+ <h2>Welcome to the Pyjoyment real-time web framework!</h2>
206
+ This is the static document "public/index.html",
207
+ <a href="/">click here</a> to get back to the start.
208
+ </body>
209
+ </html>
210
+
211
+ @@ test
212
+ from Pyjo.Test import *
213
+
214
+ import Pyjo.Test.Pyjo
215
+
216
+
217
+ t = Pyjo.Test.Pyjo.new('<%= class_ %>')
218
+ t.get_ok('/').status_is(200).content_like(r'(?i)Pyjoyment')
219
+
220
+ done_testing()
221
+
222
+ @@ layout
223
+ <!DOCTYPE html>
224
+ <html>
225
+ <head><title><%%= c.title() %></title></head>
226
+ <body><%%= content() %></body>
227
+ </html>
228
+
229
+ @@ welcome
230
+ %% c.layout('default')
231
+ %% c.title('Welcome')
232
+ <h2><%%= msg %></h2>
233
+ <p>
234
+ This page was generated from the template "templates/example/welcome.html.ep"
235
+ and the layout "templates/layouts/default.html.ep",
236
+ <%%= link_to('click here', url_for()) %> to reload the page or
237
+ <%%= link_to('here', '/index.html') %> to move forward to a static page.
238
+ </p>
239
+
240
+ @@ config
241
+ % from Pyjo.Util import generate_secret
242
+ ---
243
+ secrets:
244
+ - <%= generate_secret() %>
245
+
246
+ @@ agents
247
+ # <%= class_ %>
248
+
249
+ An application of [Pyjoyment](https://pyjoyment.readthedocs.io/), the port of
250
+ Mojolicious to Python, made with `pyjo generate app`. This file says how it is
251
+ made and run, for whoever changes it, a coding agent too.
252
+
253
+ ## Layout
254
+
255
+ - `<%= package %>/__init__.py`: the application, whose `startup` loads the
256
+ configuration and sets up the routes, as `r.get('/').to('Example#welcome')`.
257
+ - `<%= package %>/Controller/`: the controllers, a class for each, whose
258
+ methods are the actions.
259
+ - `templates/`: the `ep` templates of the actions, as
260
+ `templates/example/welcome.html.ep`, and the layouts.
261
+ - `public/`: the static files.
262
+ - `<%= config %>`: the configuration, with the secrets of the sessions.
263
+ - `t/`: the test scripts of `Pyjo.Test` and `Pyjo.Test.Pyjo`.
264
+
265
+ ## Commands
266
+
267
+ ```sh
268
+ % for command, comment in ((f'python -m {class_} daemon', 'http://127.0.0.1:3000'), (f'python -m {class_} routes', 'the routes and their names'), (f'python -m {class_} get /', 'a request without a server'), ('python -m Pyjo.Prove -l -r t', 'the tests'), ('pytest', 'the tests with pytest')):
269
+ <%= f'uv run {command}'.ljust(max(41, len(class_) + 36)) %># <%= comment %>
270
+ % end
271
+ ```
272
+
273
+ ## Conventions
274
+
275
+ - An action renders its response with `self.render(...)`, as
276
+ `self.render(json={...})`; what it returns is not the response.
277
+ - One event loop serves every connection: an action that waits is an
278
+ `async def` that awaits promises, as `await self.ua().get_p(url)`, and
279
+ nothing blocks it, as `time.sleep` or `requests`.
280
+ - A helper is called, `self.ua()`, not `self.ua`.
281
+ - A change comes with a test in `t/`, where `Pyjo.Test.Pyjo` requests the
282
+ application without a server.
283
+
284
+ ## Skills
285
+
286
+ The plugin of Claude Code for Pyjoyment has skills of its idioms, of the
287
+ migration from Flask, FastAPI and Django, of the tests, the templates, the
288
+ APIs and the deployment:
289
+
290
+ ```sh
291
+ claude plugin install pyjoyment --marketplace dex4er/Pyjoyment
292
+ ```
293
+
294
+ Other agents read their files, the `SKILL.md` of each skill, in
295
+ https://github.com/dex4er/Pyjoyment/tree/main/plugins/pyjoyment/skills.
296
+
297
+ @@ claude
298
+ @AGENTS.md
299
+ """
@@ -0,0 +1,281 @@
1
+ """
2
+ Pyjoyment.Command.Author.generate.dockerfile - Dockerfile generator command
3
+ ===========================================================================
4
+ ::
5
+
6
+ Usage: APPLICATION generate dockerfile [OPTIONS]
7
+
8
+ ./myapp.py generate dockerfile
9
+ python -m MyApp generate dockerfile
10
+ python -m MyApp generate dockerfile --user app
11
+ python -m MyApp generate dockerfile --port 8080
12
+ python -m MyApp generate dockerfile --graceful-timeout 8
13
+ python -m MyApp generate dockerfile --healthcheck /
14
+ python -m MyApp generate dockerfile --target lambda
15
+
16
+ Options:
17
+ -G, --graceful-timeout <seconds> Graceful timeout of the workers of the
18
+ server, defaults to the one of the
19
+ server
20
+ -H, --healthcheck <path> Path that the HEALTHCHECK of the image
21
+ requests, defaults to the liveness of
22
+ the plugin Health, if the application
23
+ has it
24
+ -h, --help Show this summary of available options
25
+ -p, --port <port> Port that the server listens on and the
26
+ image exposes, defaults to 3000
27
+ -t, --target <name> Target of the image, "lambda" for an
28
+ event function of AWS Lambda, defaults
29
+ to a server
30
+ -u, --user <name> Create a system user and run the
31
+ application as it, defaults to the user
32
+ of the image
33
+
34
+ :mod:`Pyjoyment.Command.Author.generate.dockerfile` generates ``Dockerfile``
35
+ and ``.dockerignore`` for applications.
36
+
37
+ The image is built in two stages. The first one installs the dependencies
38
+ with uv into ``.venv``: the ones of ``pyproject.toml``, which
39
+ :mod:`Pyjoyment.Command.Author.generate.pyproject` generates, locked with
40
+ ``uv.lock`` if there is one, or the ones of the inline metadata of PEP 723 of
41
+ a :mod:`Pyjoyment.Lite` script that has it and no ``pyproject.toml``, locked
42
+ with its ``.lock`` file if there is one. The second one has Python, ``.venv``
43
+ and the application only, and its command runs the pre-forking server without
44
+ uv, so that the signals of ``docker stop`` go straight to the server, which
45
+ stops gracefully on ``TERM``. The images of Python and of uv are those of the
46
+ versions that run the generator.
47
+
48
+ Docker and Cloud Run give a container 10 seconds to stop, less than the 25
49
+ seconds that :attr:`Pyjo.Server.Prefork.Pyjo_Server_Prefork.graceful_timeout`
50
+ gives a worker, so ``--graceful-timeout 8`` sets ``PYJO_GRACEFUL_TIMEOUT`` in
51
+ the image, which the command ``prefork`` takes for its option, and which the
52
+ environment of the container can change again. With Docker,
53
+ ``docker stop -t 30``, or ``stop_grace_period: 30s`` of Docker Compose, lets
54
+ the workers finish their requests too.
55
+
56
+ The server listens on the port of the ``PORT`` environment variable, which
57
+ the image sets to the port of ``--port``, ``3000`` by default, and exposes,
58
+ and which ``docker build --build-arg PORT=8080`` changes too. A platform as
59
+ Knative, Cloud Run, Render, Koyeb or Heroku sets ``PORT`` for the container,
60
+ and the server listens on its port, as Knative ignores ``EXPOSE`` and sets
61
+ ``PORT`` to the ``containerPort`` of the service, ``8080`` by default.
62
+
63
+ An application with :mod:`Pyjoyment.Plugin.Health` gets a ``HEALTHCHECK``
64
+ that requests its liveness, ``/livez`` by default, or the path of
65
+ ``--healthcheck``, and an empty path leaves it out. Python makes the request
66
+ with :mod:`http.client`, as the image has no ``curl``, and Docker shows the
67
+ health of the container in ``docker ps``, and Docker Compose waits for it with
68
+ ``depends_on`` and ``condition: service_healthy``. Kubernetes and Cloud Run
69
+ ignore it and have probes of their own.
70
+
71
+ With ``--target lambda``, the image is the one of an event function of AWS
72
+ Lambda: its command runs the script of the application, or its package, which
73
+ is the runtime of the function itself, as :mod:`Pyjoyment.Command.lambda`
74
+ detects, with the runtime client of :mod:`Pyjo.Server.Lambda`, and it has no
75
+ port, no ``HEALTHCHECK`` and no graceful timeout of a server, whose options
76
+ are not for it. A function whose Function URL streams its responses has
77
+ ``PYJO_LAMBDA_INVOKE_MODE=RESPONSE_STREAM`` in its environment, as
78
+ :attr:`Pyjo.Server.Lambda.Pyjo_Server_Lambda.invoke_mode` says.
79
+
80
+ This is a core command, that means it is always enabled and its code a good
81
+ example for learning to build new commands, you're welcome to fork it.
82
+
83
+ See :mod:`Pyjoyment.Commands` for a list of commands that are available by
84
+ default.
85
+
86
+ See also
87
+ --------
88
+
89
+ :mod:`Pyjoyment`, https://mojolicious.org.
90
+
91
+ Ported from `Mojolicious::Command::Author::generate::dockerfile
92
+ <https://github.com/mojolicious/mojo/blob/v9.49/lib/Mojolicious/Command/Author/generate/dockerfile.pm>`_ of
93
+ Mojolicious 9.49.
94
+
95
+ Classes
96
+ -------
97
+ """
98
+
99
+ import Pyjo.Exception
100
+ import Pyjo.File
101
+ import Pyjoyment.Command
102
+
103
+ from Pyjo.Util import extract_usage, getopt
104
+
105
+ import json
106
+ import os
107
+ import re
108
+ import shutil
109
+ import subprocess
110
+ import sys
111
+
112
+ from typing import Any
113
+
114
+
115
+ class Pyjoyment_Command_Author_generate_dockerfile(Pyjoyment.Command.object):
116
+ """
117
+ :mod:`Pyjoyment.Command.Author.generate.dockerfile` inherits all
118
+ attributes and methods from :mod:`Pyjoyment.Command` and implements the
119
+ following new ones.
120
+ """
121
+
122
+ description = 'Generate "Dockerfile"'
123
+ """::
124
+
125
+ description = dockerfile.description
126
+ dockerfile.description = 'Foo'
127
+
128
+ Short description of this command, used for the command list.
129
+ """
130
+
131
+ usage = extract_usage()
132
+ """::
133
+
134
+ usage = dockerfile.usage
135
+ dockerfile.usage = 'Foo'
136
+
137
+ Usage information for this command, used for the help screen.
138
+ """
139
+
140
+ def run(self, *args: str) -> None:
141
+ """::
142
+
143
+ dockerfile.run(*sys.argv[1:])
144
+
145
+ Run this command.
146
+ """
147
+ # Not in Mojolicious: the options
148
+ arguments = list(args)
149
+ if not (
150
+ opts := getopt(
151
+ arguments,
152
+ {
153
+ 'G|graceful-timeout=i': None,
154
+ 'H|healthcheck=s': None,
155
+ 'p|port=i': None,
156
+ 't|target=s': None,
157
+ 'u|user=s': None,
158
+ },
159
+ )
160
+ ):
161
+ raise Pyjo.Exception.new(self.usage)
162
+
163
+ # Not in Mojolicious: the target of the image, an event function of AWS Lambda, which has no options of a server
164
+ target: str | None = opts.target
165
+ if target not in (None, 'lambda'):
166
+ raise Pyjo.Exception.new(f'Unknown target: {target}')
167
+ if target == 'lambda':
168
+ for option in ('graceful_timeout', 'healthcheck', 'port'):
169
+ if getattr(opts, option) is not None:
170
+ raise Pyjo.Exception.new(f'Option --{option.replace("_", "-")} is not for the target lambda')
171
+
172
+ name = self.app.moniker
173
+
174
+ # Not in Mojolicious: the HEALTHCHECK of the liveness of Pyjoyment.Plugin.Health, with http.client, as the image
175
+ # has no curl
176
+ # Helpers of a plugin are found at run time, so the application is Any
177
+ app: Any = self.app
178
+ path: str | None = opts.healthcheck
179
+ if path is None and target is None and app.renderer.get_helper('health.path') is not None:
180
+ path = app.health.path('livez')
181
+ port = "int(os.environ['PORT'])"
182
+ code = f"c = http.client.HTTPConnection('127.0.0.1', {port}, timeout=2); c.request('GET', {path!r})"
183
+ healthcheck = (
184
+ json.dumps(
185
+ ['python', '-c', f'import http.client, os, sys; {code}; sys.exit(c.getresponse().status != 200)']
186
+ )
187
+ if path
188
+ else None
189
+ )
190
+
191
+ # Unlike Mojolicious: the script of Pyjoyment.Lite or the package of the application, which python runs, where
192
+ # Mojo runs the script, and the dependencies of the inline metadata of PEP 723 of the script, if it has them and
193
+ # there is no pyproject.toml
194
+ exe = os.environ.get('PYJO_EXE')
195
+ script = Pyjo.File.path(exe).to_rel(self.app.home).to_str() if exe else None
196
+ cmd = ['python', script] if script else ['python', '-m', type(self.app).__module__]
197
+ project = os.path.isfile(self.rel_file('pyproject.toml')) or not exe or not _metadata(Pyjo.File.path(exe))
198
+ lock = 'uv.lock' if project else f'{script}.lock'
199
+ values = {
200
+ 'cmd': json.dumps(cmd if target == 'lambda' else [*cmd, 'prefork']),
201
+ 'graceful_timeout': opts.graceful_timeout,
202
+ 'healthcheck': healthcheck,
203
+ 'lock': lock if os.path.isfile(self.rel_file(lock)) else None,
204
+ 'name': name,
205
+ 'port': opts.port or 3000,
206
+ 'python': f'{sys.version_info.major}.{sys.version_info.minor}',
207
+ 'script': None if project else script,
208
+ 'target': target,
209
+ 'user': opts.user,
210
+ 'uv': _uv(),
211
+ }
212
+ self.render_to_rel_file('dockerfile', 'Dockerfile', values)
213
+
214
+ # Not in Mojolicious: .dockerignore, as the .venv of the host is not the one of the image
215
+ self.render_to_rel_file('dockerignore', '.dockerignore')
216
+
217
+
218
+ # Not in Mojolicious: whether the script has the inline metadata of PEP 723
219
+ def _metadata(path: Pyjo.File.object) -> bool:
220
+ try:
221
+ return re.search(r'(?m)^# /// script\r?$', str(path.slurp('UTF-8') or '')) is not None
222
+ except OSError:
223
+ return False
224
+
225
+
226
+ # Not in Mojolicious: the version of uv, for its image
227
+ def _uv() -> str:
228
+ if (uv := shutil.which('uv')) is None:
229
+ return 'latest'
230
+ try:
231
+ output = subprocess.run([uv, '--version'], capture_output=True, text=True, check=True).stdout
232
+ except (OSError, subprocess.CalledProcessError):
233
+ return 'latest'
234
+ return m[1] if (m := re.match(r'uv (\d+\.\d+\.\d+)', output)) else 'latest'
235
+
236
+
237
+ new = Pyjoyment_Command_Author_generate_dockerfile.new
238
+ object = Pyjoyment_Command_Author_generate_dockerfile
239
+
240
+ __DATA__ = r"""
241
+ @@ dockerfile
242
+ FROM python:<%= python %>-slim AS builder
243
+ COPY --from=ghcr.io/astral-sh/uv:<%= uv %> /uv /bin/
244
+ ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_NO_DEV=1 UV_PYTHON_DOWNLOADS=0
245
+ WORKDIR /opt/<%= name %>
246
+ % if script:
247
+ ENV VIRTUAL_ENV=/opt/<%= name %>/.venv
248
+ COPY <%= ' '.join([script, *([lock] if lock else [])]) %> ./
249
+ RUN uv venv && uv sync --script <%= script %> --active<%= ' --locked' if lock else '' %>
250
+ % else:
251
+ COPY <%= ' '.join(['pyproject.toml', *([lock] if lock else [])]) %> ./
252
+ RUN uv sync --no-install-project<%= ' --locked' if lock else '' %>
253
+ % end
254
+
255
+ FROM python:<%= python %>-slim
256
+ WORKDIR /opt/<%= name %>
257
+ COPY --from=builder /opt/<%= name %>/.venv .venv
258
+ COPY . .
259
+ ENV PATH="/opt/<%= name %>/.venv/bin:$PATH"
260
+ % if user:
261
+ RUN useradd --system <%= user %>
262
+ USER <%= user %>
263
+ % end
264
+ % if target != 'lambda':
265
+ ARG PORT=<%= port %>
266
+ ENV PORT=${PORT}
267
+ % if graceful_timeout is not None:
268
+ ENV PYJO_GRACEFUL_TIMEOUT=<%= graceful_timeout %>
269
+ % end
270
+ EXPOSE ${PORT}
271
+ % if healthcheck:
272
+ HEALTHCHECK --timeout=3s --start-period=30s --start-interval=1s CMD <%= healthcheck %>
273
+ % end
274
+ % end
275
+ CMD <%= cmd %>
276
+
277
+ @@ dockerignore
278
+ .git
279
+ .venv
280
+ __pycache__
281
+ """