wapyt 0.2.0.dev1__tar.gz → 0.2.0.dev2__tar.gz

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 (154) hide show
  1. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/PKG-INFO +2 -1
  2. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/README.md +1 -0
  3. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/pyproject.toml +1 -1
  4. wapyt-0.2.0.dev2/tests/test_mediaplayer.py +127 -0
  5. wapyt-0.2.0.dev2/tests/test_source_warnings.py +18 -0
  6. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/__init__.py +7 -1
  7. wapyt-0.2.0.dev2/wapyt/assets/mediaplayer.js +1211 -0
  8. wapyt-0.2.0.dev2/wapyt/assets/vendor-hlsjs.LICENSE.md +28 -0
  9. wapyt-0.2.0.dev2/wapyt/assets/vendor-hlsjs.light.min.js +1 -0
  10. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/wapyt.css +443 -0
  11. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/layout/layout.py +5 -0
  12. wapyt-0.2.0.dev2/wapyt/mediaplayer/__init__.py +4 -0
  13. wapyt-0.2.0.dev2/wapyt/mediaplayer/mediaplayer.py +367 -0
  14. wapyt-0.2.0.dev2/wapyt/mediaplayer/mediaplayer_config.py +303 -0
  15. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/pytincture-assets.json +12 -2
  16. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/scheduler/scheduler_config.py +1 -1
  17. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt.egg-info/PKG-INFO +2 -1
  18. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt.egg-info/SOURCES.txt +8 -0
  19. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/MANIFEST.in +0 -0
  20. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/setup.cfg +0 -0
  21. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_chart.py +0 -0
  22. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_chat_stream.py +0 -0
  23. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_contextmenu.py +0 -0
  24. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_datatable_classes.py +0 -0
  25. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_datatable_csv.py +0 -0
  26. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_datatable_editing.py +0 -0
  27. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_datatable_frozen.py +0 -0
  28. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_filetransfer_csrf.py +0 -0
  29. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_form_buttons.py +0 -0
  30. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_form_clear.py +0 -0
  31. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_form_fields.py +0 -0
  32. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_form_fieldsets.py +0 -0
  33. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_form_files.py +0 -0
  34. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_form_focus.py +0 -0
  35. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_form_options_icons.py +0 -0
  36. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_form_properties.py +0 -0
  37. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_form_range.py +0 -0
  38. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_form_validators.py +0 -0
  39. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_jsnull.py +0 -0
  40. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_kanban.py +0 -0
  41. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_layout_splitters.py +0 -0
  42. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_listbox.py +0 -0
  43. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_menubar.py +0 -0
  44. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_message.py +0 -0
  45. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_modal_events.py +0 -0
  46. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_new_widget_configs.py +0 -0
  47. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_pagination.py +0 -0
  48. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_popup.py +0 -0
  49. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_progressbar.py +0 -0
  50. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_release_metadata.py +0 -0
  51. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_scheduler.py +0 -0
  52. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_sidebar_groups.py +0 -0
  53. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_toolbar.py +0 -0
  54. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_toolbar_dropdowns.py +0 -0
  55. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_tree_check_move.py +0 -0
  56. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/tests/test_window.py +0 -0
  57. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/_runtime.py +0 -0
  58. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/__init__.py +0 -0
  59. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/cardpanel.js +0 -0
  60. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/chart.js +0 -0
  61. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/chat.js +0 -0
  62. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/contextmenu.js +0 -0
  63. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/datatable.js +0 -0
  64. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/filetransfer.js +0 -0
  65. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/fonts/.keep +0 -0
  66. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/form.js +0 -0
  67. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/icons.js +0 -0
  68. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/kanban.js +0 -0
  69. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/layout.js +0 -0
  70. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/listbox.js +0 -0
  71. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/menubar.js +0 -0
  72. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/message.js +0 -0
  73. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/modal.js +0 -0
  74. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/pagination.js +0 -0
  75. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/popup.js +0 -0
  76. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/progressbar.js +0 -0
  77. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/resourceboard.js +0 -0
  78. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/scheduler.js +0 -0
  79. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/sidebar.js +0 -0
  80. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/tabwidget.js +0 -0
  81. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/terminal.js +0 -0
  82. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/toolbar.js +0 -0
  83. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/tooltip.js +0 -0
  84. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/tree.js +0 -0
  85. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/vendor-chartjs.LICENSE.md +0 -0
  86. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/vendor-chartjs.min.js +0 -0
  87. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/assets/window.js +0 -0
  88. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/cardpanel/__init__.py +0 -0
  89. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/cardpanel/cardpanel.py +0 -0
  90. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/cardpanel/cardpanel_config.py +0 -0
  91. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/chart/__init__.py +0 -0
  92. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/chart/chart.py +0 -0
  93. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/chart/chart_config.py +0 -0
  94. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/chat/__init__.py +0 -0
  95. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/chat/chat.py +0 -0
  96. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/chat/chat_config.py +0 -0
  97. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/contextmenu/__init__.py +0 -0
  98. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/contextmenu/contextmenu.py +0 -0
  99. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/contextmenu/contextmenu_config.py +0 -0
  100. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/datatable/__init__.py +0 -0
  101. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/datatable/datatable.py +0 -0
  102. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/datatable/datatable_config.py +0 -0
  103. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/filetransfer/__init__.py +0 -0
  104. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/filetransfer/filetransfer.py +0 -0
  105. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/form/__init__.py +0 -0
  106. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/form/form.py +0 -0
  107. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/form/form_config.py +0 -0
  108. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/kanban/__init__.py +0 -0
  109. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/kanban/kanban.py +0 -0
  110. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/kanban/kanban_config.py +0 -0
  111. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/layout/__init__.py +0 -0
  112. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/layout/layout_config.py +0 -0
  113. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/listbox/__init__.py +0 -0
  114. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/listbox/listbox.py +0 -0
  115. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/listbox/listbox_config.py +0 -0
  116. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/menubar/__init__.py +0 -0
  117. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/menubar/menubar.py +0 -0
  118. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/menubar/menubar_config.py +0 -0
  119. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/message/__init__.py +0 -0
  120. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/message/message.py +0 -0
  121. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/modal/__init__.py +0 -0
  122. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/modal/modal.py +0 -0
  123. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/pagination/__init__.py +0 -0
  124. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/pagination/pagination.py +0 -0
  125. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/pagination/pagination_config.py +0 -0
  126. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/popup/__init__.py +0 -0
  127. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/popup/popup.py +0 -0
  128. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/popup/popup_config.py +0 -0
  129. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/progressbar/__init__.py +0 -0
  130. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/progressbar/progressbar.py +0 -0
  131. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/progressbar/progressbar_config.py +0 -0
  132. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/resourceboard/__init__.py +0 -0
  133. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/resourceboard/resourceboard.py +0 -0
  134. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/resourceboard/resourceboard_config.py +0 -0
  135. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/scheduler/__init__.py +0 -0
  136. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/scheduler/scheduler.py +0 -0
  137. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/sidebar/__init__.py +0 -0
  138. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/sidebar/sidebar.py +0 -0
  139. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/sidebar/sidebar_config.py +0 -0
  140. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/tabwidget/__init__.py +0 -0
  141. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/tabwidget/tabwidget.py +0 -0
  142. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/terminal/__init__.py +0 -0
  143. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/terminal/terminal.py +0 -0
  144. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/terminal/terminal_config.py +0 -0
  145. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/toolbar/__init__.py +0 -0
  146. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/toolbar/toolbar.py +0 -0
  147. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/toolbar/toolbar_config.py +0 -0
  148. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/tree/__init__.py +0 -0
  149. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/tree/tree.py +0 -0
  150. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/tree/tree_config.py +0 -0
  151. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/window/__init__.py +0 -0
  152. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt/window/window.py +0 -0
  153. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt.egg-info/dependency_links.txt +0 -0
  154. {wapyt-0.2.0.dev1 → wapyt-0.2.0.dev2}/wapyt.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: wapyt
3
- Version: 0.2.0.dev1
3
+ Version: 0.2.0.dev2
4
4
  Summary: DHTMLX-free pyTincture widgetset: layouts, forms, tables, trees, chat, terminal, menus, Kanban and more, declared in Python and rendered in the browser.
5
5
  License: MIT
6
6
  Project-URL: homepage, https://github.com/WAwesome-AI/wa_pytincture_widgetset
@@ -38,6 +38,7 @@ Description-Content-Type: text/markdown
38
38
  - `Popup` and tooltips: An anchored popover for pickers, help and small forms; styled, keyboard-reachable tooltips that replace native `title` text across the widgets.
39
39
  - `ProgressBar` / `progress_html`: Determinate or indeterminate bars with active/done/error/paused states, as a live widget or static markup.
40
40
  - `Pagination`: First/previous/next/last, a page box or numbered buttons, a page-size selector and a "1–50 of 1,234" summary; copes with totals the server could not count.
41
+ - `MediaPlayer`: Video and audio with a queue — the picture in place, an audio bar that can dock to the foot of the window, or a compact inline player for a chat message; HLS through bundled hls.js, WebVTT subtitles, items whose URL is resolved only when reached, exclusive groups, keyboard and OS media keys, buttons of the app's own in the bar (`MediaAction`, `on_action`: "add to playlist", "save"), and any media source: files from disk, a CDN, cookies, Bearer headers (`MediaItem(headers=…)`), expiring signed URLs (`refresh()`), live streams (`live=True`) and a Quality menu.
41
42
 
42
43
  ## Runtime Architecture
43
44
  - Layouts and widgets are declared in Python but render as DOM nodes through the JavaScript bundles under `wapyt/assets/`. Each helper (e.g., `Layout.add_chat`) forwards config dictionaries directly to the matching JS constructor.
@@ -26,6 +26,7 @@
26
26
  - `Popup` and tooltips: An anchored popover for pickers, help and small forms; styled, keyboard-reachable tooltips that replace native `title` text across the widgets.
27
27
  - `ProgressBar` / `progress_html`: Determinate or indeterminate bars with active/done/error/paused states, as a live widget or static markup.
28
28
  - `Pagination`: First/previous/next/last, a page box or numbered buttons, a page-size selector and a "1–50 of 1,234" summary; copes with totals the server could not count.
29
+ - `MediaPlayer`: Video and audio with a queue — the picture in place, an audio bar that can dock to the foot of the window, or a compact inline player for a chat message; HLS through bundled hls.js, WebVTT subtitles, items whose URL is resolved only when reached, exclusive groups, keyboard and OS media keys, buttons of the app's own in the bar (`MediaAction`, `on_action`: "add to playlist", "save"), and any media source: files from disk, a CDN, cookies, Bearer headers (`MediaItem(headers=…)`), expiring signed URLs (`refresh()`), live streams (`live=True`) and a Quality menu.
29
30
 
30
31
  ## Runtime Architecture
31
32
  - Layouts and widgets are declared in Python but render as DOM nodes through the JavaScript bundles under `wapyt/assets/`. Each helper (e.g., `Layout.add_chat`) forwards config dictionaries directly to the matching JS constructor.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "wapyt"
7
- version = "0.2.0.dev1"
7
+ version = "0.2.0.dev2"
8
8
  description = "DHTMLX-free pyTincture widgetset: layouts, forms, tables, trees, chat, terminal, menus, Kanban and more, declared in Python and rendered in the browser."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -0,0 +1,127 @@
1
+ """CPython tests for the MediaPlayer config (no browser needed)."""
2
+ from __future__ import annotations
3
+
4
+ import pytest
5
+
6
+ import wapyt
7
+ from wapyt import MediaItem, MediaPlayerConfig, MediaTextTrack
8
+ from wapyt.mediaplayer.mediaplayer import resolve_patch
9
+
10
+
11
+ def test_exported_with_a_layout_helper():
12
+ from wapyt.layout.layout import Layout
13
+ assert {"MediaPlayer", "MediaPlayerConfig", "MediaItem", "MediaTextTrack"} <= set(wapyt.__all__)
14
+ assert hasattr(Layout, "add_mediaplayer")
15
+
16
+
17
+ def test_defaults_are_minimal():
18
+ assert MediaPlayerConfig().to_dict() == {"mode": "auto"}
19
+ assert MediaItem("/a.mp4").to_dict() == {"src": "/a.mp4"}
20
+ assert MediaTextTrack("/a.vtt").to_dict() == {"src": "/a.vtt"}
21
+
22
+
23
+ def test_an_item_without_src_is_allowed():
24
+ # Resolved when it becomes current (the "resolve" event).
25
+ assert MediaItem(title="Later", data={"id": 7}).to_dict() == {"title": "Later", "data": {"id": 7}}
26
+
27
+
28
+ def test_options_are_camel_cased():
29
+ payload = MediaPlayerConfig(
30
+ items=[MediaItem("/a.m3u8", title="A", subtitle="S", album="Al", poster="/p.jpg", type="hls",
31
+ kind="video", start=12.5, data={"id": 1},
32
+ text_tracks=[MediaTextTrack("/a.vtt", "English", "en", kind="captions", default=True)])],
33
+ mode="audio", autoplay=True, controls=False, muted=True, volume=0.5, rate=1.5, repeat="all",
34
+ shuffle=True, dock="bottom", group="chat", keyboard=False, media_session=False, queue_line=False,
35
+ show_shuffle=False, show_repeat=False, time_update_interval=0, preload="none",
36
+ cross_origin="anonymous", hls_config={"maxBufferLength": 30}, height=64, empty_text="—",
37
+ aria_label="Music",
38
+ ).to_dict()
39
+ assert payload["items"] == [{"src": "/a.m3u8", "title": "A", "subtitle": "S", "album": "Al", "poster": "/p.jpg",
40
+ "type": "hls", "kind": "video", "start": 12.5, "data": {"id": 1},
41
+ "textTracks": [{"src": "/a.vtt", "label": "English", "language": "en",
42
+ "kind": "captions", "default": True}]}]
43
+ for key, value in {"mode": "audio", "autoplay": True, "controls": False, "muted": True, "volume": 0.5,
44
+ "rate": 1.5, "repeat": "all", "shuffle": True, "dock": "bottom", "group": "chat",
45
+ "keyboard": False, "mediaSession": False, "queueLine": False, "showShuffle": False,
46
+ "showRepeat": False, "timeUpdateInterval": 0, "preload": "none",
47
+ "crossOrigin": "anonymous", "hlsConfig": {"maxBufferLength": 30}, "height": 64,
48
+ "emptyText": "—", "ariaLabel": "Music"}.items():
49
+ assert payload[key] == value, key
50
+
51
+
52
+ def test_dict_items_pass_through():
53
+ assert MediaPlayerConfig(items=[{"src": "/x.mp3"}]).to_dict()["items"] == [{"src": "/x.mp3"}]
54
+
55
+
56
+ @pytest.mark.parametrize("bad", [dict(mode="theatre"), dict(repeat="twice"), dict(dock="top"), dict(preload="all"),
57
+ dict(cross_origin="yes"), dict(volume=1.5), dict(rate=8),
58
+ dict(time_update_interval=-1),
59
+ dict(items=[MediaItem("/a.mp4")], start_index=1)])
60
+ def test_invalid_configs(bad):
61
+ with pytest.raises(ValueError):
62
+ MediaPlayerConfig(**bad).to_dict()
63
+
64
+
65
+ @pytest.mark.parametrize("bad", [dict(kind="podcast"), dict(start=-1), dict(data={1, 2})])
66
+ def test_invalid_items(bad):
67
+ with pytest.raises(ValueError):
68
+ MediaItem("/a.mp4", **bad).to_dict()
69
+
70
+
71
+ def test_invalid_text_tracks():
72
+ with pytest.raises(ValueError):
73
+ MediaTextTrack("/a.vtt", kind="karaoke").to_dict()
74
+ with pytest.raises(ValueError):
75
+ MediaTextTrack("").to_dict()
76
+
77
+
78
+ def test_resolve_patch_shape():
79
+ assert resolve_patch("/s.m3u8", type="hls") == {"src": "/s.m3u8", "type": "hls"}
80
+ assert resolve_patch(error="gone") == {"error": "gone"}
81
+ assert resolve_patch("/a", text_tracks=[MediaTextTrack("/a.vtt", "En")]) == {
82
+ "src": "/a", "textTracks": [{"src": "/a.vtt", "label": "En"}]}
83
+ with pytest.raises(ValueError):
84
+ resolve_patch("/a", kind="slideshow")
85
+
86
+
87
+ def test_assets_are_in_the_manifest_in_order():
88
+ import json
89
+ from pathlib import Path
90
+ manifest = json.loads((Path(wapyt.__file__).parent / "pytincture-assets.json").read_text())
91
+ paths = [a["path"] for a in manifest["assets"]]
92
+ assert paths.index("wapyt/assets/vendor-hlsjs.light.min.js") < paths.index("wapyt/assets/mediaplayer.js")
93
+
94
+
95
+ def test_actions_are_the_apps_own_buttons():
96
+ from wapyt import MediaAction
97
+ payload = MediaPlayerConfig(actions=[
98
+ MediaAction("playlist", "mdi-playlist-plus", "Add to playlist"),
99
+ MediaAction("like", "mdi-heart-outline", "Like", pressed=False, disabled=True),
100
+ ]).to_dict()
101
+ assert payload["actions"] == [
102
+ {"id": "playlist", "icon": "mdi-playlist-plus", "label": "Add to playlist"},
103
+ {"id": "like", "icon": "mdi-heart-outline", "label": "Like", "pressed": False, "disabled": True},
104
+ ]
105
+ assert MediaAction("x").to_dict() == {"id": "x", "icon": "mdi-dots-horizontal", "label": "x"}
106
+ with pytest.raises(ValueError):
107
+ MediaAction("").to_dict()
108
+ with pytest.raises(ValueError):
109
+ MediaPlayerConfig(actions=[MediaAction("a"), MediaAction("a")]).to_dict()
110
+ assert "actions" not in MediaPlayerConfig().to_dict()
111
+
112
+
113
+ def test_headers_and_live_for_any_media_server():
114
+ item = MediaItem("/live/channel.m3u8", headers={"Authorization": "Bearer abc", "X-Api-Key": "k"}, live=True)
115
+ assert item.to_dict() == {"src": "/live/channel.m3u8", "headers": {"Authorization": "Bearer abc", "X-Api-Key": "k"}, "live": True}
116
+ for bad in ({"Bad Name": "x"}, {"X": "two\r\nlines"}, {"X": 3}):
117
+ with pytest.raises(ValueError):
118
+ MediaItem("/a.m3u8", headers=bad).to_dict()
119
+ assert resolve_patch("/s.m3u8", headers={"Authorization": "Bearer t"}, live=False) == {
120
+ "src": "/s.m3u8", "headers": {"Authorization": "Bearer t"}, "live": False}
121
+ with pytest.raises(ValueError):
122
+ resolve_patch("/s.m3u8", headers={"A": "b\nc"})
123
+
124
+
125
+ def test_quality_menu_can_be_turned_off():
126
+ assert MediaPlayerConfig(show_quality=False).to_dict() == {"mode": "auto", "showQuality": False}
127
+ assert "showQuality" not in MediaPlayerConfig().to_dict()
@@ -0,0 +1,18 @@
1
+ """Every wapyt module compiles without warnings (no invalid escapes and the like)."""
2
+ from __future__ import annotations
3
+
4
+ import pathlib
5
+ import warnings
6
+
7
+ import pytest
8
+
9
+ ROOT = pathlib.Path(__file__).resolve().parents[1] / "wapyt"
10
+
11
+
12
+ @pytest.mark.parametrize("path", sorted(ROOT.rglob("*.py")), ids=lambda p: str(p.relative_to(ROOT)))
13
+ def test_compiles_without_warnings(path):
14
+ # An invalid escape such as "\;" in a normal string is a SyntaxWarning
15
+ # today (on every import, in every app) and an error in a future Python.
16
+ with warnings.catch_warnings():
17
+ warnings.simplefilter("error")
18
+ compile(path.read_text(encoding="utf-8"), str(path), "exec")
@@ -7,7 +7,7 @@ import re as _re
7
7
  __widgetset__ = "wapyt"
8
8
  # A plain string literal: pytincture reads it by parsing this file, and it must
9
9
  # match pyproject.toml and the asset manifest (CI and the release workflow check).
10
- __version__ = "0.2.0.dev1"
10
+ __version__ = "0.2.0.dev2"
11
11
  # The numeric release part only, so pre-releases ("0.2.0.dev0", "0.2.0rc1")
12
12
  # do not break `int()`.
13
13
  __version_tuple__ = tuple(int(part) for part in _re.match(r"\d+(?:\.\d+)*", __version__).group(0).split("."))
@@ -24,6 +24,7 @@ from .sidebar.sidebar_config import SidebarConfig, SidebarHeading, SidebarItem,
24
24
  from .modal.modal import ModalWindow, ModalConfig
25
25
  from .window.window import Window, WindowConfig
26
26
  from .chart import Chart, ChartConfig, ChartDataset
27
+ from .mediaplayer import MediaAction, MediaItem, MediaPlayer, MediaPlayerConfig, MediaTextTrack
27
28
  from .scheduler import Scheduler, SchedulerConfig, ScheduleItem
28
29
  from .form.form import Form
29
30
  from .datatable.datatable import DataTable
@@ -89,6 +90,11 @@ __all__ = [
89
90
  "Chart",
90
91
  "ChartConfig",
91
92
  "ChartDataset",
93
+ "MediaPlayer",
94
+ "MediaPlayerConfig",
95
+ "MediaItem",
96
+ "MediaTextTrack",
97
+ "MediaAction",
92
98
  "Scheduler",
93
99
  "SchedulerConfig",
94
100
  "ScheduleItem",