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.
- pythonnative/__init__.py +411 -0
- pythonnative/_ios_log.py +92 -0
- pythonnative/_yoga.cp313-win_amd64.pyd +0 -0
- pythonnative/alerts.py +236 -0
- pythonnative/animated.py +1687 -0
- pythonnative/animation_graph.py +78 -0
- pythonnative/appearance.py +124 -0
- pythonnative/bootstrap.py +100 -0
- pythonnative/bridge/__init__.py +302 -0
- pythonnative/bridge/android.py +78 -0
- pythonnative/bridge/codec.py +170 -0
- pythonnative/bridge/commits.py +114 -0
- pythonnative/bridge/fake.py +243 -0
- pythonnative/bridge/ios.py +138 -0
- pythonnative/bridge/web.py +489 -0
- pythonnative/cli/__init__.py +7 -0
- pythonnative/cli/pn.py +1214 -0
- pythonnative/component.py +271 -0
- pythonnative/components/__init__.py +111 -0
- pythonnative/components/_base.py +166 -0
- pythonnative/components/controls.py +521 -0
- pythonnative/components/layout.py +518 -0
- pythonnative/components/lists.py +785 -0
- pythonnative/components/media.py +204 -0
- pythonnative/components/overlays.py +107 -0
- pythonnative/components/pressable.py +252 -0
- pythonnative/components/structural.py +167 -0
- pythonnative/components/text.py +291 -0
- pythonnative/devclient.py +824 -0
- pythonnative/devserver/__init__.py +34 -0
- pythonnative/devserver/server.py +718 -0
- pythonnative/devserver/static/animation_graph.js +82 -0
- pythonnative/devserver/static/bridge.js +147 -0
- pythonnative/devserver/static/colors.js +70 -0
- pythonnative/devserver/static/host.js +552 -0
- pythonnative/devserver/static/index.html +60 -0
- pythonnative/devserver/static/layout.js +85 -0
- pythonnative/devserver/static/preview.css +415 -0
- pythonnative/devserver/static/renderer.js +1994 -0
- pythonnative/devserver/static/shell.js +345 -0
- pythonnative/devserver/static/yoga/LICENSE +21 -0
- pythonnative/devserver/static/yoga/binaries/yoga-wasm-base64-esm.js +75 -0
- pythonnative/devserver/static/yoga/src/generated/YGEnums.d.ts +189 -0
- pythonnative/devserver/static/yoga/src/generated/YGEnums.js +211 -0
- pythonnative/devserver/static/yoga/src/generated/YGEnums.js.map +1 -0
- pythonnative/devserver/static/yoga/src/index.d.ts +12 -0
- pythonnative/devserver/static/yoga/src/index.js +16 -0
- pythonnative/devserver/static/yoga/src/index.js.map +1 -0
- pythonnative/devserver/static/yoga/src/load.d.ts +11 -0
- pythonnative/devserver/static/yoga/src/load.js +17 -0
- pythonnative/devserver/static/yoga/src/load.js.map +1 -0
- pythonnative/devserver/static/yoga/src/wrapAssembly.d.ts +155 -0
- pythonnative/devserver/static/yoga/src/wrapAssembly.js +125 -0
- pythonnative/devserver/static/yoga/src/wrapAssembly.js.map +1 -0
- pythonnative/devserver/watcher.py +267 -0
- pythonnative/devserver/ws.py +421 -0
- pythonnative/diagnostics.py +241 -0
- pythonnative/element.py +159 -0
- pythonnative/equality.py +19 -0
- pythonnative/events.py +231 -0
- pythonnative/gestures.py +1333 -0
- pythonnative/hooks.py +1621 -0
- pythonnative/hosts/__init__.py +63 -0
- pythonnative/hosts/base.py +596 -0
- pythonnative/hosts/native.py +302 -0
- pythonnative/hot_reload.py +585 -0
- pythonnative/layout.py +328 -0
- pythonnative/mutations.py +142 -0
- pythonnative/native/android/build.gradle +52 -0
- pythonnative/native/android/gradle.properties +3 -0
- pythonnative/native/android/settings.gradle +12 -0
- pythonnative/native/android/src/main/AndroidManifest.xml +11 -0
- pythonnative/native/android/src/main/cpp/CMakeLists.txt +9 -0
- pythonnative/native/android/src/main/cpp/yoga_jni.cpp +52 -0
- pythonnative/native/android/src/main/java/com/pythonnative/generated/NativeModules.kt +5 -0
- pythonnative/native/android/src/main/java/com/pythonnative/generated/NativeProps.kt +3705 -0
- pythonnative/native/android/src/main/java/com/pythonnative/generated/PNContracts.kt +129 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/PNBridge.kt +261 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/PythonHost.kt +20 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/AnimationGraph.kt +108 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/AnimationSpecs.kt +141 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/animation/PNAnimator.kt +257 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/CommitState.kt +106 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/JsonUtil.kt +175 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/MainThread.kt +40 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNLog.kt +33 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNRegistry.kt +82 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/PNTransaction.kt +80 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/TransactionApplier.kt +117 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/bridge/ViewRegistry.kt +74 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/BuiltinComponents.kt +37 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ButtonManager.kt +32 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ComponentManager.kt +213 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ContainerManagers.kt +69 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ControlManagers.kt +186 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/DatePickerManager.kt +123 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ImageLoader.kt +141 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ImageManager.kt +162 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ModalManager.kt +134 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PNColor.kt +201 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PickerManager.kt +86 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PortalManager.kt +55 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/PressableManager.kt +122 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ScreenStackManager.kt +110 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ScrollViewManager.kt +214 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/SegmentedControlManager.kt +113 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/StatusBarManager.kt +52 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TabBarManager.kt +131 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TextInputManager.kt +250 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/TextManager.kt +233 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/ViewStyler.kt +355 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/VirtualListManager.kt +159 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/components/WebViewManager.kt +149 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureArbiter.kt +233 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureCoordinator.kt +127 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/gestures/GestureRecognizers.kt +527 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/layout/NativeLayout.kt +145 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/layout/YogaNode.kt +29 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/BuiltinModules.kt +73 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/DeviceModules.kt +316 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/HostModule.kt +61 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/MediaModules.kt +200 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/NativeModule.kt +158 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/NotificationsModule.kt +100 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/PermissionsModule.kt +99 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/StorageModules.kt +89 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/modules/SystemModules.kt +178 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/plugins/GeneratedPlugins.kt +16 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/screens/PNScreenFragment.kt +188 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/screens/ScreenRegistry.kt +80 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNAccessibilityDelegate.kt +48 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNBorderDrawable.kt +76 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNEditText.kt +18 -0
- pythonnative/native/android/src/main/java/com/pythonnative/runtime/views/PNFrameLayout.kt +49 -0
- pythonnative/native/android/src/test/java/com/pythonnative/runtime/AnimationSpecsTest.kt +75 -0
- pythonnative/native/android/src/test/java/com/pythonnative/runtime/GestureArbiterTest.kt +197 -0
- pythonnative/native/android/src/test/java/com/pythonnative/runtime/PNColorTest.kt +47 -0
- pythonnative/native/android/src/test/java/com/pythonnative/runtime/PNTransactionTest.kt +78 -0
- pythonnative/native/android/src/test/java/com/pythonnative/runtime/PromiseTest.kt +89 -0
- pythonnative/native/ios/Package.swift +23 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Animation/PNAnimationGraph.swift +199 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Animation/PNAnimator.swift +312 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNBridge.swift +255 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNCommit.swift +104 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNRegistry.swift +142 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNTransaction.swift +166 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Bridge/PNViewRegistry.swift +53 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNButtonManagers.swift +207 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNColor.swift +173 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNComponentManager.swift +183 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNContainerView.swift +56 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNControlManagers.swift +281 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNImageManager.swift +222 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNOverlayManagers.swift +243 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNPressableManager.swift +114 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNScreenStackManager.swift +107 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNScrollViewManager.swift +197 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTabBarManager.swift +106 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTextInputManager.swift +297 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTextManager.swift +230 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNTransform.swift +82 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewManager.swift +86 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewState.swift +87 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNViewStyler.swift +340 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNVirtualListManager.swift +155 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Components/PNWebViewManager.swift +124 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Generated/NativeModules.swift +1 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Generated/NativeProps.swift +3775 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Generated/PNContracts.swift +46 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Gestures/PNGestureCoordinator.swift +260 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Layout/PNLayout.swift +173 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/AlertModule.swift +92 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/HostModule.swift +80 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/LifecycleModules.swift +174 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/MediaModules.swift +177 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/PNNativeModule.swift +168 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/PermissionModules.swift +266 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Modules/SystemModules.swift +251 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Plugins/PNPluginRegistration.swift +8 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Screens/PNScreenRegistry.swift +46 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Screens/PNViewController.swift +237 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Support/PNCompat.swift +46 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Support/PNJSON.swift +173 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Support/PNLog.swift +47 -0
- pythonnative/native/ios/Sources/PythonNativeKit/Support/PNWindow.swift +51 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNAnimationGraphTests.swift +26 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNColorTests.swift +50 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNCommitTests.swift +86 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNGestureTests.swift +101 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNManagerTests.swift +126 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNModuleTests.swift +111 -0
- pythonnative/native/ios/Tests/PythonNativeKitTests/PNTransactionTests.swift +91 -0
- pythonnative/native/yoga/LICENSE +21 -0
- pythonnative/native/yoga/Package.swift +9 -0
- pythonnative/native/yoga/VENDORED.md +2 -0
- pythonnative/native/yoga/include/PNStyle.h +10 -0
- pythonnative/native/yoga/include/module.modulemap +5 -0
- pythonnative/native/yoga/include/yoga/YGConfig.h +158 -0
- pythonnative/native/yoga/include/yoga/YGEnums.h +142 -0
- pythonnative/native/yoga/include/yoga/YGMacros.h +99 -0
- pythonnative/native/yoga/include/yoga/YGNode.h +304 -0
- pythonnative/native/yoga/include/yoga/YGNodeLayout.h +35 -0
- pythonnative/native/yoga/include/yoga/YGNodeStyle.h +130 -0
- pythonnative/native/yoga/include/yoga/YGPixelGrid.h +29 -0
- pythonnative/native/yoga/include/yoga/YGValue.h +84 -0
- pythonnative/native/yoga/include/yoga/Yoga.h +21 -0
- pythonnative/native/yoga/python.cpp +16 -0
- pythonnative/native/yoga/style.cpp +272 -0
- pythonnative/native/yoga/yoga/CMakeLists.txt +41 -0
- pythonnative/native/yoga/yoga/YGConfig.cpp +92 -0
- pythonnative/native/yoga/yoga/YGConfig.h +158 -0
- pythonnative/native/yoga/yoga/YGEnums.cpp +262 -0
- pythonnative/native/yoga/yoga/YGEnums.h +142 -0
- pythonnative/native/yoga/yoga/YGMacros.h +99 -0
- pythonnative/native/yoga/yoga/YGNode.cpp +366 -0
- pythonnative/native/yoga/yoga/YGNode.h +304 -0
- pythonnative/native/yoga/yoga/YGNodeLayout.cpp +92 -0
- pythonnative/native/yoga/yoga/YGNodeLayout.h +35 -0
- pythonnative/native/yoga/yoga/YGNodeStyle.cpp +405 -0
- pythonnative/native/yoga/yoga/YGNodeStyle.h +130 -0
- pythonnative/native/yoga/yoga/YGPixelGrid.cpp +22 -0
- pythonnative/native/yoga/yoga/YGPixelGrid.h +29 -0
- pythonnative/native/yoga/yoga/YGValue.cpp +20 -0
- pythonnative/native/yoga/yoga/YGValue.h +84 -0
- pythonnative/native/yoga/yoga/Yoga.h +21 -0
- pythonnative/native/yoga/yoga/algorithm/AbsoluteLayout.cpp +563 -0
- pythonnative/native/yoga/yoga/algorithm/AbsoluteLayout.h +41 -0
- pythonnative/native/yoga/yoga/algorithm/Align.h +72 -0
- pythonnative/native/yoga/yoga/algorithm/Baseline.cpp +78 -0
- pythonnative/native/yoga/yoga/algorithm/Baseline.h +21 -0
- pythonnative/native/yoga/yoga/algorithm/BoundAxis.h +79 -0
- pythonnative/native/yoga/yoga/algorithm/Cache.cpp +121 -0
- pythonnative/native/yoga/yoga/algorithm/Cache.h +30 -0
- pythonnative/native/yoga/yoga/algorithm/CalculateLayout.cpp +2428 -0
- pythonnative/native/yoga/yoga/algorithm/CalculateLayout.h +38 -0
- pythonnative/native/yoga/yoga/algorithm/FlexDirection.h +120 -0
- pythonnative/native/yoga/yoga/algorithm/FlexLine.cpp +124 -0
- pythonnative/native/yoga/yoga/algorithm/FlexLine.h +75 -0
- pythonnative/native/yoga/yoga/algorithm/PixelGrid.cpp +132 -0
- pythonnative/native/yoga/yoga/algorithm/PixelGrid.h +29 -0
- pythonnative/native/yoga/yoga/algorithm/SizingMode.h +73 -0
- pythonnative/native/yoga/yoga/algorithm/TrailingPosition.h +44 -0
- pythonnative/native/yoga/yoga/config/Config.cpp +135 -0
- pythonnative/native/yoga/yoga/config/Config.h +92 -0
- pythonnative/native/yoga/yoga/debug/AssertFatal.cpp +53 -0
- pythonnative/native/yoga/yoga/debug/AssertFatal.h +29 -0
- pythonnative/native/yoga/yoga/debug/Log.cpp +107 -0
- pythonnative/native/yoga/yoga/debug/Log.h +34 -0
- pythonnative/native/yoga/yoga/enums/Align.h +47 -0
- pythonnative/native/yoga/yoga/enums/BoxSizing.h +40 -0
- pythonnative/native/yoga/yoga/enums/Dimension.h +40 -0
- pythonnative/native/yoga/yoga/enums/Direction.h +41 -0
- pythonnative/native/yoga/yoga/enums/Display.h +41 -0
- pythonnative/native/yoga/yoga/enums/Edge.h +47 -0
- pythonnative/native/yoga/yoga/enums/Errata.h +41 -0
- pythonnative/native/yoga/yoga/enums/ExperimentalFeature.h +39 -0
- pythonnative/native/yoga/yoga/enums/FlexDirection.h +42 -0
- pythonnative/native/yoga/yoga/enums/Gutter.h +41 -0
- pythonnative/native/yoga/yoga/enums/Justify.h +44 -0
- pythonnative/native/yoga/yoga/enums/LogLevel.h +44 -0
- pythonnative/native/yoga/yoga/enums/MeasureMode.h +41 -0
- pythonnative/native/yoga/yoga/enums/NodeType.h +40 -0
- pythonnative/native/yoga/yoga/enums/Overflow.h +41 -0
- pythonnative/native/yoga/yoga/enums/PhysicalEdge.h +21 -0
- pythonnative/native/yoga/yoga/enums/PositionType.h +41 -0
- pythonnative/native/yoga/yoga/enums/Unit.h +42 -0
- pythonnative/native/yoga/yoga/enums/Wrap.h +41 -0
- pythonnative/native/yoga/yoga/enums/YogaEnums.h +85 -0
- pythonnative/native/yoga/yoga/event/event.cpp +87 -0
- pythonnative/native/yoga/yoga/event/event.h +130 -0
- pythonnative/native/yoga/yoga/module.modulemap +21 -0
- pythonnative/native/yoga/yoga/node/CachedMeasurement.h +53 -0
- pythonnative/native/yoga/yoga/node/LayoutResults.cpp +48 -0
- pythonnative/native/yoga/yoga/node/LayoutResults.h +122 -0
- pythonnative/native/yoga/yoga/node/LayoutableChildren.h +148 -0
- pythonnative/native/yoga/yoga/node/Node.cpp +443 -0
- pythonnative/native/yoga/yoga/node/Node.h +337 -0
- pythonnative/native/yoga/yoga/numeric/Comparison.h +81 -0
- pythonnative/native/yoga/yoga/numeric/FloatOptional.h +93 -0
- pythonnative/native/yoga/yoga/style/SmallValueBuffer.h +133 -0
- pythonnative/native/yoga/yoga/style/Style.h +757 -0
- pythonnative/native/yoga/yoga/style/StyleLength.h +107 -0
- pythonnative/native/yoga/yoga/style/StyleValueHandle.h +98 -0
- pythonnative/native/yoga/yoga/style/StyleValuePool.h +127 -0
- pythonnative/native_modules/__init__.py +124 -0
- pythonnative/native_modules/app_state.py +99 -0
- pythonnative/native_modules/battery.py +75 -0
- pythonnative/native_modules/biometrics.py +42 -0
- pythonnative/native_modules/camera.py +65 -0
- pythonnative/native_modules/clipboard.py +46 -0
- pythonnative/native_modules/fallback.py +324 -0
- pythonnative/native_modules/file_system.py +145 -0
- pythonnative/native_modules/haptics.py +75 -0
- pythonnative/native_modules/linking.py +123 -0
- pythonnative/native_modules/location.py +75 -0
- pythonnative/native_modules/net_info.py +112 -0
- pythonnative/native_modules/notifications.py +129 -0
- pythonnative/native_modules/permissions.py +108 -0
- pythonnative/native_modules/registry.py +526 -0
- pythonnative/native_modules/secure_store.py +52 -0
- pythonnative/native_modules/share.py +51 -0
- pythonnative/native_views/__init__.py +375 -0
- pythonnative/native_views/base.py +316 -0
- pythonnative/native_views/bridge_backend.py +277 -0
- pythonnative/navigation/__init__.py +103 -0
- pythonnative/navigation/container.py +154 -0
- pythonnative/navigation/handle.py +540 -0
- pythonnative/navigation/hooks.py +133 -0
- pythonnative/navigation/host.py +58 -0
- pythonnative/navigation/linking.py +200 -0
- pythonnative/navigation/navigators.py +659 -0
- pythonnative/navigation/screen.py +149 -0
- pythonnative/navigation/state.py +269 -0
- pythonnative/net.py +248 -0
- pythonnative/platform.py +166 -0
- pythonnative/platform_metrics.py +252 -0
- pythonnative/preview.py +287 -0
- pythonnative/profiling.py +101 -0
- pythonnative/project/__init__.py +68 -0
- pythonnative/project/android.py +557 -0
- pythonnative/project/builder.py +817 -0
- pythonnative/project/config.py +773 -0
- pythonnative/project/deps.py +719 -0
- pythonnative/project/devices.py +304 -0
- pythonnative/project/doctor.py +283 -0
- pythonnative/project/fingerprint.py +171 -0
- pythonnative/project/icons.py +247 -0
- pythonnative/project/ios.py +325 -0
- pythonnative/project/lockfile.py +100 -0
- pythonnative/project/permissions.py +361 -0
- pythonnative/project/plugins.py +519 -0
- pythonnative/project/runtime_assets.py +183 -0
- pythonnative/py.typed +0 -0
- pythonnative/query.py +153 -0
- pythonnative/reconciler/__init__.py +29 -0
- pythonnative/reconciler/boundaries.py +375 -0
- pythonnative/reconciler/children.py +88 -0
- pythonnative/reconciler/core.py +1165 -0
- pythonnative/reconciler/layout_pass.py +393 -0
- pythonnative/reconciler/vnode.py +266 -0
- pythonnative/refresh.py +55 -0
- pythonnative/runtime.py +306 -0
- pythonnative/scheduler.py +159 -0
- pythonnative/sdk/__init__.py +153 -0
- pythonnative/sdk/_components.py +427 -0
- pythonnative/sdk/builtins.py +91 -0
- pythonnative/sdk/codegen.py +149 -0
- pythonnative/sdk/module_codegen.py +184 -0
- pythonnative/sdk/schema.py +224 -0
- pythonnative/sdk/templates/contracts.kt +46 -0
- pythonnative/sdk/templates/contracts.swift +46 -0
- pythonnative/storage.py +184 -0
- pythonnative/style.py +841 -0
- pythonnative/suspense.py +183 -0
- pythonnative/templates/android_template/app/build.gradle +75 -0
- pythonnative/templates/android_template/app/proguard-rules.pro +21 -0
- pythonnative/templates/android_template/app/src/androidTest/java/com/pythonnative/android_template/ExampleInstrumentedTest.kt +24 -0
- pythonnative/templates/android_template/app/src/debug/AndroidManifest.xml +7 -0
- pythonnative/templates/android_template/app/src/main/AndroidManifest.xml +32 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/MainActivity.kt +130 -0
- pythonnative/templates/android_template/app/src/main/java/com/pythonnative/android_template/ScreenFragment.kt +13 -0
- pythonnative/templates/android_template/app/src/main/res/drawable/ic_launcher_background.xml +170 -0
- pythonnative/templates/android_template/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +30 -0
- pythonnative/templates/android_template/app/src/main/res/layout/activity_main.xml +10 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +6 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +6 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp +0 -0
- pythonnative/templates/android_template/app/src/main/res/navigation/nav_graph.xml +22 -0
- pythonnative/templates/android_template/app/src/main/res/values/colors.xml +5 -0
- pythonnative/templates/android_template/app/src/main/res/values/strings.xml +5 -0
- pythonnative/templates/android_template/app/src/main/res/values/themes.xml +9 -0
- pythonnative/templates/android_template/app/src/main/res/values-night/themes.xml +7 -0
- pythonnative/templates/android_template/app/src/main/res/xml/backup_rules.xml +13 -0
- pythonnative/templates/android_template/app/src/main/res/xml/data_extraction_rules.xml +19 -0
- pythonnative/templates/android_template/app/src/test/java/com/pythonnative/android_template/ExampleUnitTest.kt +17 -0
- pythonnative/templates/android_template/build.gradle +10 -0
- pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.jar +0 -0
- pythonnative/templates/android_template/gradle/wrapper/gradle-wrapper.properties +6 -0
- pythonnative/templates/android_template/gradle.properties +23 -0
- pythonnative/templates/android_template/gradlew +185 -0
- pythonnative/templates/android_template/gradlew.bat +89 -0
- pythonnative/templates/android_template/settings.gradle +17 -0
- pythonnative/templates/ios_template/ios_template/AppDelegate.swift +51 -0
- pythonnative/templates/ios_template/ios_template/Assets.xcassets/AccentColor.colorset/Contents.json +11 -0
- pythonnative/templates/ios_template/ios_template/Assets.xcassets/AppIcon.appiconset/Contents.json +13 -0
- pythonnative/templates/ios_template/ios_template/Assets.xcassets/Contents.json +6 -0
- pythonnative/templates/ios_template/ios_template/Base.lproj/LaunchScreen.storyboard +25 -0
- pythonnative/templates/ios_template/ios_template/BridgingHeader.h +14 -0
- pythonnative/templates/ios_template/ios_template/Info.plist +28 -0
- pythonnative/templates/ios_template/ios_template/PythonRuntime.swift +390 -0
- pythonnative/templates/ios_template/ios_template/SceneDelegate.swift +61 -0
- pythonnative/templates/ios_template/ios_template/ViewController.swift +23 -0
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.pbxproj +745 -0
- pythonnative/templates/ios_template/ios_template.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- pythonnative/templates/ios_template/ios_templateTests/ios_templateTests.swift +36 -0
- pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITests.swift +41 -0
- pythonnative/templates/ios_template/ios_templateUITests/ios_templateUITestsLaunchTests.swift +32 -0
- pythonnative/testing/__init__.py +53 -0
- pythonnative/testing/backend.py +276 -0
- pythonnative/testing/harness.py +369 -0
- pythonnative/utils.py +145 -0
- pythonnative-0.40.0.dist-info/DELVEWHEEL +2 -0
- pythonnative-0.40.0.dist-info/METADATA +150 -0
- pythonnative-0.40.0.dist-info/RECORD +418 -0
- pythonnative-0.40.0.dist-info/WHEEL +5 -0
- pythonnative-0.40.0.dist-info/entry_points.txt +2 -0
- pythonnative-0.40.0.dist-info/licenses/LICENSE +21 -0
- pythonnative-0.40.0.dist-info/top_level.txt +1 -0
- 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
|