Pyjoyment 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- Pyjo/App/ASGI.py +735 -0
- Pyjo/App/CGI.py +773 -0
- Pyjo/App/FastCGI.py +631 -0
- Pyjo/App/Lambda.py +987 -0
- Pyjo/App/WSGI.py +427 -0
- Pyjo/App/__init__.py +409 -0
- Pyjo/Asset/File.py +377 -0
- Pyjo/Asset/Memory.py +235 -0
- Pyjo/Asset/__init__.py +200 -0
- Pyjo/Base.py +215 -0
- Pyjo/Cache.py +119 -0
- Pyjo/Collection.py +570 -0
- Pyjo/Content/MultiPart.py +325 -0
- Pyjo/Content/Single.py +193 -0
- Pyjo/Content/__init__.py +806 -0
- Pyjo/Cookie/Request.py +90 -0
- Pyjo/Cookie/Response.py +294 -0
- Pyjo/Cookie/__init__.py +143 -0
- Pyjo/DOM/CSS.py +1019 -0
- Pyjo/DOM/HTML.py +550 -0
- Pyjo/DOM/__init__.py +1329 -0
- Pyjo/Date.py +294 -0
- Pyjo/EventEmitter.py +522 -0
- Pyjo/Exception.py +494 -0
- Pyjo/File.py +959 -0
- Pyjo/GetOpt.py +605 -0
- Pyjo/Headers.py +760 -0
- Pyjo/HelloWorld.py +77 -0
- Pyjo/Home.py +109 -0
- Pyjo/IOLoop/ChildProcess.py +591 -0
- Pyjo/IOLoop/Client.py +736 -0
- Pyjo/IOLoop/Datagram.py +982 -0
- Pyjo/IOLoop/Messages.py +664 -0
- Pyjo/IOLoop/Server.py +458 -0
- Pyjo/IOLoop/Stdio.py +446 -0
- Pyjo/IOLoop/Stream.py +644 -0
- Pyjo/IOLoop/Subprocess.py +419 -0
- Pyjo/IOLoop/TLS.py +360 -0
- Pyjo/IOLoop/__init__.py +1036 -0
- Pyjo/IOLoop/certs/README.md +41 -0
- Pyjo/IOLoop/certs/ca.crt +20 -0
- Pyjo/IOLoop/certs/server.crt +21 -0
- Pyjo/IOLoop/certs/server.key +28 -0
- Pyjo/JSON/Pointer.py +135 -0
- Pyjo/JSON/RPC.py +772 -0
- Pyjo/JSON/Schema.py +2506 -0
- Pyjo/JSON/__init__.py +573 -0
- Pyjo/Loader.py +342 -0
- Pyjo/Log.py +723 -0
- Pyjo/Message/Request.py +725 -0
- Pyjo/Message/Response.py +476 -0
- Pyjo/Message/__init__.py +830 -0
- Pyjo/Parameters.py +472 -0
- Pyjo/Path.py +419 -0
- Pyjo/Promise.py +894 -0
- Pyjo/Prove.py +1305 -0
- Pyjo/Reactor/Asyncio.py +854 -0
- Pyjo/Reactor/Poll.py +439 -0
- Pyjo/Reactor/__init__.py +525 -0
- Pyjo/SSE.py +119 -0
- Pyjo/Server/ASGI.py +486 -0
- Pyjo/Server/CGI.py +259 -0
- Pyjo/Server/Daemon.py +1007 -0
- Pyjo/Server/FastCGI.py +517 -0
- Pyjo/Server/Hot/__init__.py +687 -0
- Pyjo/Server/Hot/__main__.py +5 -0
- Pyjo/Server/Lambda.py +641 -0
- Pyjo/Server/Prefork.py +917 -0
- Pyjo/Server/Prethread.py +394 -0
- Pyjo/Server/Reload/Backend/Poll.py +114 -0
- Pyjo/Server/Reload/Backend/__init__.py +126 -0
- Pyjo/Server/Reload/__init__.py +482 -0
- Pyjo/Server/Reload/__main__.py +5 -0
- Pyjo/Server/Stdio.py +228 -0
- Pyjo/Server/WSGI.py +349 -0
- Pyjo/Server/__init__.py +396 -0
- Pyjo/String/Bytes.py +532 -0
- Pyjo/String/HTML.py +210 -0
- Pyjo/String/Mixin.py +333 -0
- Pyjo/String/Template.py +470 -0
- Pyjo/String/Unicode.py +553 -0
- Pyjo/String/__init__.py +0 -0
- Pyjo/TOML.py +232 -0
- Pyjo/Template.py +1290 -0
- Pyjo/Test/Pyjo.py +1454 -0
- Pyjo/Test/__init__.py +1233 -0
- Pyjo/Test/pytest_plugin.py +305 -0
- Pyjo/Transaction/HTTP.py +374 -0
- Pyjo/Transaction/WebSocket.py +709 -0
- Pyjo/Transaction/__init__.py +429 -0
- Pyjo/URL/Template.py +459 -0
- Pyjo/URL/__init__.py +651 -0
- Pyjo/Upload.py +139 -0
- Pyjo/UserAgent/CookieJar.py +391 -0
- Pyjo/UserAgent/Proxy.py +138 -0
- Pyjo/UserAgent/Server.py +239 -0
- Pyjo/UserAgent/Transactor.py +766 -0
- Pyjo/UserAgent/__init__.py +1243 -0
- Pyjo/Util.py +3567 -0
- Pyjo/WebSocket.py +332 -0
- Pyjo/YAML.py +1167 -0
- Pyjo/__init__.py +441 -0
- Pyjo/__main__.py +35 -0
- Pyjo/py.typed +0 -0
- Pyjoyment/Command/Author/__init__.py +20 -0
- Pyjoyment/Command/Author/generate/__init__.py +106 -0
- Pyjoyment/Command/Author/generate/app.py +299 -0
- Pyjoyment/Command/Author/generate/dockerfile.py +281 -0
- Pyjoyment/Command/Author/generate/kubernetes.py +514 -0
- Pyjoyment/Command/Author/generate/lambda_zip.py +372 -0
- Pyjoyment/Command/Author/generate/lite_app.py +113 -0
- Pyjoyment/Command/Author/generate/plugin.py +267 -0
- Pyjoyment/Command/Author/generate/pyproject.py +123 -0
- Pyjoyment/Command/Author/inflate.py +101 -0
- Pyjoyment/Command/__init__.py +332 -0
- Pyjoyment/Command/asgi.py +92 -0
- Pyjoyment/Command/cgi.py +90 -0
- Pyjoyment/Command/daemon.py +160 -0
- Pyjoyment/Command/eval.py +128 -0
- Pyjoyment/Command/fastcgi.py +99 -0
- Pyjoyment/Command/get.py +305 -0
- Pyjoyment/Command/lambda.py +117 -0
- Pyjoyment/Command/prefork.py +181 -0
- Pyjoyment/Command/prethread.py +126 -0
- Pyjoyment/Command/routes.py +133 -0
- Pyjoyment/Command/stdio.py +92 -0
- Pyjoyment/Command/version.py +141 -0
- Pyjoyment/Command/wsgi.py +93 -0
- Pyjoyment/Commands.py +546 -0
- Pyjoyment/Controller.py +1307 -0
- Pyjoyment/HelloWorld.py +56 -0
- Pyjoyment/Lite.py +494 -0
- Pyjoyment/Plugin/AllowedHosts.py +205 -0
- Pyjoyment/Plugin/Config.py +248 -0
- Pyjoyment/Plugin/DefaultHelpers.py +1202 -0
- Pyjoyment/Plugin/EPLRenderer.py +134 -0
- Pyjoyment/Plugin/EPRenderer.py +212 -0
- Pyjoyment/Plugin/HeaderCondition.py +98 -0
- Pyjoyment/Plugin/Health.py +325 -0
- Pyjoyment/Plugin/JSONConfig.py +172 -0
- Pyjoyment/Plugin/Mount.py +223 -0
- Pyjoyment/Plugin/PyRenderer.py +209 -0
- Pyjoyment/Plugin/TOMLConfig.py +141 -0
- Pyjoyment/Plugin/TagHelpers.py +959 -0
- Pyjoyment/Plugin/YAMLConfig.py +178 -0
- Pyjoyment/Plugin/__init__.py +80 -0
- Pyjoyment/Plugins.py +215 -0
- Pyjoyment/Renderer.py +673 -0
- Pyjoyment/Routes/Match.py +251 -0
- Pyjoyment/Routes/Pattern.py +443 -0
- Pyjoyment/Routes/Route.py +827 -0
- Pyjoyment/Routes/__init__.py +419 -0
- Pyjoyment/Sessions.py +255 -0
- Pyjoyment/Static.py +421 -0
- Pyjoyment/Types.py +238 -0
- Pyjoyment/Validator/Validation.py +395 -0
- Pyjoyment/Validator/__init__.py +304 -0
- Pyjoyment/__init__.py +1083 -0
- Pyjoyment/__main__.py +5 -0
- Pyjoyment/py.typed +0 -0
- Pyjoyment/resources/public/favicon.ico +0 -0
- Pyjoyment/resources/public/favicon.svg +345 -0
- Pyjoyment/resources/public/pyjo/bootstrap/LICENSE +21 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.bundle.min.js.map +1 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.css +6 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.js +7 -0
- Pyjoyment/resources/public/pyjo/bootstrap/bootstrap.min.css.map +1 -0
- Pyjoyment/resources/public/pyjo/failunicat.png +0 -0
- Pyjoyment/resources/public/pyjo/failunicat.svg +50 -0
- Pyjoyment/resources/public/pyjo/highlight.js/LICENSE +29 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight-mojo-dark.css +56 -0
- Pyjoyment/resources/public/pyjo/highlight.js/highlight.min.js +1262 -0
- Pyjoyment/resources/public/pyjo/highlight.js/pyjoyment.min.js +1 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-black-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-2x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white-4x.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo-white.png +0 -0
- Pyjoyment/resources/public/pyjo/logo-white.svg +346 -0
- Pyjoyment/resources/public/pyjo/logo.png +0 -0
- Pyjoyment/resources/public/pyjo/logo.svg +346 -0
- Pyjoyment/resources/public/pyjo/notfound.png +0 -0
- Pyjoyment/resources/public/pyjo/notfound.svg +3 -0
- Pyjoyment/resources/public/pyjo/nounicat.png +0 -0
- Pyjoyment/resources/public/pyjo/nounicat.svg +5 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-dark.svg +4 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.png +0 -0
- Pyjoyment/resources/public/pyjo/pinstripe-light.svg +4 -0
- Pyjoyment/resources/public/pyjo/pyjo.css +204 -0
- Pyjoyment/resources/templates/pyjo/debug.html.ep +346 -0
- Pyjoyment/resources/templates/pyjo/exception.html.ep +32 -0
- Pyjoyment/resources/templates/pyjo/not_found.html.ep +45 -0
- pyjoyment-0.1.0.dist-info/METADATA +376 -0
- pyjoyment-0.1.0.dist-info/RECORD +200 -0
- pyjoyment-0.1.0.dist-info/WHEEL +4 -0
- pyjoyment-0.1.0.dist-info/entry_points.txt +8 -0
- pyjoyment-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -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
|
+
"""
|