pywire-templates 0.2.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.
@@ -0,0 +1,29 @@
1
+ """Shared Jinja2 templates for PyWire deploy + scaffolding."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib import resources
6
+ from typing import Any
7
+
8
+ import jinja2
9
+
10
+ __version__ = "0.2.0"
11
+
12
+
13
+ def _deploy_env() -> jinja2.Environment:
14
+ return jinja2.Environment(
15
+ loader=jinja2.PackageLoader("pywire_templates", "deploy"),
16
+ trim_blocks=True,
17
+ lstrip_blocks=True,
18
+ keep_trailing_newline=True,
19
+ )
20
+
21
+
22
+ def render_deploy_template(name: str, **context: Any) -> str:
23
+ """Render a deploy template by file name (e.g. ``Dockerfile.j2``)."""
24
+ return _deploy_env().get_template(name).render(**context)
25
+
26
+
27
+ def deploy_template_path(name: str) -> str:
28
+ """Return absolute path to a deploy template file — useful for copies."""
29
+ return str(resources.files("pywire_templates").joinpath("deploy", name))
@@ -0,0 +1,14 @@
1
+ FROM python:3.12-slim
2
+ WORKDIR /app
3
+
4
+ COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
5
+ COPY pyproject.toml uv.lock ./
6
+ RUN uv sync --frozen --no-dev
7
+
8
+ COPY . .
9
+
10
+ # Session timeout in seconds (default: 1800 = 30 min)
11
+ ENV SESSION_TTL=1800
12
+
13
+ EXPOSE 8000
14
+ CMD ["uv", "run", "pywire", "run", "--host", "0.0.0.0", "--port", "8000", "--workers", "{{ workers }}"]
@@ -0,0 +1,35 @@
1
+ import asgi
2
+ from {{ app_module }} import {{ app_attr }}
3
+ import _routes # noqa: F401 — registers routes as a side effect
4
+ from pywire_do import PyWireSessionDO
5
+ from workers import WorkerEntrypoint, Response
6
+ from js import URL, Request as JsRequest
7
+ import uuid
8
+
9
+
10
+ class Default(WorkerEntrypoint):
11
+ async def on_fetch(self, request):
12
+ url = URL.new(request.url)
13
+ upgrade = (request.headers.get("Upgrade") or "").lower()
14
+
15
+ if upgrade == "websocket":
16
+ session_id = url.searchParams.get("session") or str(uuid.uuid4())
17
+ stub = self.env.PYWIRE_SESSION.getByName(session_id)
18
+ return await stub.fetch(request)
19
+
20
+ # Pre-warm a Durable Object so it's ready when the client opens a WS.
21
+ # The DO cold start overlaps with the browser loading HTML + JS.
22
+ session_id = str(uuid.uuid4())
23
+ stub = self.env.PYWIRE_SESSION.getByName(session_id)
24
+ self.ctx.waitUntil(stub.fetch(JsRequest.new("http://do/warm")))
25
+
26
+ # Serve the page via ASGI
27
+ response = await asgi.fetch({{ app_attr }}, request.js_object, self.env)
28
+
29
+ # Inject pre-assigned session into the HTML so the client
30
+ # connects to the already-warming DO instead of creating a new one.
31
+ body = await response.text()
32
+ meta = '<meta name="pywire-session" content="' + session_id + '">'
33
+ body = body.replace("</head>", meta + "</head>", 1)
34
+
35
+ return Response.new(body, status=response.status, headers=response.headers)
@@ -0,0 +1,19 @@
1
+ app = "{{ project_name }}"
2
+ primary_region = "ord"
3
+
4
+ [build]
5
+ dockerfile = "Dockerfile"
6
+
7
+ [http_service]
8
+ internal_port = 8000
9
+ force_https = true
10
+ auto_stop_machines = "stop"
11
+ auto_start_machines = true
12
+
13
+ [env]
14
+ # Session timeout in seconds (default: 1800 = 30 min)
15
+ SESSION_TTL = "1800"
16
+
17
+ [[vm]]
18
+ memory = "512mb"
19
+ cpus = 1
@@ -0,0 +1,180 @@
1
+ """PyWire Durable Object — session actor with WebSocket hibernation.
2
+
3
+ This module is generated by `pywire build --platform cloudflare`.
4
+ Uses shared pywire.runtime modules for protocol + page resolution.
5
+
6
+ All imports are at the top level so they are captured in the Cloudflare
7
+ Workers memory snapshot at deploy time, giving fast cold starts.
8
+ """
9
+
10
+ from workers import DurableObject, Response
11
+ from js import WebSocketPair
12
+ from pyodide.ffi import to_js
13
+ from {{ app_module }} import {{ app_attr }}
14
+ import _routes # noqa: F401 — registers routes as a side effect
15
+ import msgpack
16
+ from pywire.runtime.session_serializer import snapshot_page_state, restore_page_state
17
+ from pywire.runtime.protocol import build_update_payload
18
+ from pywire.runtime.page_resolver import resolve_page
19
+
20
+
21
+ def _send(ws, msg):
22
+ ws.send(to_js(msgpack.packb(msg)))
23
+
24
+
25
+ def _send_update(ws, update):
26
+ _send(ws, build_update_payload(update))
27
+
28
+
29
+ def _decode(message):
30
+ try:
31
+ if isinstance(message, (bytes, bytearray, memoryview)):
32
+ return msgpack.unpackb(message, raw=False)
33
+ if hasattr(message, "to_py"):
34
+ return msgpack.unpackb(bytes(message.to_py()), raw=False)
35
+ return {}
36
+ except Exception:
37
+ return {}
38
+
39
+
40
+ def _resolve_page(path):
41
+ result = resolve_page({{ app_attr }}.router, path)
42
+ if result is None:
43
+ return None
44
+ page, _params, _variant_name = result
45
+ return page
46
+
47
+
48
+ class PyWireSessionDO(DurableObject):
49
+ def __init__(self, state, env):
50
+ super().__init__(state, env)
51
+ self.ctx = state
52
+ self.page = None
53
+
54
+ async def on_fetch(self, request):
55
+ upgrade = (request.headers.get("Upgrade") or "").lower()
56
+ if upgrade == "websocket":
57
+ pair = WebSocketPair.new()
58
+ client, server = pair.object_values()
59
+ self.ctx.acceptWebSocket(server)
60
+ return Response(None, status=101, web_socket=client)
61
+ return Response("OK", headers={"Content-Type": "text/plain"})
62
+
63
+ async def on_webSocketMessage(self, ws, message):
64
+ msg = _decode(message)
65
+ msg_type = msg.get("type", "")
66
+ try:
67
+ if msg_type == "init":
68
+ await self._handle_init(ws, msg)
69
+ elif msg_type == "event":
70
+ await self._handle_event(ws, msg)
71
+ elif msg_type == "relocate":
72
+ await self._handle_relocate(ws, msg)
73
+ elif msg_type == "pong":
74
+ pass
75
+ except Exception as e:
76
+ import traceback
77
+ traceback.print_exc()
78
+ _send(ws, {"type": "error", "error": str(e)})
79
+
80
+ async def on_webSocketClose(self, ws, code, reason, was_clean):
81
+ if self.page:
82
+ await self._persist_state()
83
+ ws.close(code, reason)
84
+
85
+ async def _restore_from_storage(self, page):
86
+ snapshot = await self.ctx.storage.get("page_state")
87
+ if snapshot:
88
+ try:
89
+ state = snapshot.to_py() if hasattr(snapshot, "to_py") else snapshot
90
+ restore_page_state(page, state)
91
+ return True
92
+ except Exception:
93
+ pass
94
+ return False
95
+
96
+ def _setup_broadcaster(self, ws, page):
97
+ async def broadcast_update():
98
+ update = await page.render_update(init=False)
99
+ _send_update(ws, update)
100
+ page._on_update = broadcast_update
101
+
102
+ async def _handle_init(self, ws, msg):
103
+ path = msg.get("path", "/")
104
+ self.page = _resolve_page(path)
105
+ if not self.page:
106
+ _send(ws, {"type": "error", "error": "Not Found"})
107
+ return
108
+
109
+ session_restored = await self._restore_from_storage(self.page)
110
+ self._setup_broadcaster(ws, self.page)
111
+ await self.page.render(init=True)
112
+
113
+ if self.page._pending_navigation:
114
+ _send(ws, {"type": "navigate", "path": self.page._pending_navigation})
115
+ self.page._pending_navigation = None
116
+ return
117
+
118
+ _send(ws, {
119
+ "type": "init_ack",
120
+ "session_id": "",
121
+ "session_restored": session_restored,
122
+ })
123
+ await self.page._run_hooks(self.page.MOUNT_HOOKS)
124
+ await self._persist_state()
125
+
126
+ async def _handle_event(self, ws, msg):
127
+ handler_name = msg.get("handler")
128
+ event_data = msg.get("data", {})
129
+
130
+ if not self.page:
131
+ self.page = _resolve_page(msg.get("path", "/"))
132
+ if not self.page:
133
+ _send(ws, {"type": "error", "error": "No page"})
134
+ return
135
+ await self._restore_from_storage(self.page)
136
+ await self.page.render(init=True)
137
+
138
+ self._setup_broadcaster(ws, self.page)
139
+
140
+ if handler_name:
141
+ update = await self.page.handle_event(handler_name, event_data)
142
+ else:
143
+ update = await self.page.render_update(init=False)
144
+
145
+ if self.page._pending_navigation:
146
+ _send(ws, {"type": "navigate", "path": self.page._pending_navigation})
147
+ self.page._pending_navigation = None
148
+ return
149
+
150
+ _send_update(ws, update)
151
+ await self.page._run_hooks(self.page.AFTER_UPDATE_HOOKS)
152
+ await self._persist_state()
153
+
154
+ async def _handle_relocate(self, ws, msg):
155
+ path = msg.get("path", "/")
156
+ old_page = self.page
157
+
158
+ self.page = _resolve_page(path)
159
+ if not self.page:
160
+ _send(ws, {"type": "reload"})
161
+ return
162
+
163
+ if old_page:
164
+ self.page.user = getattr(old_page, "user", None)
165
+
166
+ self._setup_broadcaster(ws, self.page)
167
+ response = await self.page.render(init=False)
168
+ html = response.body.decode("utf-8") if isinstance(response.body, bytes) else response.body
169
+ _send(ws, {"type": "update", "html": html})
170
+ await self.page._run_hooks(self.page.MOUNT_HOOKS)
171
+ await self._persist_state()
172
+
173
+ async def _persist_state(self):
174
+ if not self.page:
175
+ return
176
+ try:
177
+ snapshot = snapshot_page_state(self.page)
178
+ await self.ctx.storage.put("page_state", to_js(snapshot))
179
+ except Exception:
180
+ pass
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://railway.com/railway.schema.json",
3
+ "build": {
4
+ "dockerfilePath": "Dockerfile"
5
+ }
6
+ }
@@ -0,0 +1,25 @@
1
+ services:
2
+ - type: web
3
+ name: {{ project_name }}
4
+ runtime: docker
5
+ {% if redis_enabled %}
6
+ plan: starter
7
+ envVars:
8
+ - key: REDIS_URL
9
+ fromService:
10
+ name: {{ project_name }}-kv
11
+ type: keyvalue
12
+ property: connectionString
13
+ - key: SESSION_TTL
14
+ value: "1800" # Session timeout in seconds (default: 30 min)
15
+
16
+ - type: keyvalue
17
+ name: {{ project_name }}-kv
18
+ plan: starter
19
+ ipAllowList: []
20
+ {% else %}
21
+ plan: free
22
+ envVars:
23
+ - key: SESSION_TTL
24
+ value: "1800" # Session timeout in seconds (default: 30 min)
25
+ {% endif %}
@@ -0,0 +1,18 @@
1
+ name = "{{ project_name }}"
2
+ main = "entry.py"
3
+ compatibility_date = "2025-01-01"
4
+ compatibility_flags = ["python_workers"]
5
+ workers_dev = false
6
+ preview_urls = false
7
+
8
+ [assets]
9
+ directory = "./.pywire/deploy/public"
10
+ binding = "ASSETS"
11
+
12
+ [[durable_objects.bindings]]
13
+ name = "PYWIRE_SESSION"
14
+ class_name = "PyWireSessionDO"
15
+
16
+ [[migrations]]
17
+ tag = "v1"
18
+ new_sqlite_classes = ["PyWireSessionDO"]
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.4
2
+ Name: pywire-templates
3
+ Version: 0.2.0
4
+ Summary: Shared Jinja2 templates for PyWire deployment and scaffolding
5
+ Project-URL: Homepage, https://pywire.dev
6
+ Project-URL: Repository, https://github.com/pywire/pywire
7
+ Author-email: Reece Holmdahl <reece@pywire.dev>
8
+ License-Expression: MIT
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Software Development :: Code Generators
16
+ Requires-Python: >=3.11
17
+ Requires-Dist: jinja2>=3.1.0
18
+ Description-Content-Type: text/markdown
19
+
20
+ # pywire-templates
21
+
22
+ Shared Jinja2 templates consumed by `pywire-cli` (for `pywire deploy`) and `create-pywire-app` (for project scaffolding).
23
+
24
+ End users should not depend on this package directly. It exists so deployment configs (Dockerfile, fly.toml, render.yaml, wrangler.toml, railway.json, Cloudflare entry/DO) live in one place.
25
+
26
+ ## Layout
27
+
28
+ ```
29
+ src/pywire_templates/
30
+ deploy/ # deployment config templates
31
+ Dockerfile.j2
32
+ fly.toml.j2
33
+ render.yaml.j2
34
+ wrangler.toml.j2
35
+ railway.json.j2
36
+ entry.py.j2
37
+ pywire_do.py.j2
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ ```python
43
+ from pywire_templates import render_deploy_template
44
+
45
+ content = render_deploy_template("Dockerfile.j2", workers=4)
46
+ ```
@@ -0,0 +1,11 @@
1
+ pywire_templates/__init__.py,sha256=ceclc3g9hgCRfwyh7smPC7sk3TtAWIvrMaNqMFumLng,833
2
+ pywire_templates/deploy/Dockerfile.j2,sha256=KUVcV0_p9Dxdw6CaFKQmE5CsZXlhgixDRNvxQalwOlE,352
3
+ pywire_templates/deploy/entry.py.j2,sha256=1UlTdjheYJ6PKn1tDQJaFk4StYW6hsgMGKUPE0hxKRU,1499
4
+ pywire_templates/deploy/fly.toml.j2,sha256=k8rh159RpI1bc_1mCNcro4FLUyRtrEh2KwyNVkSumyk,330
5
+ pywire_templates/deploy/pywire_do.py.j2,sha256=KcrOMRok_-XA9IZHOFMr8T2DJyiCdfrObRL-Q2WENpc,6108
6
+ pywire_templates/deploy/railway.json.j2,sha256=l5eYYAkAvk7VmT-DNVybj8J82ws1Tw_zb6ESaTI9G-U,112
7
+ pywire_templates/deploy/render.yaml.j2,sha256=mIqoc2j0TJPCAEdUgs2oxuih_8C0s9usss-VdO46svk,602
8
+ pywire_templates/deploy/wrangler.toml.j2,sha256=hNv6H89fi1tVxiwILCoLC3PusWtLvqzxmXvITm9wuTs,382
9
+ pywire_templates-0.2.0.dist-info/METADATA,sha256=2UDhoncqcis5VyLmt_jbMFsY-Z1ZqnrnjsS98suyCvM,1441
10
+ pywire_templates-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
11
+ pywire_templates-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any