pywire 0.1.1__py3-none-any.whl → 0.1.3__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 (103) hide show
  1. pywire/__init__.py +2 -0
  2. pywire/cli/__init__.py +1 -0
  3. pywire/cli/generators.py +48 -0
  4. pywire/cli/main.py +309 -0
  5. pywire/cli/tui.py +563 -0
  6. pywire/cli/validate.py +26 -0
  7. pywire/client/.prettierignore +8 -0
  8. pywire/client/.prettierrc +7 -0
  9. pywire/client/build.mjs +73 -0
  10. pywire/client/eslint.config.js +46 -0
  11. pywire/client/package.json +39 -0
  12. pywire/client/pnpm-lock.yaml +2971 -0
  13. pywire/client/src/core/app.ts +263 -0
  14. pywire/client/src/core/dom-updater.test.ts +78 -0
  15. pywire/client/src/core/dom-updater.ts +321 -0
  16. pywire/client/src/core/index.ts +5 -0
  17. pywire/client/src/core/transport-manager.test.ts +179 -0
  18. pywire/client/src/core/transport-manager.ts +159 -0
  19. pywire/client/src/core/transports/base.ts +122 -0
  20. pywire/client/src/core/transports/http.ts +142 -0
  21. pywire/client/src/core/transports/index.ts +13 -0
  22. pywire/client/src/core/transports/websocket.ts +97 -0
  23. pywire/client/src/core/transports/webtransport.ts +149 -0
  24. pywire/client/src/dev/dev-app.ts +93 -0
  25. pywire/client/src/dev/error-trace.test.ts +97 -0
  26. pywire/client/src/dev/error-trace.ts +76 -0
  27. pywire/client/src/dev/index.ts +4 -0
  28. pywire/client/src/dev/status-overlay.ts +63 -0
  29. pywire/client/src/events/handler.test.ts +318 -0
  30. pywire/client/src/events/handler.ts +454 -0
  31. pywire/client/src/pywire.core.ts +22 -0
  32. pywire/client/src/pywire.dev.ts +27 -0
  33. pywire/client/tsconfig.json +17 -0
  34. pywire/client/vitest.config.ts +15 -0
  35. pywire/compiler/__init__.py +6 -0
  36. pywire/compiler/ast_nodes.py +304 -0
  37. pywire/compiler/attributes/__init__.py +6 -0
  38. pywire/compiler/attributes/base.py +24 -0
  39. pywire/compiler/attributes/conditional.py +37 -0
  40. pywire/compiler/attributes/events.py +55 -0
  41. pywire/compiler/attributes/form.py +37 -0
  42. pywire/compiler/attributes/loop.py +75 -0
  43. pywire/compiler/attributes/reactive.py +34 -0
  44. pywire/compiler/build.py +28 -0
  45. pywire/compiler/build_artifacts.py +342 -0
  46. pywire/compiler/codegen/__init__.py +5 -0
  47. pywire/compiler/codegen/attributes/__init__.py +6 -0
  48. pywire/compiler/codegen/attributes/base.py +19 -0
  49. pywire/compiler/codegen/attributes/events.py +35 -0
  50. pywire/compiler/codegen/directives/__init__.py +6 -0
  51. pywire/compiler/codegen/directives/base.py +16 -0
  52. pywire/compiler/codegen/directives/path.py +53 -0
  53. pywire/compiler/codegen/generator.py +2341 -0
  54. pywire/compiler/codegen/template.py +2178 -0
  55. pywire/compiler/directives/__init__.py +7 -0
  56. pywire/compiler/directives/base.py +20 -0
  57. pywire/compiler/directives/component.py +33 -0
  58. pywire/compiler/directives/context.py +93 -0
  59. pywire/compiler/directives/layout.py +49 -0
  60. pywire/compiler/directives/no_spa.py +24 -0
  61. pywire/compiler/directives/path.py +71 -0
  62. pywire/compiler/directives/props.py +88 -0
  63. pywire/compiler/exceptions.py +19 -0
  64. pywire/compiler/interpolation/__init__.py +6 -0
  65. pywire/compiler/interpolation/base.py +28 -0
  66. pywire/compiler/interpolation/jinja.py +272 -0
  67. pywire/compiler/parser.py +750 -0
  68. pywire/compiler/paths.py +29 -0
  69. pywire/compiler/preprocessor.py +43 -0
  70. pywire/core/wire.py +119 -0
  71. pywire/py.typed +0 -0
  72. pywire/runtime/__init__.py +7 -0
  73. pywire/runtime/aioquic_server.py +194 -0
  74. pywire/runtime/app.py +901 -0
  75. pywire/runtime/compile_error_page.py +195 -0
  76. pywire/runtime/debug.py +203 -0
  77. pywire/runtime/dev_server.py +433 -0
  78. pywire/runtime/dev_server.py.broken +268 -0
  79. pywire/runtime/error_page.py +64 -0
  80. pywire/runtime/error_renderer.py +23 -0
  81. pywire/runtime/escape.py +23 -0
  82. pywire/runtime/files.py +40 -0
  83. pywire/runtime/helpers.py +97 -0
  84. pywire/runtime/http_transport.py +253 -0
  85. pywire/runtime/loader.py +272 -0
  86. pywire/runtime/logging.py +72 -0
  87. pywire/runtime/page.py +384 -0
  88. pywire/runtime/pydantic_integration.py +52 -0
  89. pywire/runtime/router.py +229 -0
  90. pywire/runtime/server.py +25 -0
  91. pywire/runtime/style_collector.py +31 -0
  92. pywire/runtime/upload_manager.py +76 -0
  93. pywire/runtime/validation.py +449 -0
  94. pywire/runtime/websocket.py +665 -0
  95. pywire/runtime/webtransport_handler.py +195 -0
  96. pywire/static/pywire.core.min.js +3 -0
  97. pywire/static/pywire.dev.min.js +20 -0
  98. {pywire-0.1.1.dist-info → pywire-0.1.3.dist-info}/METADATA +1 -1
  99. pywire-0.1.3.dist-info/RECORD +106 -0
  100. pywire-0.1.1.dist-info/RECORD +0 -9
  101. {pywire-0.1.1.dist-info → pywire-0.1.3.dist-info}/WHEEL +0 -0
  102. {pywire-0.1.1.dist-info → pywire-0.1.3.dist-info}/entry_points.txt +0 -0
  103. {pywire-0.1.1.dist-info → pywire-0.1.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,149 @@
1
+ import { BaseTransport, ServerMessage } from './base'
2
+
3
+ /**
4
+ * WebTransport implementation using the browser's native WebTransport API.
5
+ * WebTransport provides lower latency than WebSocket via HTTP/3 and QUIC.
6
+ */
7
+ export class WebTransportTransport extends BaseTransport {
8
+ readonly name = 'WebTransport'
9
+
10
+ private transport: WebTransport | null = null
11
+ private writer: WritableStreamDefaultWriter<Uint8Array> | null = null
12
+ private readonly url: string
13
+ private encoder = new TextEncoder()
14
+ private decoder = new TextDecoder()
15
+
16
+ constructor(url?: string) {
17
+ super()
18
+ this.url = url || this.getDefaultUrl()
19
+ }
20
+
21
+ private getDefaultUrl(): string {
22
+ // WebTransport requires HTTPS
23
+ return `https://${window.location.host}/_pywire/webtransport`
24
+ }
25
+
26
+ /**
27
+ * Check if WebTransport is available in this browser.
28
+ */
29
+ static isSupported(): boolean {
30
+ return typeof WebTransport !== 'undefined'
31
+ }
32
+
33
+ async connect(): Promise<void> {
34
+ if (!WebTransportTransport.isSupported()) {
35
+ throw new Error('WebTransport not supported in this browser')
36
+ }
37
+
38
+ try {
39
+ // Check for self-signed cert hash (Dev Mode)
40
+ const options: WebTransportOptions = {}
41
+ const certHash = (window as Window & { PYWIRE_CERT_HASH?: number[] }).PYWIRE_CERT_HASH
42
+ if (certHash && Array.isArray(certHash)) {
43
+ options.serverCertificateHashes = [
44
+ {
45
+ algorithm: 'sha-256',
46
+ value: new Uint8Array(certHash).buffer,
47
+ },
48
+ ]
49
+ console.log('PyWire: Using explicit certificate hash for WebTransport')
50
+ }
51
+
52
+ this.transport = new WebTransport(this.url, options)
53
+
54
+ // Wait for the connection to be ready
55
+ await this.transport.ready
56
+
57
+ console.log('PyWire: WebTransport ready')
58
+ this.connected = true
59
+
60
+ // Start reading incoming streams
61
+ this.startReading()
62
+ } catch (e) {
63
+ this.handleDisconnect()
64
+ throw e
65
+ }
66
+ }
67
+
68
+ private async startReading(): Promise<void> {
69
+ if (!this.transport) return
70
+
71
+ // Read from bidirectional streams
72
+ const reader = this.transport.incomingBidirectionalStreams.getReader()
73
+
74
+ try {
75
+ while (true) {
76
+ const { value: stream, done } = await reader.read()
77
+ if (done) break
78
+
79
+ // Handle each incoming stream
80
+ this.handleStream(stream)
81
+ }
82
+ } catch (e) {
83
+ if (this.connected) {
84
+ console.error('PyWire: WebTransport read error', e)
85
+ this.handleDisconnect()
86
+ }
87
+ }
88
+ }
89
+
90
+ private async handleStream(stream: WebTransportBidirectionalStream): Promise<void> {
91
+ const reader = stream.readable.getReader()
92
+
93
+ try {
94
+ while (true) {
95
+ const { value, done } = await reader.read()
96
+ if (done) break
97
+
98
+ if (value) {
99
+ const text = this.decoder.decode(value)
100
+ try {
101
+ const msg = JSON.parse(text) as ServerMessage
102
+ this.notifyHandlers(msg)
103
+ } catch (e) {
104
+ console.error('PyWire: Error parsing WebTransport message', e)
105
+ }
106
+ }
107
+ }
108
+ } catch (e) {
109
+ console.error('PyWire: Stream read error', e)
110
+ }
111
+ }
112
+
113
+ async send(message: object): Promise<void> {
114
+ if (!this.transport || !this.connected) {
115
+ console.warn('PyWire: Cannot send message, WebTransport not connected')
116
+ return
117
+ }
118
+
119
+ try {
120
+ // Create a new bidirectional stream for each message
121
+ const stream = await this.transport.createBidirectionalStream()
122
+ const writer = stream.writable.getWriter()
123
+
124
+ const data = this.encoder.encode(JSON.stringify(message))
125
+ await writer.write(data)
126
+ await writer.close()
127
+
128
+ // Read the response from this stream
129
+ this.handleStream(stream)
130
+ } catch (e) {
131
+ console.error('PyWire: WebTransport send error', e)
132
+ }
133
+ }
134
+
135
+ disconnect(): void {
136
+ if (this.transport) {
137
+ this.transport.close()
138
+ this.transport = null
139
+ }
140
+ this.writer = null
141
+ this.connected = false
142
+ }
143
+
144
+ private handleDisconnect(): void {
145
+ this.connected = false
146
+ this.transport = null
147
+ this.writer = null
148
+ }
149
+ }
@@ -0,0 +1,93 @@
1
+ import { PyWireApp, ServerMessage } from '../core'
2
+ import { StatusOverlay } from './status-overlay'
3
+ import { ErrorTraceHandler } from './error-trace'
4
+
5
+ /**
6
+ * Development-mode PyWire Application.
7
+ * Extends core app with:
8
+ * - Connection status overlay
9
+ * - Error trace handling with source loading
10
+ * - Enhanced console output
11
+ */
12
+ export class PyWireDevApp extends PyWireApp {
13
+ private overlay: StatusOverlay | null = null
14
+ private errorHandler: ErrorTraceHandler
15
+
16
+ constructor(config = {}) {
17
+ super(config)
18
+ this.errorHandler = new ErrorTraceHandler()
19
+ }
20
+
21
+ async init(): Promise<void> {
22
+ // Create overlay before init so status changes are captured
23
+ this.overlay = new StatusOverlay()
24
+
25
+ await super.init()
26
+ }
27
+
28
+ /**
29
+ * Handle connection status changes with UI overlay.
30
+ */
31
+ protected handleStatusChange(connected: boolean): void {
32
+ super.handleStatusChange(connected)
33
+ if (this.overlay) {
34
+ this.overlay.update(connected)
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Navigate with offline feedback.
40
+ */
41
+ navigateTo(path: string): void {
42
+ if (!this.isConnected) {
43
+ console.warn('PyWire: Navigation blocked - Offline')
44
+ if (this.overlay) {
45
+ this.overlay.showNavigationBlocked()
46
+ }
47
+ return
48
+ }
49
+
50
+ super.navigateTo(path)
51
+ }
52
+
53
+ /**
54
+ * Handle incoming server messages with enhanced error trace support.
55
+ */
56
+ protected async handleMessage(msg: ServerMessage): Promise<void> {
57
+ switch (msg.type) {
58
+ case 'error_trace':
59
+ if (msg.trace) {
60
+ await this.errorHandler.handle(msg.error || 'Unknown Error', msg.trace)
61
+ }
62
+ return
63
+
64
+ case 'console':
65
+ // Enhanced console output with grouping
66
+ if (msg.lines && msg.lines.length > 0) {
67
+ const prefix = 'PyWire Server:'
68
+ const joined = msg.lines.join('\n')
69
+ if (msg.level === 'error') {
70
+ console.group(prefix + ' Error')
71
+ console.error(joined)
72
+ console.groupEnd()
73
+ } else if (msg.level === 'warn') {
74
+ console.groupCollapsed(prefix + ' Warning')
75
+ console.warn(joined)
76
+ console.groupEnd()
77
+ } else {
78
+ if (msg.lines.length === 1) {
79
+ console.log(prefix, joined)
80
+ } else {
81
+ console.groupCollapsed(prefix + ' Log')
82
+ console.log(joined)
83
+ console.groupEnd()
84
+ }
85
+ }
86
+ }
87
+ return
88
+
89
+ default:
90
+ await super.handleMessage(msg)
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,97 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { ErrorTraceHandler } from './error-trace'
3
+
4
+ describe('ErrorTraceHandler', () => {
5
+ let handler: ErrorTraceHandler
6
+
7
+ beforeEach(() => {
8
+ vi.clearAllMocks()
9
+ handler = new ErrorTraceHandler()
10
+ vi.stubGlobal('location', { origin: 'http://localhost:8000' })
11
+ vi.stubGlobal('fetch', vi.fn())
12
+ vi.stubGlobal('console', { error: vi.fn(), warn: vi.fn(), log: vi.fn() })
13
+ // vi.stubGlobal('eval', vi.fn()); // indirect eval is hard to mock, but we can check if it's called
14
+ })
15
+
16
+ it('should generate virtual URLs correctly', () => {
17
+ // getVirtualUrl is private, so we'll test it through handle()'s side effects or by casting
18
+ const virtualUrl = (
19
+ handler as unknown as { getVirtualUrl: (filename: string) => string }
20
+ ).getVirtualUrl('/path/to/script.py')
21
+ expect(virtualUrl).toContain('http://localhost:8000/_pywire/file/')
22
+ expect(virtualUrl).toContain('script.py')
23
+ })
24
+
25
+ it('should fetch source files and inject them with sourceURL', async () => {
26
+ const fetchMock = vi.mocked(fetch)
27
+ fetchMock.mockResolvedValue({
28
+ ok: true,
29
+ text: async () => 'print("hello")',
30
+ } as Response)
31
+
32
+ const trace = [
33
+ {
34
+ filename: '/path/to/script.py',
35
+ lineno: 10,
36
+ colno: 5,
37
+ name: 'my_func',
38
+ line: 'def my_func():',
39
+ },
40
+ ]
41
+
42
+ await handler.handle('Test Error', trace)
43
+
44
+ expect(fetchMock).toHaveBeenCalledWith('/_pywire/source?path=%2Fpath%2Fto%2Fscript.py')
45
+ // We expect console.error to have been called with the constructed stack
46
+ expect(vi.mocked(console.error)).toHaveBeenCalledWith(
47
+ expect.stringContaining('at my_func (http://localhost:8000/_pywire/file/')
48
+ )
49
+ expect(vi.mocked(console.error)).toHaveBeenCalledWith(expect.stringContaining(':10:5)'))
50
+ })
51
+
52
+ it('should handle missing column numbers', async () => {
53
+ vi.mocked(fetch).mockResolvedValue({ ok: false } as Response)
54
+
55
+ const trace = [{ filename: 'test.py', lineno: 5, name: 'foo', line: 'x = 1' }]
56
+
57
+ await handler.handle('Error', trace)
58
+
59
+ expect(vi.mocked(console.error)).toHaveBeenCalledWith(
60
+ expect.stringContaining('at foo (http://localhost:8000/_pywire/file/')
61
+ )
62
+ expect(vi.mocked(console.error)).toHaveBeenCalledWith(
63
+ expect.stringContaining(':5:1)') // Default colno is 1
64
+ )
65
+ })
66
+
67
+ it('should not reload already loaded sources', async () => {
68
+ const fetchMock = vi.mocked(fetch)
69
+ fetchMock.mockResolvedValue({
70
+ ok: true,
71
+ text: async () => 'content',
72
+ } as Response)
73
+
74
+ const trace = [{ filename: 'test.py', lineno: 1, name: 'foo', line: 'content' }]
75
+
76
+ await handler.handle('Err 1', trace)
77
+ await handler.handle('Err 2', trace)
78
+
79
+ expect(fetchMock).toHaveBeenCalledTimes(1)
80
+ })
81
+
82
+ it('should handle fetch failures gracefully', async () => {
83
+ vi.mocked(fetch).mockRejectedValue(new Error('Network error'))
84
+
85
+ const trace = [{ filename: 'test.py', lineno: 1, name: 'foo', line: 'content' }]
86
+
87
+ await handler.handle('Err', trace)
88
+
89
+ expect(vi.mocked(console.warn)).toHaveBeenCalledWith(
90
+ expect.stringContaining('PyWire: Failed to load source'),
91
+ 'test.py',
92
+ expect.any(Error)
93
+ )
94
+ // Should still log the error stack
95
+ expect(vi.mocked(console.error)).toHaveBeenCalled()
96
+ })
97
+ })
@@ -0,0 +1,76 @@
1
+ import { StackFrame } from '../core/transports'
2
+
3
+ /**
4
+ * Handles error traces from the server in development mode.
5
+ * Loads source files and displays errors with proper source mapping in DevTools.
6
+ */
7
+ export class ErrorTraceHandler {
8
+ private loadedSources = new Set<string>()
9
+
10
+ /**
11
+ * Get a virtual URL for a filename that Chrome will display in stack traces.
12
+ */
13
+ private getVirtualUrl(filename: string): string {
14
+ const encoded = btoa(filename).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
15
+ const cleanName = filename.split(/[/\\]/).pop() || 'unknown'
16
+ return `${window.location.origin}/_pywire/file/${encoded}/${cleanName}`
17
+ }
18
+
19
+ /**
20
+ * Handle an error trace message from the server.
21
+ */
22
+ async handle(errorMessage: string, trace: StackFrame[]): Promise<void> {
23
+ // Load sources for frames
24
+ const filesToLoad = new Set<string>()
25
+ for (const frame of trace) {
26
+ if (!this.loadedSources.has(frame.filename)) {
27
+ filesToLoad.add(frame.filename)
28
+ }
29
+ }
30
+
31
+ await Promise.all(
32
+ Array.from(filesToLoad).map(async (filename) => {
33
+ try {
34
+ const virtualUrl = this.getVirtualUrl(filename)
35
+
36
+ // Fetch content
37
+ const url = `/_pywire/source?path=${encodeURIComponent(filename)}`
38
+ const resp = await fetch(url)
39
+ if (resp.ok) {
40
+ const content = await resp.text()
41
+
42
+ // Inject the raw source with sourceURL for DevTools
43
+ const sourceCode = `${content}\n//# sourceURL=${virtualUrl}`
44
+ try {
45
+ ;(0, eval)(sourceCode)
46
+ } catch {
47
+ // Syntax errors from Python content are expected - ignore
48
+ }
49
+
50
+ this.loadedSources.add(filename)
51
+ }
52
+
53
+ this.loadedSources.add(filename)
54
+ } catch (e) {
55
+ console.warn('PyWire: Failed to load source', filename, e)
56
+ }
57
+ })
58
+ )
59
+
60
+ // Construct Error with stack pointing to virtual URLs
61
+ const err = new Error(errorMessage)
62
+ const stackLines = [`${err.name}: ${err.message}`]
63
+
64
+ for (const frame of trace) {
65
+ const fn = frame.name || '<module>'
66
+ const virtualUrl = this.getVirtualUrl(frame.filename)
67
+ const col = frame.colno ?? 1
68
+ stackLines.push(` at ${fn} (${virtualUrl}:${frame.lineno}:${col})`)
69
+ }
70
+
71
+ err.stack = stackLines.join('\n')
72
+
73
+ // Log just the stack string to avoid Chrome appending its own call stack
74
+ console.error(err.stack)
75
+ }
76
+ }
@@ -0,0 +1,4 @@
1
+ // Dev module exports
2
+ export { PyWireDevApp } from './dev-app'
3
+ export { StatusOverlay } from './status-overlay'
4
+ export { ErrorTraceHandler } from './error-trace'
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Connection status overlay for development mode.
3
+ * Shows a toast-style notification when connection is lost.
4
+ */
5
+ export class StatusOverlay {
6
+ private element: HTMLElement | null = null
7
+
8
+ constructor() {
9
+ this.create()
10
+ }
11
+
12
+ private create(): void {
13
+ this.element = document.createElement('div')
14
+ this.element.style.cssText = `
15
+ position: fixed;
16
+ bottom: 20px;
17
+ right: 20px;
18
+ background: rgba(0, 0, 0, 0.8);
19
+ color: white;
20
+ padding: 10px 20px;
21
+ border-radius: 5px;
22
+ font-family: system-ui, -apple-system, sans-serif;
23
+ font-size: 14px;
24
+ z-index: 10000;
25
+ display: none;
26
+ transition: opacity 0.3s;
27
+ pointer-events: none;
28
+ `
29
+ document.body.appendChild(this.element)
30
+ }
31
+
32
+ /**
33
+ * Update overlay based on connection status.
34
+ */
35
+ update(connected: boolean): void {
36
+ if (!this.element) return
37
+
38
+ if (connected) {
39
+ this.element.style.display = 'none'
40
+ } else {
41
+ this.element.textContent = 'Connection Lost - Reconnecting...'
42
+ this.element.style.display = 'block'
43
+ this.element.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Show a temporary navigation-blocked message.
49
+ */
50
+ showNavigationBlocked(): void {
51
+ if (!this.element) return
52
+
53
+ this.element.style.backgroundColor = 'rgba(200, 0, 0, 0.9)'
54
+ this.element.textContent = 'Cannot navigate - Offline'
55
+ this.element.style.display = 'block'
56
+
57
+ setTimeout(() => {
58
+ if (this.element) {
59
+ this.element.style.backgroundColor = 'rgba(0, 0, 0, 0.8)'
60
+ }
61
+ }, 1500)
62
+ }
63
+ }