pythonnative 0.40.0__cp313-cp313-win_amd64.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 (418) hide show
  1. pythonnative/__init__.py +411 -0
  2. pythonnative/_ios_log.py +92 -0
  3. pythonnative/_yoga.cp313-win_amd64.pyd +0 -0
  4. pythonnative/alerts.py +236 -0
  5. pythonnative/animated.py +1687 -0
  6. pythonnative/animation_graph.py +78 -0
  7. pythonnative/appearance.py +124 -0
  8. pythonnative/bootstrap.py +100 -0
  9. pythonnative/bridge/__init__.py +302 -0
  10. pythonnative/bridge/android.py +78 -0
  11. pythonnative/bridge/codec.py +170 -0
  12. pythonnative/bridge/commits.py +114 -0
  13. pythonnative/bridge/fake.py +243 -0
  14. pythonnative/bridge/ios.py +138 -0
  15. pythonnative/bridge/web.py +489 -0
  16. pythonnative/cli/__init__.py +7 -0
  17. pythonnative/cli/pn.py +1214 -0
  18. pythonnative/component.py +271 -0
  19. pythonnative/components/__init__.py +111 -0
  20. pythonnative/components/_base.py +166 -0
  21. pythonnative/components/controls.py +521 -0
  22. pythonnative/components/layout.py +518 -0
  23. pythonnative/components/lists.py +785 -0
  24. pythonnative/components/media.py +204 -0
  25. pythonnative/components/overlays.py +107 -0
  26. pythonnative/components/pressable.py +252 -0
  27. pythonnative/components/structural.py +167 -0
  28. pythonnative/components/text.py +291 -0
  29. pythonnative/devclient.py +824 -0
  30. pythonnative/devserver/__init__.py +34 -0
  31. pythonnative/devserver/server.py +718 -0
  32. pythonnative/devserver/static/animation_graph.js +82 -0
  33. pythonnative/devserver/static/bridge.js +147 -0
  34. pythonnative/devserver/static/colors.js +70 -0
  35. pythonnative/devserver/static/host.js +552 -0
  36. pythonnative/devserver/static/index.html +60 -0
  37. pythonnative/devserver/static/layout.js +85 -0
  38. pythonnative/devserver/static/preview.css +415 -0
  39. pythonnative/devserver/static/renderer.js +1994 -0
  40. pythonnative/devserver/static/shell.js +345 -0
  41. pythonnative/devserver/static/yoga/LICENSE +21 -0
  42. pythonnative/devserver/static/yoga/binaries/yoga-wasm-base64-esm.js +75 -0
  43. pythonnative/devserver/static/yoga/src/generated/YGEnums.d.ts +189 -0
  44. pythonnative/devserver/static/yoga/src/generated/YGEnums.js +211 -0
  45. pythonnative/devserver/static/yoga/src/generated/YGEnums.js.map +1 -0
  46. pythonnative/devserver/static/yoga/src/index.d.ts +12 -0
  47. pythonnative/devserver/static/yoga/src/index.js +16 -0
  48. pythonnative/devserver/static/yoga/src/index.js.map +1 -0
  49. pythonnative/devserver/static/yoga/src/load.d.ts +11 -0
  50. pythonnative/devserver/static/yoga/src/load.js +17 -0
  51. pythonnative/devserver/static/yoga/src/load.js.map +1 -0
  52. pythonnative/devserver/static/yoga/src/wrapAssembly.d.ts +155 -0
  53. pythonnative/devserver/static/yoga/src/wrapAssembly.js +125 -0
  54. pythonnative/devserver/static/yoga/src/wrapAssembly.js.map +1 -0
  55. pythonnative/devserver/watcher.py +267 -0
  56. pythonnative/devserver/ws.py +421 -0
  57. pythonnative/diagnostics.py +241 -0
  58. pythonnative/element.py +159 -0
  59. pythonnative/equality.py +19 -0
  60. pythonnative/events.py +231 -0
  61. pythonnative/gestures.py +1333 -0
  62. pythonnative/hooks.py +1621 -0
  63. pythonnative/hosts/__init__.py +63 -0
  64. pythonnative/hosts/base.py +596 -0
  65. pythonnative/hosts/native.py +302 -0
  66. pythonnative/hot_reload.py +585 -0
  67. pythonnative/layout.py +328 -0
  68. pythonnative/mutations.py +142 -0
  69. pythonnative/native/android/build.gradle +52 -0
  70. pythonnative/native/android/gradle.properties +3 -0
  71. pythonnative/native/android/settings.gradle +12 -0
  72. pythonnative/native/android/src/main/AndroidManifest.xml +11 -0
  73. pythonnative/native/android/src/main/cpp/CMakeLists.txt +9 -0
  74. pythonnative/native/android/src/main/cpp/yoga_jni.cpp +52 -0
  75. pythonnative/native/android/src/main/java/com/pythonnative/generated/NativeModules.kt +5 -0
  76. pythonnative/native/android/src/main/java/com/pythonnative/generated/NativeProps.kt +3705 -0
  77. pythonnative/native/android/src/main/java/com/pythonnative/generated/PNContracts.kt +129 -0
  78. pythonnative/native/android/src/main/java/com/pythonnative/runtime/PNBridge.kt +261 -0
  79. pythonnative/native/android/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
  80. pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/AnimationGraph.kt +108 -0
  81. pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
  82. pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +257 -0
  83. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/CommitState.kt +106 -0
  84. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
  85. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +40 -0
  86. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
  87. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
  88. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
  89. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +117 -0
  90. pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
  91. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +37 -0
  92. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +32 -0
  93. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
  94. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
  95. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
  96. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
  97. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
  98. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +162 -0
  99. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
  100. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
  101. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +86 -0
  102. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
  103. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
  104. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ScreenStackManager.kt +110 -0
  105. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
  106. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
  107. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
  108. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
  109. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +250 -0
  110. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
  111. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
  112. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +159 -0
  113. pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
  114. pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
  115. pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
  116. pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
  117. pythonnative/native/android/src/main/java/com/pythonnative/runtime/layout/NativeLayout.kt +145 -0
  118. pythonnative/native/android/src/main/java/com/pythonnative/runtime/layout/YogaNode.kt +29 -0
  119. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
  120. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
  121. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +61 -0
  122. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
  123. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +158 -0
  124. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
  125. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +99 -0
  126. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
  127. pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
  128. pythonnative/native/android/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
  129. pythonnative/native/android/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +188 -0
  130. pythonnative/native/android/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
  131. pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNAccessibilityDelegate.kt +48 -0
  132. pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNBorderDrawable.kt +76 -0
  133. pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
  134. pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNFrameLayout.kt +49 -0
  135. pythonnative/native/android/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
  136. pythonnative/native/android/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
  137. pythonnative/native/android/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
  138. pythonnative/native/android/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
  139. pythonnative/native/android/src/test/java/com/pythonnative/runtime/PromiseTest.kt +89 -0
  140. pythonnative/native/ios/Package.swift +23 -0
  141. pythonnative/native/ios/Sources/PythonNativeKit/Animation/PNAnimationGraph.swift +199 -0
  142. pythonnative/native/ios/Sources/PythonNativeKit/Animation/PNAnimator.swift +312 -0
  143. pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNBridge.swift +255 -0
  144. pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNCommit.swift +104 -0
  145. pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNRegistry.swift +142 -0
  146. pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNTransaction.swift +166 -0
  147. pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
  148. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNButtonManagers.swift +207 -0
  149. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
  150. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
  151. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
  152. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
  153. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNImageManager.swift +222 -0
  154. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
  155. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
  156. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNScreenStackManager.swift +107 -0
  157. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
  158. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
  159. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTextInputManager.swift +297 -0
  160. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
  161. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
  162. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
  163. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
  164. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
  165. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +155 -0
  166. pythonnative/native/ios/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
  167. pythonnative/native/ios/Sources/PythonNativeKit/Generated/NativeModules.swift +1 -0
  168. pythonnative/native/ios/Sources/PythonNativeKit/Generated/NativeProps.swift +3775 -0
  169. pythonnative/native/ios/Sources/PythonNativeKit/Generated/PNContracts.swift +46 -0
  170. pythonnative/native/ios/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
  171. pythonnative/native/ios/Sources/PythonNativeKit/Layout/PNLayout.swift +173 -0
  172. pythonnative/native/ios/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
  173. pythonnative/native/ios/Sources/PythonNativeKit/Modules/HostModule.swift +80 -0
  174. pythonnative/native/ios/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
  175. pythonnative/native/ios/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
  176. pythonnative/native/ios/Sources/PythonNativeKit/Modules/PNNativeModule.swift +168 -0
  177. pythonnative/native/ios/Sources/PythonNativeKit/Modules/PermissionModules.swift +266 -0
  178. pythonnative/native/ios/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
  179. pythonnative/native/ios/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
  180. pythonnative/native/ios/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
  181. pythonnative/native/ios/Sources/PythonNativeKit/Screens/PNViewController.swift +237 -0
  182. pythonnative/native/ios/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
  183. pythonnative/native/ios/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
  184. pythonnative/native/ios/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
  185. pythonnative/native/ios/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
  186. pythonnative/native/ios/Tests/PythonNativeKitTests/PNAnimationGraphTests.swift +26 -0
  187. pythonnative/native/ios/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
  188. pythonnative/native/ios/Tests/PythonNativeKitTests/PNCommitTests.swift +86 -0
  189. pythonnative/native/ios/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
  190. pythonnative/native/ios/Tests/PythonNativeKitTests/PNManagerTests.swift +126 -0
  191. pythonnative/native/ios/Tests/PythonNativeKitTests/PNModuleTests.swift +111 -0
  192. pythonnative/native/ios/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
  193. pythonnative/native/yoga/LICENSE +21 -0
  194. pythonnative/native/yoga/Package.swift +9 -0
  195. pythonnative/native/yoga/VENDORED.md +2 -0
  196. pythonnative/native/yoga/include/PNStyle.h +10 -0
  197. pythonnative/native/yoga/include/module.modulemap +5 -0
  198. pythonnative/native/yoga/include/yoga/YGConfig.h +158 -0
  199. pythonnative/native/yoga/include/yoga/YGEnums.h +142 -0
  200. pythonnative/native/yoga/include/yoga/YGMacros.h +99 -0
  201. pythonnative/native/yoga/include/yoga/YGNode.h +304 -0
  202. pythonnative/native/yoga/include/yoga/YGNodeLayout.h +35 -0
  203. pythonnative/native/yoga/include/yoga/YGNodeStyle.h +130 -0
  204. pythonnative/native/yoga/include/yoga/YGPixelGrid.h +29 -0
  205. pythonnative/native/yoga/include/yoga/YGValue.h +84 -0
  206. pythonnative/native/yoga/include/yoga/Yoga.h +21 -0
  207. pythonnative/native/yoga/python.cpp +16 -0
  208. pythonnative/native/yoga/style.cpp +272 -0
  209. pythonnative/native/yoga/yoga/CMakeLists.txt +41 -0
  210. pythonnative/native/yoga/yoga/YGConfig.cpp +92 -0
  211. pythonnative/native/yoga/yoga/YGConfig.h +158 -0
  212. pythonnative/native/yoga/yoga/YGEnums.cpp +262 -0
  213. pythonnative/native/yoga/yoga/YGEnums.h +142 -0
  214. pythonnative/native/yoga/yoga/YGMacros.h +99 -0
  215. pythonnative/native/yoga/yoga/YGNode.cpp +366 -0
  216. pythonnative/native/yoga/yoga/YGNode.h +304 -0
  217. pythonnative/native/yoga/yoga/YGNodeLayout.cpp +92 -0
  218. pythonnative/native/yoga/yoga/YGNodeLayout.h +35 -0
  219. pythonnative/native/yoga/yoga/YGNodeStyle.cpp +405 -0
  220. pythonnative/native/yoga/yoga/YGNodeStyle.h +130 -0
  221. pythonnative/native/yoga/yoga/YGPixelGrid.cpp +22 -0
  222. pythonnative/native/yoga/yoga/YGPixelGrid.h +29 -0
  223. pythonnative/native/yoga/yoga/YGValue.cpp +20 -0
  224. pythonnative/native/yoga/yoga/YGValue.h +84 -0
  225. pythonnative/native/yoga/yoga/Yoga.h +21 -0
  226. pythonnative/native/yoga/yoga/algorithm/AbsoluteLayout.cpp +563 -0
  227. pythonnative/native/yoga/yoga/algorithm/AbsoluteLayout.h +41 -0
  228. pythonnative/native/yoga/yoga/algorithm/Align.h +72 -0
  229. pythonnative/native/yoga/yoga/algorithm/Baseline.cpp +78 -0
  230. pythonnative/native/yoga/yoga/algorithm/Baseline.h +21 -0
  231. pythonnative/native/yoga/yoga/algorithm/BoundAxis.h +79 -0
  232. pythonnative/native/yoga/yoga/algorithm/Cache.cpp +121 -0
  233. pythonnative/native/yoga/yoga/algorithm/Cache.h +30 -0
  234. pythonnative/native/yoga/yoga/algorithm/CalculateLayout.cpp +2428 -0
  235. pythonnative/native/yoga/yoga/algorithm/CalculateLayout.h +38 -0
  236. pythonnative/native/yoga/yoga/algorithm/FlexDirection.h +120 -0
  237. pythonnative/native/yoga/yoga/algorithm/FlexLine.cpp +124 -0
  238. pythonnative/native/yoga/yoga/algorithm/FlexLine.h +75 -0
  239. pythonnative/native/yoga/yoga/algorithm/PixelGrid.cpp +132 -0
  240. pythonnative/native/yoga/yoga/algorithm/PixelGrid.h +29 -0
  241. pythonnative/native/yoga/yoga/algorithm/SizingMode.h +73 -0
  242. pythonnative/native/yoga/yoga/algorithm/TrailingPosition.h +44 -0
  243. pythonnative/native/yoga/yoga/config/Config.cpp +135 -0
  244. pythonnative/native/yoga/yoga/config/Config.h +92 -0
  245. pythonnative/native/yoga/yoga/debug/AssertFatal.cpp +53 -0
  246. pythonnative/native/yoga/yoga/debug/AssertFatal.h +29 -0
  247. pythonnative/native/yoga/yoga/debug/Log.cpp +107 -0
  248. pythonnative/native/yoga/yoga/debug/Log.h +34 -0
  249. pythonnative/native/yoga/yoga/enums/Align.h +47 -0
  250. pythonnative/native/yoga/yoga/enums/BoxSizing.h +40 -0
  251. pythonnative/native/yoga/yoga/enums/Dimension.h +40 -0
  252. pythonnative/native/yoga/yoga/enums/Direction.h +41 -0
  253. pythonnative/native/yoga/yoga/enums/Display.h +41 -0
  254. pythonnative/native/yoga/yoga/enums/Edge.h +47 -0
  255. pythonnative/native/yoga/yoga/enums/Errata.h +41 -0
  256. pythonnative/native/yoga/yoga/enums/ExperimentalFeature.h +39 -0
  257. pythonnative/native/yoga/yoga/enums/FlexDirection.h +42 -0
  258. pythonnative/native/yoga/yoga/enums/Gutter.h +41 -0
  259. pythonnative/native/yoga/yoga/enums/Justify.h +44 -0
  260. pythonnative/native/yoga/yoga/enums/LogLevel.h +44 -0
  261. pythonnative/native/yoga/yoga/enums/MeasureMode.h +41 -0
  262. pythonnative/native/yoga/yoga/enums/NodeType.h +40 -0
  263. pythonnative/native/yoga/yoga/enums/Overflow.h +41 -0
  264. pythonnative/native/yoga/yoga/enums/PhysicalEdge.h +21 -0
  265. pythonnative/native/yoga/yoga/enums/PositionType.h +41 -0
  266. pythonnative/native/yoga/yoga/enums/Unit.h +42 -0
  267. pythonnative/native/yoga/yoga/enums/Wrap.h +41 -0
  268. pythonnative/native/yoga/yoga/enums/YogaEnums.h +85 -0
  269. pythonnative/native/yoga/yoga/event/event.cpp +87 -0
  270. pythonnative/native/yoga/yoga/event/event.h +130 -0
  271. pythonnative/native/yoga/yoga/module.modulemap +21 -0
  272. pythonnative/native/yoga/yoga/node/CachedMeasurement.h +53 -0
  273. pythonnative/native/yoga/yoga/node/LayoutResults.cpp +48 -0
  274. pythonnative/native/yoga/yoga/node/LayoutResults.h +122 -0
  275. pythonnative/native/yoga/yoga/node/LayoutableChildren.h +148 -0
  276. pythonnative/native/yoga/yoga/node/Node.cpp +443 -0
  277. pythonnative/native/yoga/yoga/node/Node.h +337 -0
  278. pythonnative/native/yoga/yoga/numeric/Comparison.h +81 -0
  279. pythonnative/native/yoga/yoga/numeric/FloatOptional.h +93 -0
  280. pythonnative/native/yoga/yoga/style/SmallValueBuffer.h +133 -0
  281. pythonnative/native/yoga/yoga/style/Style.h +757 -0
  282. pythonnative/native/yoga/yoga/style/StyleLength.h +107 -0
  283. pythonnative/native/yoga/yoga/style/StyleValueHandle.h +98 -0
  284. pythonnative/native/yoga/yoga/style/StyleValuePool.h +127 -0
  285. pythonnative/native_modules/__init__.py +124 -0
  286. pythonnative/native_modules/app_state.py +99 -0
  287. pythonnative/native_modules/battery.py +75 -0
  288. pythonnative/native_modules/biometrics.py +42 -0
  289. pythonnative/native_modules/camera.py +65 -0
  290. pythonnative/native_modules/clipboard.py +46 -0
  291. pythonnative/native_modules/fallback.py +324 -0
  292. pythonnative/native_modules/file_system.py +145 -0
  293. pythonnative/native_modules/haptics.py +75 -0
  294. pythonnative/native_modules/linking.py +123 -0
  295. pythonnative/native_modules/location.py +75 -0
  296. pythonnative/native_modules/net_info.py +112 -0
  297. pythonnative/native_modules/notifications.py +129 -0
  298. pythonnative/native_modules/permissions.py +108 -0
  299. pythonnative/native_modules/registry.py +526 -0
  300. pythonnative/native_modules/secure_store.py +52 -0
  301. pythonnative/native_modules/share.py +51 -0
  302. pythonnative/native_views/__init__.py +375 -0
  303. pythonnative/native_views/base.py +316 -0
  304. pythonnative/native_views/bridge_backend.py +277 -0
  305. pythonnative/navigation/__init__.py +103 -0
  306. pythonnative/navigation/container.py +154 -0
  307. pythonnative/navigation/handle.py +540 -0
  308. pythonnative/navigation/hooks.py +133 -0
  309. pythonnative/navigation/host.py +58 -0
  310. pythonnative/navigation/linking.py +200 -0
  311. pythonnative/navigation/navigators.py +659 -0
  312. pythonnative/navigation/screen.py +149 -0
  313. pythonnative/navigation/state.py +269 -0
  314. pythonnative/net.py +248 -0
  315. pythonnative/platform.py +166 -0
  316. pythonnative/platform_metrics.py +252 -0
  317. pythonnative/preview.py +287 -0
  318. pythonnative/profiling.py +101 -0
  319. pythonnative/project/__init__.py +68 -0
  320. pythonnative/project/android.py +557 -0
  321. pythonnative/project/builder.py +817 -0
  322. pythonnative/project/config.py +773 -0
  323. pythonnative/project/deps.py +719 -0
  324. pythonnative/project/devices.py +304 -0
  325. pythonnative/project/doctor.py +283 -0
  326. pythonnative/project/fingerprint.py +171 -0
  327. pythonnative/project/icons.py +247 -0
  328. pythonnative/project/ios.py +325 -0
  329. pythonnative/project/lockfile.py +100 -0
  330. pythonnative/project/permissions.py +361 -0
  331. pythonnative/project/plugins.py +519 -0
  332. pythonnative/project/runtime_assets.py +183 -0
  333. pythonnative/py.typed +0 -0
  334. pythonnative/query.py +153 -0
  335. pythonnative/reconciler/__init__.py +29 -0
  336. pythonnative/reconciler/boundaries.py +375 -0
  337. pythonnative/reconciler/children.py +88 -0
  338. pythonnative/reconciler/core.py +1165 -0
  339. pythonnative/reconciler/layout_pass.py +393 -0
  340. pythonnative/reconciler/vnode.py +266 -0
  341. pythonnative/refresh.py +55 -0
  342. pythonnative/runtime.py +306 -0
  343. pythonnative/scheduler.py +159 -0
  344. pythonnative/sdk/__init__.py +153 -0
  345. pythonnative/sdk/_components.py +427 -0
  346. pythonnative/sdk/builtins.py +91 -0
  347. pythonnative/sdk/codegen.py +149 -0
  348. pythonnative/sdk/module_codegen.py +184 -0
  349. pythonnative/sdk/schema.py +224 -0
  350. pythonnative/sdk/templates/contracts.kt +46 -0
  351. pythonnative/sdk/templates/contracts.swift +46 -0
  352. pythonnative/storage.py +184 -0
  353. pythonnative/style.py +841 -0
  354. pythonnative/suspense.py +183 -0
  355. pythonnative/templates/android_template/app/build.gradle +75 -0
  356. pythonnative/templates/android_template/app/proguard-rules.pro +21 -0
  357. pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt +24 -0
  358. pythonnative/templates/android_template/app/src/debug/AndroidManifest.xml +7 -0
  359. pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +32 -0
  360. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +130 -0
  361. pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -0
  362. pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +170 -0
  363. pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +30 -0
  364. pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +10 -0
  365. pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
  366. pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
  367. pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
  368. pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
  369. pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
  370. pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
  371. pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
  372. pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
  373. pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
  374. pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
  375. pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
  376. pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
  377. pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +22 -0
  378. pythonnative/templates/android_template/app/src/main/res/values/colors.xml +5 -0
  379. pythonnative/templates/android_template/app/src/main/res/values/strings.xml +5 -0
  380. pythonnative/templates/android_template/app/src/main/res/values/themes.xml +9 -0
  381. pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +7 -0
  382. pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +13 -0
  383. pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +19 -0
  384. pythonnative/templates/android_template/app/src/test/java/com/pythonnative/android_template/ExampleUnitTest.kt +17 -0
  385. pythonnative/templates/android_template/build.gradle +10 -0
  386. pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
  387. pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +6 -0
  388. pythonnative/templates/android_template/gradle.properties +23 -0
  389. pythonnative/templates/android_template/gradlew +185 -0
  390. pythonnative/templates/android_template/gradlew.bat +89 -0
  391. pythonnative/templates/android_template/settings.gradle +17 -0
  392. pythonnative/templates/ios_template/ios_template/AppDelegate.swift +51 -0
  393. pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +11 -0
  394. pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +13 -0
  395. pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +6 -0
  396. pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +25 -0
  397. pythonnative/templates/ios_template/ios_template/BridgingHeader.h +14 -0
  398. pythonnative/templates/ios_template/ios_template/Info.plist +28 -0
  399. pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +390 -0
  400. pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +61 -0
  401. pythonnative/templates/ios_template/ios_template/ViewController.swift +23 -0
  402. pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +745 -0
  403. pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  404. pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +36 -0
  405. pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +41 -0
  406. pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +32 -0
  407. pythonnative/testing/__init__.py +53 -0
  408. pythonnative/testing/backend.py +276 -0
  409. pythonnative/testing/harness.py +369 -0
  410. pythonnative/utils.py +145 -0
  411. pythonnative-0.40.0.dist-info/DELVEWHEEL +2 -0
  412. pythonnative-0.40.0.dist-info/METADATA +150 -0
  413. pythonnative-0.40.0.dist-info/RECORD +418 -0
  414. pythonnative-0.40.0.dist-info/WHEEL +5 -0
  415. pythonnative-0.40.0.dist-info/entry_points.txt +2 -0
  416. pythonnative-0.40.0.dist-info/licenses/LICENSE +21 -0
  417. pythonnative-0.40.0.dist-info/top_level.txt +1 -0
  418. pythonnative.libs/msvcp140-a4c2229bdc2a2a630acdc095b4d86008.dll +0 -0
@@ -0,0 +1,267 @@
1
+ """Source snapshots and change detection for the dev server.
2
+
3
+ The dev server treats the project's ``app/`` directory as a content-
4
+ addressed tree: every dev client gets the same manifest (relative path
5
+ to SHA-256), so a client can tell exactly which files it is missing
6
+ after a reconnect, and a change notification can carry the new bytes.
7
+
8
+ [`FileWatcher`][pythonnative.devserver.watcher.FileWatcher] polls the
9
+ tree with ``os.stat`` rather than a native file-system event API so it
10
+ behaves identically on every host OS without a dependency; the poll
11
+ interval is short enough that saves feel instant.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import hashlib
17
+ import os
18
+ import threading
19
+ import time
20
+ from dataclasses import dataclass, field
21
+ from typing import Callable, Dict, List, Optional, Sequence, Set, Tuple
22
+
23
+ __all__ = ["FileWatcher", "SourceSnapshot", "SourceChange", "is_synced_file", "snapshot_sources"]
24
+
25
+ MAX_SYNC_FILE_BYTES = 8 * 1024 * 1024
26
+ """Files larger than this are left out of the sync set (they belong in a real build)."""
27
+
28
+ _IGNORED_DIRS = {"__pycache__", ".git", ".hg", ".svn", ".mypy_cache", ".pytest_cache", ".ruff_cache", "node_modules"}
29
+ _IGNORED_SUFFIXES = (".pyc", ".pyo", ".swp", ".swo", ".tmp", "~")
30
+ _IGNORED_NAMES = {".DS_Store", "Thumbs.db"}
31
+
32
+
33
+ def is_synced_file(rel_path: str) -> bool:
34
+ """Whether a file under ``app/`` takes part in dev sync.
35
+
36
+ Editor swap files, byte-code caches, and VCS metadata are skipped;
37
+ everything else (Python modules and data files an app reads at
38
+ runtime) is synced so the on-device overlay mirrors the source tree.
39
+ """
40
+ parts = rel_path.replace("\\", "/").split("/")
41
+ if any(part in _IGNORED_DIRS for part in parts[:-1]):
42
+ return False
43
+ name = parts[-1]
44
+ if not name or name in _IGNORED_NAMES:
45
+ return False
46
+ if name.startswith(".#"):
47
+ return False
48
+ return not name.endswith(_IGNORED_SUFFIXES)
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class SourceSnapshot:
53
+ """The state of a source tree at one instant.
54
+
55
+ Attributes:
56
+ root: Absolute directory the relative paths are resolved against
57
+ (the project root; paths start with ``app/``).
58
+ files: ``relative_path -> sha256`` for every synced file.
59
+ mtimes: ``relative_path -> (mtime_ns, size)`` used to skip
60
+ re-hashing unchanged files between polls.
61
+ version: A digest of the whole tree, stable across processes
62
+ for identical contents.
63
+ """
64
+
65
+ root: str
66
+ files: Dict[str, str] = field(default_factory=dict)
67
+ mtimes: Dict[str, Tuple[int, int]] = field(default_factory=dict)
68
+ version: str = ""
69
+
70
+ def read(self, rel_path: str) -> Optional[bytes]:
71
+ """Return the current bytes of ``rel_path`` (``None`` if it is gone)."""
72
+ try:
73
+ with open(os.path.join(self.root, rel_path), "rb") as handle:
74
+ return handle.read()
75
+ except OSError:
76
+ return None
77
+
78
+ def diff(self, other: "SourceSnapshot") -> "SourceChange":
79
+ """Describe how to get from ``self`` to ``other``."""
80
+ changed = sorted(path for path, digest in other.files.items() if self.files.get(path) != digest)
81
+ removed = sorted(path for path in self.files if path not in other.files)
82
+ return SourceChange(changed=changed, removed=removed, version=other.version)
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class SourceChange:
87
+ """Paths that changed (added or modified) and paths that disappeared."""
88
+
89
+ changed: List[str]
90
+ removed: List[str]
91
+ version: str
92
+
93
+ def __bool__(self) -> bool:
94
+ return bool(self.changed or self.removed)
95
+
96
+
97
+ def _tree_version(files: Dict[str, str]) -> str:
98
+ digest = hashlib.sha256()
99
+ for path in sorted(files):
100
+ digest.update(path.encode("utf-8"))
101
+ digest.update(b"\0")
102
+ digest.update(files[path].encode("ascii"))
103
+ digest.update(b"\n")
104
+ return digest.hexdigest()[:16]
105
+
106
+
107
+ def snapshot_sources(
108
+ root: str,
109
+ subdirs: Sequence[str] = ("app",),
110
+ *,
111
+ previous: Optional[SourceSnapshot] = None,
112
+ ) -> SourceSnapshot:
113
+ """Hash every synced file under ``root/<subdir>`` for each of ``subdirs``.
114
+
115
+ Args:
116
+ root: Project root.
117
+ subdirs: Directories (relative to ``root``) to include.
118
+ previous: An earlier snapshot; files whose ``(mtime, size)`` are
119
+ unchanged reuse their digest instead of being re-read.
120
+ """
121
+ files: Dict[str, str] = {}
122
+ mtimes: Dict[str, Tuple[int, int]] = {}
123
+ prev_mtimes = previous.mtimes if previous is not None else {}
124
+ prev_files = previous.files if previous is not None else {}
125
+ for subdir in subdirs:
126
+ base = os.path.join(root, subdir)
127
+ if not os.path.isdir(base):
128
+ continue
129
+ for dirpath, dirnames, filenames in os.walk(base):
130
+ dirnames[:] = sorted(d for d in dirnames if d not in _IGNORED_DIRS)
131
+ for name in sorted(filenames):
132
+ full = os.path.join(dirpath, name)
133
+ rel = os.path.relpath(full, root).replace(os.sep, "/")
134
+ if not is_synced_file(rel):
135
+ continue
136
+ try:
137
+ stat = os.stat(full)
138
+ except OSError:
139
+ continue
140
+ if stat.st_size > MAX_SYNC_FILE_BYTES:
141
+ continue
142
+ key = (stat.st_mtime_ns, stat.st_size)
143
+ if prev_mtimes.get(rel) == key and rel in prev_files:
144
+ files[rel] = prev_files[rel]
145
+ mtimes[rel] = key
146
+ continue
147
+ try:
148
+ with open(full, "rb") as handle:
149
+ data = handle.read()
150
+ except OSError:
151
+ continue
152
+ files[rel] = hashlib.sha256(data).hexdigest()
153
+ mtimes[rel] = key
154
+ return SourceSnapshot(root=os.path.abspath(root), files=files, mtimes=mtimes, version=_tree_version(files))
155
+
156
+
157
+ class FileWatcher:
158
+ """Poll a source tree and report changes on a background thread.
159
+
160
+ Args:
161
+ root: Project root.
162
+ on_change: Called with a
163
+ [`SourceChange`][pythonnative.devserver.watcher.SourceChange]
164
+ and the new snapshot after every poll that found changes.
165
+ subdirs: Directories under ``root`` to watch.
166
+ interval: Seconds between polls.
167
+ settle: Seconds a change must be stable before it is reported,
168
+ so an editor's write-then-rename or a multi-file save lands
169
+ as one reload instead of several.
170
+ """
171
+
172
+ def __init__(
173
+ self,
174
+ root: str,
175
+ on_change: Callable[[SourceChange, SourceSnapshot], None],
176
+ *,
177
+ subdirs: Sequence[str] = ("app",),
178
+ interval: float = 0.25,
179
+ settle: float = 0.08,
180
+ ) -> None:
181
+ self.root = os.path.abspath(root)
182
+ self.on_change = on_change
183
+ self.subdirs = tuple(subdirs)
184
+ self.interval = interval
185
+ self.settle = settle
186
+ self._snapshot = snapshot_sources(self.root, self.subdirs)
187
+ self._thread: Optional[threading.Thread] = None
188
+ self._stop = threading.Event()
189
+ self._lock = threading.Lock()
190
+
191
+ @property
192
+ def snapshot(self) -> SourceSnapshot:
193
+ """The most recent snapshot."""
194
+ with self._lock:
195
+ return self._snapshot
196
+
197
+ def start(self) -> None:
198
+ """Start polling on a daemon thread (idempotent)."""
199
+ if self._thread is not None:
200
+ return
201
+ self._stop.clear()
202
+ self._thread = threading.Thread(target=self._loop, name="pn-file-watcher", daemon=True)
203
+ self._thread.start()
204
+
205
+ def stop(self) -> None:
206
+ """Stop polling and join the thread."""
207
+ self._stop.set()
208
+ thread, self._thread = self._thread, None
209
+ if thread is not None:
210
+ thread.join(timeout=max(1.0, self.interval * 4))
211
+
212
+ def poll(self) -> Optional[SourceChange]:
213
+ """Scan once and return the change since the last scan (or ``None``)."""
214
+ with self._lock:
215
+ previous = self._snapshot
216
+ current = snapshot_sources(self.root, self.subdirs, previous=previous)
217
+ change = previous.diff(current)
218
+ if not change:
219
+ return None
220
+ if self.settle > 0:
221
+ # Wait for the tree to stop changing so partial writes never ship.
222
+ deadline = time.monotonic() + max(self.settle * 20, 2.0)
223
+ while time.monotonic() < deadline:
224
+ time.sleep(self.settle)
225
+ settled = snapshot_sources(self.root, self.subdirs, previous=current)
226
+ if settled.version == current.version:
227
+ break
228
+ current = settled
229
+ change = previous.diff(current)
230
+ if not change:
231
+ return None
232
+ with self._lock:
233
+ self._snapshot = current
234
+ return change
235
+
236
+ def _loop(self) -> None:
237
+ while not self._stop.wait(self.interval):
238
+ try:
239
+ change = self.poll()
240
+ except Exception:
241
+ continue
242
+ if change is None:
243
+ continue
244
+ try:
245
+ self.on_change(change, self.snapshot)
246
+ except Exception:
247
+ pass
248
+
249
+
250
+ def modules_for_paths(paths: Sequence[str]) -> List[str]:
251
+ """Map synced ``.py`` paths (``app/screens/home.py``) to dotted modules."""
252
+ modules: List[str] = []
253
+ seen: Set[str] = set()
254
+ for path in paths:
255
+ norm = path.replace("\\", "/")
256
+ if not norm.endswith(".py"):
257
+ continue
258
+ parts = norm[:-3].split("/")
259
+ if parts and parts[-1] == "__init__":
260
+ parts = parts[:-1]
261
+ if not parts or not all(part.isidentifier() for part in parts):
262
+ continue
263
+ dotted = ".".join(parts)
264
+ if dotted not in seen:
265
+ seen.add(dotted)
266
+ modules.append(dotted)
267
+ return modules
@@ -0,0 +1,421 @@
1
+ """A small RFC 6455 (WebSocket) implementation on the standard library.
2
+
3
+ The dev server and the on-device dev client both need WebSockets, and
4
+ neither can assume a third-party package: the client runs inside the
5
+ embedded interpreter on iOS and Android, where every dependency has to
6
+ be bundled. This module provides just enough of the protocol for a
7
+ trusted development network:
8
+
9
+ - [`encode_frame`][pythonnative.devserver.ws.encode_frame] and
10
+ [`FrameDecoder`][pythonnative.devserver.ws.FrameDecoder] handle the
11
+ wire format (masking, 7/16/64-bit lengths, fragmentation, control
12
+ frames).
13
+ - [`server_handshake`][pythonnative.devserver.ws.server_handshake] and
14
+ [`client_handshake_request`][pythonnative.devserver.ws.client_handshake_request]
15
+ build the HTTP upgrade.
16
+ - [`WebSocketClient`][pythonnative.devserver.ws.WebSocketClient] is a
17
+ blocking client meant to live on a background thread (the dev client
18
+ uses one; the main thread never waits on the network).
19
+
20
+ Extensions (compression) and subprotocols are not negotiated.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import base64
26
+ import hashlib
27
+ import os
28
+ import socket
29
+ import struct
30
+ import threading
31
+ from typing import Dict, Iterator, List, Optional, Tuple
32
+ from urllib.parse import urlsplit
33
+
34
+ __all__ = [
35
+ "CLOSE",
36
+ "BINARY",
37
+ "CONTINUATION",
38
+ "PING",
39
+ "PONG",
40
+ "TEXT",
41
+ "FrameDecoder",
42
+ "HandshakeError",
43
+ "WebSocketClient",
44
+ "WebSocketError",
45
+ "accept_key",
46
+ "client_handshake_request",
47
+ "encode_close",
48
+ "encode_frame",
49
+ "parse_http_headers",
50
+ "server_handshake",
51
+ ]
52
+
53
+ _GUID = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
54
+
55
+ CONTINUATION = 0x0
56
+ TEXT = 0x1
57
+ BINARY = 0x2
58
+ CLOSE = 0x8
59
+ PING = 0x9
60
+ PONG = 0xA
61
+
62
+ _CONTROL_OPCODES = {CLOSE, PING, PONG}
63
+ MAX_MESSAGE_BYTES = 64 * 1024 * 1024
64
+ """Upper bound on one reassembled message; anything larger is a protocol error."""
65
+
66
+
67
+ class WebSocketError(Exception):
68
+ """A framing or protocol violation on the connection."""
69
+
70
+
71
+ class HandshakeError(WebSocketError):
72
+ """The HTTP upgrade did not complete."""
73
+
74
+
75
+ # ======================================================================
76
+ # Handshake
77
+ # ======================================================================
78
+
79
+
80
+ def accept_key(client_key: str) -> str:
81
+ """Return the ``Sec-WebSocket-Accept`` value for ``client_key``."""
82
+ digest = hashlib.sha1(client_key.strip().encode("ascii") + _GUID).digest()
83
+ return base64.b64encode(digest).decode("ascii")
84
+
85
+
86
+ def parse_http_headers(raw: bytes) -> Tuple[str, Dict[str, str]]:
87
+ """Split an HTTP request or response head into ``(start_line, headers)``.
88
+
89
+ Header names are lower-cased. ``raw`` should be the bytes up to
90
+ (and optionally including) the blank line that ends the head.
91
+ """
92
+ text = raw.decode("iso-8859-1")
93
+ lines = text.split("\r\n")
94
+ start_line = lines[0].strip()
95
+ headers: Dict[str, str] = {}
96
+ for line in lines[1:]:
97
+ if not line.strip():
98
+ break
99
+ name, sep, value = line.partition(":")
100
+ if sep:
101
+ headers[name.strip().lower()] = value.strip()
102
+ return start_line, headers
103
+
104
+
105
+ def server_handshake(headers: Dict[str, str]) -> bytes:
106
+ """Build the ``101 Switching Protocols`` response for an upgrade request.
107
+
108
+ Args:
109
+ headers: Lower-cased request headers (see
110
+ [`parse_http_headers`][pythonnative.devserver.ws.parse_http_headers]).
111
+
112
+ Raises:
113
+ HandshakeError: When the request is not a WebSocket upgrade.
114
+ """
115
+ if "websocket" not in headers.get("upgrade", "").lower():
116
+ raise HandshakeError("not a WebSocket upgrade request")
117
+ key = headers.get("sec-websocket-key")
118
+ if not key:
119
+ raise HandshakeError("missing Sec-WebSocket-Key")
120
+ return (
121
+ "HTTP/1.1 101 Switching Protocols\r\n"
122
+ "Upgrade: websocket\r\n"
123
+ "Connection: Upgrade\r\n"
124
+ f"Sec-WebSocket-Accept: {accept_key(key)}\r\n"
125
+ "\r\n"
126
+ ).encode("ascii")
127
+
128
+
129
+ def client_handshake_request(host: str, path: str, key: Optional[str] = None) -> Tuple[bytes, str]:
130
+ """Build a client upgrade request; returns ``(request_bytes, key)``."""
131
+ if key is None:
132
+ key = base64.b64encode(os.urandom(16)).decode("ascii")
133
+ request = (
134
+ f"GET {path or '/'} HTTP/1.1\r\n"
135
+ f"Host: {host}\r\n"
136
+ "Upgrade: websocket\r\n"
137
+ "Connection: Upgrade\r\n"
138
+ f"Sec-WebSocket-Key: {key}\r\n"
139
+ "Sec-WebSocket-Version: 13\r\n"
140
+ "\r\n"
141
+ ).encode("ascii")
142
+ return request, key
143
+
144
+
145
+ # ======================================================================
146
+ # Framing
147
+ # ======================================================================
148
+
149
+
150
+ def encode_frame(opcode: int, payload: bytes, *, mask: bool = False, fin: bool = True) -> bytes:
151
+ """Encode one frame.
152
+
153
+ Clients must send masked frames and servers unmasked ones; the
154
+ caller picks. Text payloads must already be UTF-8 encoded.
155
+ """
156
+ head = bytearray()
157
+ head.append((0x80 if fin else 0x00) | (opcode & 0x0F))
158
+ length = len(payload)
159
+ mask_bit = 0x80 if mask else 0x00
160
+ if length < 126:
161
+ head.append(mask_bit | length)
162
+ elif length < 65536:
163
+ head.append(mask_bit | 126)
164
+ head += struct.pack("!H", length)
165
+ else:
166
+ head.append(mask_bit | 127)
167
+ head += struct.pack("!Q", length)
168
+ if not mask:
169
+ return bytes(head) + payload
170
+ key = os.urandom(4)
171
+ head += key
172
+ return bytes(head) + _apply_mask(payload, key)
173
+
174
+
175
+ def encode_close(code: int = 1000, reason: str = "", *, mask: bool = False) -> bytes:
176
+ """Encode a close frame carrying ``code`` and ``reason``."""
177
+ body = struct.pack("!H", code) + reason.encode("utf-8")
178
+ return encode_frame(CLOSE, body, mask=mask)
179
+
180
+
181
+ def _apply_mask(data: bytes, key: bytes) -> bytes:
182
+ """XOR ``data`` with the repeating 4-byte ``key`` (masking is symmetric)."""
183
+ if not data:
184
+ return b""
185
+ # Extend the key across the payload and XOR the two as big integers;
186
+ # this is far faster than a per-byte loop in pure Python.
187
+ repeated = (key * (len(data) // 4 + 1))[: len(data)]
188
+ return (int.from_bytes(data, "big") ^ int.from_bytes(repeated, "big")).to_bytes(len(data), "big")
189
+
190
+
191
+ class FrameDecoder:
192
+ """Incremental frame parser that reassembles fragmented messages.
193
+
194
+ Feed raw bytes with [`feed`][pythonnative.devserver.ws.FrameDecoder.feed];
195
+ it yields complete ``(opcode, payload)`` messages. Control frames
196
+ (ping, pong, close) are yielded as they arrive, even in the middle
197
+ of a fragmented data message, as the RFC allows.
198
+ """
199
+
200
+ def __init__(self) -> None:
201
+ self._buffer = bytearray()
202
+ self._fragments: List[bytes] = []
203
+ self._fragment_opcode: Optional[int] = None
204
+
205
+ def feed(self, data: bytes) -> Iterator[Tuple[int, bytes]]:
206
+ """Consume ``data`` and yield every message it completes."""
207
+ self._buffer += data
208
+ while True:
209
+ parsed = self._parse_one()
210
+ if parsed is None:
211
+ return
212
+ fin, opcode, payload = parsed
213
+ if opcode in _CONTROL_OPCODES:
214
+ if not fin:
215
+ raise WebSocketError("fragmented control frame")
216
+ yield opcode, payload
217
+ continue
218
+ if opcode == CONTINUATION:
219
+ if self._fragment_opcode is None:
220
+ raise WebSocketError("continuation frame without a start")
221
+ self._fragments.append(payload)
222
+ else:
223
+ if self._fragment_opcode is not None:
224
+ raise WebSocketError("new data frame while a message is fragmented")
225
+ self._fragment_opcode = opcode
226
+ self._fragments = [payload]
227
+ if sum(len(f) for f in self._fragments) > MAX_MESSAGE_BYTES:
228
+ raise WebSocketError("message exceeds the size limit")
229
+ if fin:
230
+ message_opcode = self._fragment_opcode
231
+ message = b"".join(self._fragments)
232
+ self._fragments = []
233
+ self._fragment_opcode = None
234
+ yield message_opcode, message
235
+
236
+ def _parse_one(self) -> Optional[Tuple[bool, int, bytes]]:
237
+ buf = self._buffer
238
+ if len(buf) < 2:
239
+ return None
240
+ first, second = buf[0], buf[1]
241
+ fin = bool(first & 0x80)
242
+ if first & 0x70:
243
+ raise WebSocketError("reserved bits set (extensions are not negotiated)")
244
+ opcode = first & 0x0F
245
+ masked = bool(second & 0x80)
246
+ length = second & 0x7F
247
+ offset = 2
248
+ if length == 126:
249
+ if len(buf) < 4:
250
+ return None
251
+ length = struct.unpack("!H", bytes(buf[2:4]))[0]
252
+ offset = 4
253
+ elif length == 127:
254
+ if len(buf) < 10:
255
+ return None
256
+ length = struct.unpack("!Q", bytes(buf[2:10]))[0]
257
+ offset = 10
258
+ if length > MAX_MESSAGE_BYTES:
259
+ raise WebSocketError("frame exceeds the size limit")
260
+ key = b""
261
+ if masked:
262
+ if len(buf) < offset + 4:
263
+ return None
264
+ key = bytes(buf[offset : offset + 4])
265
+ offset += 4
266
+ if len(buf) < offset + length:
267
+ return None
268
+ payload = bytes(buf[offset : offset + length])
269
+ del buf[: offset + length]
270
+ if masked:
271
+ payload = _apply_mask(payload, key)
272
+ return fin, opcode, payload
273
+
274
+
275
+ # ======================================================================
276
+ # Blocking client
277
+ # ======================================================================
278
+
279
+
280
+ class WebSocketClient:
281
+ """A blocking WebSocket client for background threads.
282
+
283
+ ``recv`` blocks until a text message arrives and transparently
284
+ answers pings; ``send_text`` is safe to call from any thread.
285
+
286
+ Args:
287
+ url: ``ws://host:port/path`` (``wss`` is not supported; dev
288
+ traffic stays on the local network).
289
+ timeout: Connect and read timeout in seconds. Reads that time
290
+ out raise ``socket.timeout``, letting the owning thread
291
+ check for shutdown between waits.
292
+ """
293
+
294
+ def __init__(self, url: str, *, timeout: Optional[float] = 30.0) -> None:
295
+ parts = urlsplit(url)
296
+ if parts.scheme not in ("ws", "http"):
297
+ raise ValueError(f"unsupported WebSocket scheme in {url!r} (use ws://)")
298
+ if not parts.hostname:
299
+ raise ValueError(f"missing host in {url!r}")
300
+ self.host = parts.hostname
301
+ self.port = parts.port or 80
302
+ self.path = (parts.path or "/") + (f"?{parts.query}" if parts.query else "")
303
+ self.timeout = timeout
304
+ self._sock: Optional[socket.socket] = None
305
+ self._decoder = FrameDecoder()
306
+ self._send_lock = threading.Lock()
307
+ self._closed = False
308
+
309
+ @property
310
+ def connected(self) -> bool:
311
+ """Whether the socket is open."""
312
+ return self._sock is not None and not self._closed
313
+
314
+ def connect(self) -> None:
315
+ """Open the TCP connection and complete the upgrade handshake."""
316
+ sock = socket.create_connection((self.host, self.port), timeout=self.timeout)
317
+ sock.settimeout(self.timeout)
318
+ request, key = client_handshake_request(f"{self.host}:{self.port}", self.path)
319
+ sock.sendall(request)
320
+ head = b""
321
+ while b"\r\n\r\n" not in head:
322
+ chunk = sock.recv(4096)
323
+ if not chunk:
324
+ sock.close()
325
+ raise HandshakeError("connection closed during the WebSocket handshake")
326
+ head += chunk
327
+ if len(head) > 65536:
328
+ sock.close()
329
+ raise HandshakeError("oversized handshake response")
330
+ head_bytes, _, rest = head.partition(b"\r\n\r\n")
331
+ status, headers = parse_http_headers(head_bytes)
332
+ if " 101 " not in f" {status} ":
333
+ sock.close()
334
+ raise HandshakeError(f"server refused the upgrade: {status}")
335
+ if headers.get("sec-websocket-accept") != accept_key(key):
336
+ sock.close()
337
+ raise HandshakeError("bad Sec-WebSocket-Accept from server")
338
+ self._sock = sock
339
+ self._closed = False
340
+ if rest:
341
+ # Frames may already trail the handshake; keep them.
342
+ self._pending = list(self._decoder.feed(rest))
343
+ else:
344
+ self._pending = []
345
+
346
+ def send_text(self, text: str) -> None:
347
+ """Send one text message (masked, as clients must)."""
348
+ self._send(encode_frame(TEXT, text.encode("utf-8"), mask=True))
349
+
350
+ def _send(self, frame: bytes) -> None:
351
+ sock = self._sock
352
+ if sock is None or self._closed:
353
+ raise WebSocketError("socket is closed")
354
+ with self._send_lock:
355
+ sock.sendall(frame)
356
+
357
+ def recv(self) -> Optional[str]:
358
+ """Block until a text message arrives.
359
+
360
+ Returns ``None`` once the peer closes. Raises ``socket.timeout``
361
+ when the read timeout elapses with no data, so callers can poll
362
+ a shutdown flag.
363
+ """
364
+ while True:
365
+ if self._pending:
366
+ opcode, payload = self._pending.pop(0)
367
+ else:
368
+ sock = self._sock
369
+ if sock is None or self._closed:
370
+ return None
371
+ chunk = sock.recv(65536)
372
+ if not chunk:
373
+ self._mark_closed()
374
+ return None
375
+ messages = list(self._decoder.feed(chunk))
376
+ if not messages:
377
+ continue
378
+ opcode, payload = messages[0]
379
+ self._pending = messages[1:]
380
+ if opcode == TEXT:
381
+ return payload.decode("utf-8")
382
+ if opcode == PING:
383
+ try:
384
+ self._send(encode_frame(PONG, payload, mask=True))
385
+ except OSError:
386
+ self._mark_closed()
387
+ return None
388
+ continue
389
+ if opcode == CLOSE:
390
+ try:
391
+ self._send(encode_close(mask=True))
392
+ except OSError:
393
+ pass
394
+ self._mark_closed()
395
+ return None
396
+ # Binary and pong frames are ignored by the dev protocol.
397
+
398
+ def close(self, code: int = 1000, reason: str = "") -> None:
399
+ """Send a close frame (best effort) and shut the socket."""
400
+ sock = self._sock
401
+ if sock is None:
402
+ return
403
+ if not self._closed:
404
+ try:
405
+ self._send(encode_close(code, reason, mask=True))
406
+ except OSError:
407
+ pass
408
+ self._mark_closed()
409
+
410
+ def _mark_closed(self) -> None:
411
+ self._closed = True
412
+ sock, self._sock = self._sock, None
413
+ if sock is not None:
414
+ try:
415
+ sock.shutdown(socket.SHUT_RDWR)
416
+ except OSError:
417
+ pass
418
+ try:
419
+ sock.close()
420
+ except OSError:
421
+ pass