winipedia-utils 0.1.16__py3-none-any.whl → 0.1.18__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. winipedia_utils/modules/module.py +4 -0
  2. winipedia_utils/pyside/__init__.py +1 -0
  3. winipedia_utils/pyside/ui/__init__.py +1 -0
  4. winipedia_utils/pyside/ui/base/__init__.py +1 -0
  5. winipedia_utils/pyside/ui/base/base.py +117 -0
  6. winipedia_utils/pyside/ui/pages/__init__.py +1 -0
  7. winipedia_utils/pyside/ui/pages/base/__init__.py +1 -0
  8. winipedia_utils/pyside/ui/pages/base/base.py +78 -0
  9. winipedia_utils/pyside/ui/pages/browser.py +24 -0
  10. winipedia_utils/pyside/ui/pages/player.py +26 -0
  11. winipedia_utils/pyside/ui/widgets/__init__.py +1 -0
  12. winipedia_utils/pyside/ui/widgets/browser.py +166 -0
  13. winipedia_utils/pyside/ui/widgets/media_player.py +265 -0
  14. winipedia_utils/pyside/ui/widgets/notification.py +56 -0
  15. winipedia_utils/pyside/ui/windows/__init__.py +1 -0
  16. winipedia_utils/pyside/ui/windows/base/__init__.py +1 -0
  17. winipedia_utils/pyside/ui/windows/base/base.py +56 -0
  18. winipedia_utils/resources/__init__.py +1 -0
  19. winipedia_utils/resources/svgs/__init__.py +1 -0
  20. winipedia_utils/resources/svgs/download_arrow.svg +3 -0
  21. winipedia_utils/resources/svgs/exit_fullscreen_icon.svg +7 -0
  22. winipedia_utils/resources/svgs/fullscreen_icon.svg +4 -0
  23. winipedia_utils/resources/svgs/pause_icon.svg +4 -0
  24. winipedia_utils/resources/svgs/play_icon.svg +8 -0
  25. winipedia_utils/resources/svgs/svg.py +11 -0
  26. winipedia_utils/security/__init__.py +1 -0
  27. winipedia_utils/security/keyring.py +30 -0
  28. {winipedia_utils-0.1.16.dist-info → winipedia_utils-0.1.18.dist-info}/METADATA +6 -2
  29. {winipedia_utils-0.1.16.dist-info → winipedia_utils-0.1.18.dist-info}/RECORD +31 -5
  30. {winipedia_utils-0.1.16.dist-info → winipedia_utils-0.1.18.dist-info}/LICENSE +0 -0
  31. {winipedia_utils-0.1.16.dist-info → winipedia_utils-0.1.18.dist-info}/WHEEL +0 -0
@@ -11,6 +11,7 @@ making them suitable for code generation, testing frameworks, and dynamic import
11
11
 
12
12
  import inspect
13
13
  import os
14
+ import sys
14
15
  import time
15
16
  from collections.abc import Callable, Sequence
16
17
  from importlib import import_module
@@ -115,6 +116,9 @@ def to_path(module_name: str | ModuleType | Path, *, is_package: bool) -> Path:
115
116
  """
116
117
  module_name = to_module_name(module_name)
117
118
  path = Path(module_name.replace(".", os.sep))
119
+ # for smth like pyinstaller we support frozen path
120
+ if getattr(sys, "frozen", False):
121
+ path = Path(getattr(sys, "_MEIPASS", "")) / path
118
122
  if is_package:
119
123
  return path
120
124
  return path.with_suffix(".py")
@@ -0,0 +1 @@
1
+ """__init__ module for winipedia_utils.pyside6."""
@@ -0,0 +1 @@
1
+ """__init__ module for winipedia_utils.pyside6.ui."""
@@ -0,0 +1 @@
1
+ """__init__ module."""
@@ -0,0 +1,117 @@
1
+ """Base UI module.
2
+
3
+ This module contains the base UI class for the VideoVault application.
4
+ """
5
+
6
+ from abc import abstractmethod
7
+ from types import ModuleType
8
+ from typing import TYPE_CHECKING, Any, Self, cast, final
9
+
10
+ from PySide6.QtCore import QObject
11
+ from PySide6.QtGui import QIcon
12
+ from PySide6.QtWidgets import QStackedWidget
13
+
14
+ from winipedia_utils.modules.package import walk_package
15
+ from winipedia_utils.oop.mixins.meta import ABCImplementationLoggingMeta
16
+ from winipedia_utils.resources.svgs.svg import get_svg_path
17
+ from winipedia_utils.text.string import split_on_uppercase
18
+
19
+ # Avoid circular import
20
+ if TYPE_CHECKING:
21
+ from winipedia_utils.pyside.ui.pages.base.base import Base as BasePage
22
+
23
+
24
+ class QABCImplementationLoggingMeta(
25
+ ABCImplementationLoggingMeta,
26
+ type(QObject), # type: ignore[misc]
27
+ ):
28
+ """Metaclass for the QABCImplementationLoggingMixin."""
29
+
30
+
31
+ class Base(metaclass=QABCImplementationLoggingMeta):
32
+ """Base UI class for a Qt application."""
33
+
34
+ @final
35
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
36
+ """Initialize the base UI."""
37
+ super().__init__(*args, **kwargs)
38
+ self.base_setup()
39
+ self.pre_setup()
40
+ self.setup()
41
+ self.post_setup()
42
+
43
+ @abstractmethod
44
+ def base_setup(self) -> None:
45
+ """Get the Qt object of the UI."""
46
+
47
+ @abstractmethod
48
+ def setup(self) -> None:
49
+ """Setup the UI."""
50
+
51
+ @abstractmethod
52
+ def pre_setup(self) -> None:
53
+ """Setup the UI."""
54
+
55
+ @abstractmethod
56
+ def post_setup(self) -> None:
57
+ """Setup the UI."""
58
+
59
+ @classmethod
60
+ @final
61
+ def get_display_name(cls) -> str:
62
+ """Get the display name of the UI."""
63
+ return " ".join(split_on_uppercase(cls.__name__))
64
+
65
+ @classmethod
66
+ @final
67
+ def get_subclasses(cls, package: ModuleType) -> list[type[Self]]:
68
+ """Get all subclasses of the UI.
69
+
70
+ Args:
71
+ package: The package to search for subclasses in.
72
+ """
73
+ _ = list(walk_package(package))
74
+
75
+ children = cls.__subclasses__()
76
+ return sorted(children, key=lambda cls: cls.__name__)
77
+
78
+ @final
79
+ def set_current_page(self, page_cls: type["BasePage"]) -> None:
80
+ """Set the current page."""
81
+ self.get_stack().setCurrentWidget(self.get_page(page_cls))
82
+
83
+ @final
84
+ def get_stack(self) -> QStackedWidget:
85
+ """Get the stack of the window."""
86
+ from winipedia_utils.pyside.ui.windows.base.base import Base as BaseWindow
87
+
88
+ window = getattr(self, "window", lambda: None)()
89
+
90
+ if not isinstance(window, BaseWindow):
91
+ msg = f"Cannot get stack on {window.__class__.__name__}"
92
+ raise TypeError(msg)
93
+
94
+ return window.stack
95
+
96
+ @final
97
+ def get_stack_pages(self) -> list["BasePage"]:
98
+ """Get all the pages."""
99
+ # Import here to avoid circular import
100
+
101
+ stack = self.get_stack()
102
+ # get all the pages
103
+ return [cast("BasePage", stack.widget(i)) for i in range(stack.count())]
104
+
105
+ @final
106
+ def get_page[T: "BasePage"](self, page_cls: type[T]) -> T:
107
+ """Get the page."""
108
+ page = next(
109
+ page for page in self.get_stack_pages() if page.__class__ is page_cls
110
+ )
111
+ return cast("T", page)
112
+
113
+ @classmethod
114
+ @final
115
+ def get_svg_icon(cls, svg_name: str) -> QIcon:
116
+ """Get the Qicon for a svg."""
117
+ return QIcon(str(get_svg_path(svg_name)))
@@ -0,0 +1 @@
1
+ """__init__ module."""
@@ -0,0 +1 @@
1
+ """__init__ module."""
@@ -0,0 +1,78 @@
1
+ """Base page module.
2
+
3
+ This module contains the base page class for the VideoVault application.
4
+ """
5
+
6
+ from functools import partial
7
+ from types import ModuleType
8
+ from typing import final
9
+
10
+ from PySide6.QtCore import Qt
11
+ from PySide6.QtWidgets import (
12
+ QHBoxLayout,
13
+ QLayout,
14
+ QMenu,
15
+ QPushButton,
16
+ QSizePolicy,
17
+ QVBoxLayout,
18
+ QWidget,
19
+ )
20
+
21
+ from winipedia_utils.pyside.ui.base.base import Base as BaseUI
22
+
23
+
24
+ class Base(BaseUI, QWidget):
25
+ """Base page class for the VideoVault application."""
26
+
27
+ PAGES_PACKAGE: ModuleType = NotImplemented
28
+
29
+ @final
30
+ def base_setup(self) -> None:
31
+ """Get the Qt object of the UI."""
32
+ self.v_layout = QVBoxLayout()
33
+ self.setLayout(self.v_layout)
34
+
35
+ # add a horizontal layout for the top row
36
+ self.h_layout = QHBoxLayout()
37
+ self.v_layout.addLayout(self.h_layout)
38
+
39
+ self.add_menu_dropdown_button()
40
+
41
+ @final
42
+ def add_menu_dropdown_button(self) -> None:
43
+ """Add a dropdown menu that leadds to each page."""
44
+ self.menu_button = QPushButton("Menu")
45
+ self.menu_button.setSizePolicy(
46
+ QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum
47
+ )
48
+ self.h_layout.addWidget(
49
+ self.menu_button,
50
+ alignment=Qt.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft,
51
+ )
52
+ self.menu_dropdown = QMenu(self.menu_button)
53
+ self.menu_button.setMenu(self.menu_dropdown)
54
+
55
+ for page_cls in self.get_page_classes():
56
+ action = self.menu_dropdown.addAction(page_cls.get_display_name())
57
+ action.triggered.connect(partial(self.set_current_page, page_cls))
58
+
59
+ @final
60
+ def add_to_page_button(
61
+ self, to_page_cls: type["Base"], layout: QLayout
62
+ ) -> QPushButton:
63
+ """Add a button to go to the page."""
64
+ button = QPushButton(to_page_cls.get_display_name())
65
+
66
+ # connect to open page on click
67
+ button.clicked.connect(lambda: self.set_current_page(to_page_cls))
68
+
69
+ # add to layout
70
+ layout.addWidget(button)
71
+
72
+ return button
73
+
74
+ @classmethod
75
+ @final
76
+ def get_page_classes(cls) -> list[type["Base"]]:
77
+ """Get all the page classes."""
78
+ return Base.get_subclasses(cls.PAGES_PACKAGE)
@@ -0,0 +1,24 @@
1
+ """Add downloads page module.
2
+
3
+ This module contains the add downloads page class for the VideoVault application.
4
+ """
5
+
6
+ from typing import final
7
+
8
+ from winipedia_utils.pyside.ui.pages.base.base import Base as BasePage
9
+ from winipedia_utils.pyside.ui.widgets.browser import Browser as BrowserWidget
10
+
11
+
12
+ class Browser(BasePage):
13
+ """Add downloads page for the VideoVault application."""
14
+
15
+ @final
16
+ def setup(self) -> None:
17
+ """Setup the UI."""
18
+ # add a download button in the top right
19
+ self.add_brwoser()
20
+
21
+ @final
22
+ def add_brwoser(self) -> None:
23
+ """Add a browser to surfe the web."""
24
+ self.browser = BrowserWidget(self.v_layout)
@@ -0,0 +1,26 @@
1
+ """Player page module.
2
+
3
+ This module contains the player page class for the VideoVault application.
4
+ """
5
+
6
+ from typing import final
7
+
8
+ from winipedia_utils.pyside.ui.pages.base.base import Base as BasePage
9
+ from winipedia_utils.pyside.ui.widgets.media_player import MediaPlayer
10
+
11
+
12
+ class Player(BasePage):
13
+ """Player page for the VideoVault application."""
14
+
15
+ @final
16
+ def setup(self) -> None:
17
+ """Setup the UI."""
18
+ self.media_player = MediaPlayer(self.v_layout)
19
+
20
+ @final
21
+ def play_data(self, data: bytes, name: str) -> None:
22
+ """Play the video."""
23
+ # set current page to player
24
+ self.set_current_page(self.__class__)
25
+ # Stop current playback and clean up resources
26
+ self.media_player.play_data(data, name)
@@ -0,0 +1 @@
1
+ """__init__ module."""
@@ -0,0 +1,166 @@
1
+ """Browser module.
2
+
3
+ This module contains the browser class for the application.
4
+ """
5
+
6
+ from collections import defaultdict
7
+ from http.cookiejar import Cookie
8
+ from typing import Any
9
+
10
+ from PySide6.QtCore import QUrl
11
+ from PySide6.QtGui import QIcon
12
+ from PySide6.QtNetwork import QNetworkCookie
13
+ from PySide6.QtWebEngineWidgets import QWebEngineView
14
+ from PySide6.QtWidgets import (
15
+ QHBoxLayout,
16
+ QLayout,
17
+ QLineEdit,
18
+ QPushButton,
19
+ QSizePolicy,
20
+ QVBoxLayout,
21
+ QWidget,
22
+ )
23
+
24
+
25
+ class Browser(QWebEngineView):
26
+ """Browser class that creates a simple ready to use browser and not just a view."""
27
+
28
+ def __init__(self, parent_layout: QLayout, *args: Any, **kwargs: Any) -> None:
29
+ """Initialize the browser."""
30
+ super().__init__(*args, **kwargs)
31
+ self.parent_layout = parent_layout
32
+ self.make_widget()
33
+ self.connect_signals()
34
+ self.load_first_url()
35
+
36
+ def make_address_bar(self) -> None:
37
+ """Make the address bar."""
38
+ self.address_bar_layout = QHBoxLayout()
39
+
40
+ # Add back button
41
+ self.back_button = QPushButton()
42
+ self.back_button.setIcon(QIcon.fromTheme("go-previous"))
43
+ self.back_button.setToolTip("Go back")
44
+ self.back_button.clicked.connect(self.back)
45
+ self.address_bar_layout.addWidget(self.back_button)
46
+
47
+ # Add forward button
48
+ self.forward_button = QPushButton()
49
+ self.forward_button.setIcon(QIcon.fromTheme("go-next"))
50
+ self.forward_button.setToolTip("Go forward")
51
+ self.forward_button.clicked.connect(self.forward)
52
+ self.address_bar_layout.addWidget(self.forward_button)
53
+
54
+ # Add address bar
55
+ self.address_bar = QLineEdit()
56
+ self.address_bar.setPlaceholderText("Enter URL...")
57
+ self.address_bar.returnPressed.connect(self.navigate_to_url)
58
+ self.address_bar_layout.addWidget(self.address_bar)
59
+
60
+ # Add go button
61
+ self.go_button = QPushButton("Go")
62
+ self.go_button.clicked.connect(self.navigate_to_url)
63
+ self.address_bar_layout.addWidget(self.go_button)
64
+
65
+ self.browser_layout.addLayout(self.address_bar_layout)
66
+
67
+ def navigate_to_url(self) -> None:
68
+ """Navigate to the URL entered in the address bar."""
69
+ url = self.address_bar.text()
70
+ self.load(QUrl(url))
71
+
72
+ def make_widget(self) -> None:
73
+ """Make the widget."""
74
+ self.browser_widget = QWidget()
75
+ self.browser_layout = QVBoxLayout(self.browser_widget)
76
+ self.set_size_policy()
77
+ self.make_address_bar()
78
+ self.browser_layout.addWidget(self)
79
+ self.parent_layout.addWidget(self.browser_widget)
80
+
81
+ def set_size_policy(self) -> None:
82
+ """Set the size policy."""
83
+ self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
84
+
85
+ def connect_signals(self) -> None:
86
+ """Connect the signals."""
87
+ self.connect_load_finished_signal()
88
+ self.connect_on_cookie_added_signal()
89
+
90
+ def connect_load_finished_signal(self) -> None:
91
+ """Connect the load finished signal."""
92
+ self.loadFinished.connect(self.on_load_finished)
93
+
94
+ def on_load_finished(self, _ok: bool) -> None: # noqa: FBT001
95
+ """Handle the load finished signal."""
96
+ self.update_address_bar(self.url())
97
+
98
+ def update_address_bar(self, url: QUrl) -> None:
99
+ """Update the address bar with the current URL."""
100
+ self.address_bar.setText(url.toString())
101
+
102
+ def connect_on_cookie_added_signal(self) -> None:
103
+ """Connect the on cookie added signal."""
104
+ self.cookies: dict[str, list[QNetworkCookie]] = defaultdict(list)
105
+ self.page().profile().cookieStore().cookieAdded.connect(self.on_cookie_added)
106
+
107
+ def on_cookie_added(self, cookie: Any) -> None:
108
+ """Handle the on cookie added signal."""
109
+ self.cookies[cookie.domain()].append(cookie)
110
+
111
+ def load_first_url(self) -> None:
112
+ """Load the first URL."""
113
+ self.load(QUrl("https://www.google.com/"))
114
+
115
+ @property
116
+ def http_cookies(self) -> dict[str, list[Cookie]]:
117
+ """Get the http cookies for the given URL."""
118
+ return {
119
+ domain: self.qcookies_to_httpcookies(qcookies)
120
+ for domain, qcookies in self.cookies.items()
121
+ }
122
+
123
+ def qcookies_to_httpcookies(self, qcookies: list[QNetworkCookie]) -> list[Cookie]:
124
+ """Convert a list of QNetworkCookies to a CookieJar."""
125
+ return [self.qcookie_to_httpcookie(q_cookie) for q_cookie in qcookies]
126
+
127
+ def qcookie_to_httpcookie(self, qcookie: QNetworkCookie) -> Cookie:
128
+ """Convert a QNetworkCookie to a http.cookiejar.Cookie."""
129
+ name = bytes(qcookie.name().data()).decode()
130
+ value = bytes(qcookie.value().data()).decode()
131
+ domain = qcookie.domain()
132
+ path = qcookie.path() if qcookie.path() else "/"
133
+ secure = qcookie.isSecure()
134
+ expires = None
135
+ if qcookie.expirationDate().isValid():
136
+ expires = int(qcookie.expirationDate().toSecsSinceEpoch())
137
+ rest = {"HttpOnly": str(qcookie.isHttpOnly())}
138
+
139
+ return Cookie(
140
+ version=0,
141
+ name=name,
142
+ value=value,
143
+ port=None,
144
+ port_specified=False,
145
+ domain=domain,
146
+ domain_specified=bool(domain),
147
+ domain_initial_dot=domain.startswith("."),
148
+ path=path,
149
+ path_specified=bool(path),
150
+ secure=secure,
151
+ expires=expires or None,
152
+ discard=False,
153
+ comment=None,
154
+ comment_url=None,
155
+ rest=rest,
156
+ rfc2109=False,
157
+ )
158
+
159
+ def get_domain_cookies(self, domain: str) -> list[QNetworkCookie]:
160
+ """Get the cookies for the given domain."""
161
+ return self.cookies[domain]
162
+
163
+ def get_domain_http_cookies(self, domain: str) -> list[Cookie]:
164
+ """Get the http cookies for the given domain."""
165
+ cookies = self.get_domain_cookies(domain)
166
+ return self.qcookies_to_httpcookies(cookies)
@@ -0,0 +1,265 @@
1
+ """Media player module.
2
+
3
+ This module contains the media player class.
4
+ """
5
+
6
+ from functools import partial
7
+ from typing import Any
8
+
9
+ from PySide6.QtCore import QBuffer, QByteArray, Qt, Signal
10
+ from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer
11
+ from PySide6.QtMultimediaWidgets import QVideoWidget
12
+ from PySide6.QtWidgets import (
13
+ QHBoxLayout,
14
+ QLayout,
15
+ QMenu,
16
+ QPushButton,
17
+ QSizePolicy,
18
+ QSlider,
19
+ QVBoxLayout,
20
+ QWidget,
21
+ )
22
+
23
+ from winipedia_utils.pyside.ui.base.base import Base as BaseUI
24
+
25
+
26
+ class ClickableWidget(QWidget):
27
+ """Widget that can be clicked."""
28
+
29
+ clicked = Signal()
30
+
31
+ def mousePressEvent(self, event: Any) -> None: # noqa: N802
32
+ """Handle mouse press event."""
33
+ if event.button() == Qt.MouseButton.LeftButton:
34
+ self.clicked.emit()
35
+ super().mousePressEvent(event)
36
+
37
+
38
+ class ClickableVideoWidget(QVideoWidget):
39
+ """Video widget that can be clicked."""
40
+
41
+ clicked = Signal()
42
+
43
+ def mousePressEvent(self, event: Any) -> None: # noqa: N802
44
+ """Handle mouse press event."""
45
+ if event.button() == Qt.MouseButton.LeftButton:
46
+ self.clicked.emit()
47
+ super().mousePressEvent(event)
48
+
49
+
50
+ class MediaPlayer(QMediaPlayer):
51
+ """Media player class."""
52
+
53
+ def __init__(self, parent_layout: QLayout, *args: Any, **kwargs: Any) -> None:
54
+ """Initialize the media player."""
55
+ super().__init__(*args, **kwargs)
56
+ self.parent_layout = parent_layout
57
+ self.make_widget()
58
+
59
+ def make_widget(self) -> None:
60
+ """Make the widget."""
61
+ self.media_player_widget = QWidget()
62
+ self.media_player_layout = QVBoxLayout(self.media_player_widget)
63
+ self.parent_layout.addWidget(self.media_player_widget)
64
+ self.add_media_controls_above()
65
+ self.make_video_widget()
66
+ self.add_media_controls_below()
67
+
68
+ def make_video_widget(self) -> None:
69
+ """Make the video widget."""
70
+ self.video_widget = ClickableVideoWidget()
71
+ self.video_widget.clicked.connect(self.on_video_clicked)
72
+ self.video_widget.setSizePolicy(
73
+ QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
74
+ )
75
+ self.setVideoOutput(self.video_widget)
76
+
77
+ self.audio_output = QAudioOutput()
78
+ self.setAudioOutput(self.audio_output)
79
+
80
+ self.media_player_layout.addWidget(self.video_widget)
81
+
82
+ def on_video_clicked(self) -> None:
83
+ """Handle video widget click."""
84
+ if self.media_controls_widget_above.isVisible():
85
+ self.hide_media_controls()
86
+ return
87
+ self.show_media_controls()
88
+
89
+ def show_media_controls(self) -> None:
90
+ """Show media controls."""
91
+ self.media_controls_widget_above.show()
92
+ self.media_controls_widget_below.show()
93
+
94
+ def hide_media_controls(self) -> None:
95
+ """Hide media controls."""
96
+ self.media_controls_widget_above.hide()
97
+ self.media_controls_widget_below.hide()
98
+
99
+ def add_media_controls_above(self) -> None:
100
+ """Add media controls above the video."""
101
+ # main above widget
102
+ self.media_controls_widget_above = QWidget()
103
+ self.media_controls_layout_above = QHBoxLayout(self.media_controls_widget_above)
104
+ self.media_player_layout.addWidget(self.media_controls_widget_above)
105
+ # left contorls
106
+ self.left_controls_widget = QWidget()
107
+ self.left_controls_layout = QHBoxLayout(self.left_controls_widget)
108
+ self.media_controls_layout_above.addWidget(
109
+ self.left_controls_widget, alignment=Qt.AlignmentFlag.AlignLeft
110
+ )
111
+ # center contorls
112
+ self.center_controls_widget = QWidget()
113
+ self.center_controls_layout = QHBoxLayout(self.center_controls_widget)
114
+ self.media_controls_layout_above.addWidget(
115
+ self.center_controls_widget, alignment=Qt.AlignmentFlag.AlignCenter
116
+ )
117
+ self.right_controls_widget = QWidget()
118
+ self.right_controls_layout = QHBoxLayout(self.right_controls_widget)
119
+ self.media_controls_layout_above.addWidget(
120
+ self.right_controls_widget, alignment=Qt.AlignmentFlag.AlignRight
121
+ )
122
+
123
+ self.add_speed_control()
124
+ self.add_volume_control()
125
+ self.add_playback_control()
126
+ self.add_fullscreen_control()
127
+
128
+ def add_media_controls_below(self) -> None:
129
+ """Add media controls below the video."""
130
+ self.media_controls_widget_below = QWidget()
131
+ self.media_controls_layout_below = QHBoxLayout(self.media_controls_widget_below)
132
+ self.media_player_layout.addWidget(self.media_controls_widget_below)
133
+ self.add_progress_control()
134
+
135
+ def add_playback_control(self) -> None:
136
+ """Add playback control."""
137
+ self.play_icon = BaseUI.get_svg_icon("play_icon.svg")
138
+ self.pause_icon = BaseUI.get_svg_icon("pause_icon.svg")
139
+ # Pause symbol: ⏸ (U+23F8)
140
+ self.playback_button = QPushButton()
141
+ self.playback_button.setIcon(self.pause_icon)
142
+ self.playback_button.clicked.connect(self.toggle_playback)
143
+
144
+ self.center_controls_layout.addWidget(self.playback_button)
145
+
146
+ def toggle_playback(self) -> None:
147
+ """Toggle playback."""
148
+ if self.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
149
+ self.pause()
150
+ self.playback_button.setIcon(self.play_icon)
151
+ else:
152
+ self.play()
153
+ self.playback_button.setIcon(self.pause_icon)
154
+
155
+ def add_speed_control(self) -> None:
156
+ """Add speed control.
157
+
158
+ A button in the top left that on click shows a dropdown to select the speed.
159
+ """
160
+ self.default_speed = 1
161
+ self.speed_options = [0.2, 0.5, self.default_speed, 1.5, 2, 3, 4, 5]
162
+ self.speed_button = QPushButton(f"{self.default_speed}x")
163
+ self.speed_menu = QMenu(self.speed_button)
164
+ for speed in self.speed_options:
165
+ action = self.speed_menu.addAction(f"{speed}x")
166
+ action.triggered.connect(partial(self.change_speed, speed))
167
+
168
+ self.speed_button.setMenu(self.speed_menu)
169
+ self.left_controls_layout.addWidget(self.speed_button)
170
+
171
+ def change_speed(self, speed: float) -> None:
172
+ """Change playback speed."""
173
+ self.setPlaybackRate(speed)
174
+ self.speed_button.setText(f"{speed}x")
175
+
176
+ def add_volume_control(self) -> None:
177
+ """Add volume control."""
178
+ self.volume_slider = QSlider(Qt.Orientation.Horizontal)
179
+ self.volume_slider.setRange(0, 100)
180
+ self.volume_slider.valueChanged.connect(self.on_volume_changed)
181
+ self.left_controls_layout.addWidget(self.volume_slider)
182
+
183
+ def on_volume_changed(self, value: int) -> None:
184
+ """Handle volume slider value change."""
185
+ volume = value / 100.0 # Convert to 0.0-1.0 range
186
+ self.audio_output.setVolume(volume)
187
+
188
+ def add_fullscreen_control(self) -> None:
189
+ """Add fullscreen control."""
190
+ self.fullscreen_icon = BaseUI.get_svg_icon("fullscreen_icon.svg")
191
+ self.exit_fullscreen_icon = BaseUI.get_svg_icon("exit_fullscreen_icon.svg")
192
+ self.fullscreen_button = QPushButton()
193
+ self.fullscreen_button.setIcon(self.fullscreen_icon)
194
+
195
+ self.parent_widget = self.parent_layout.parentWidget()
196
+ self.other_visible_widgets = [
197
+ w
198
+ for w in set(self.parent_widget.findChildren(QWidget))
199
+ - {
200
+ self.media_player_widget,
201
+ *self.media_player_widget.findChildren(QWidget),
202
+ }
203
+ if w.isVisible() or not (w.isHidden() or w.isVisible())
204
+ ]
205
+ self.fullscreen_button.clicked.connect(self.toggle_fullscreen)
206
+
207
+ self.right_controls_layout.addWidget(self.fullscreen_button)
208
+
209
+ def toggle_fullscreen(self) -> None:
210
+ """Toggle fullscreen mode."""
211
+ # Get the main window
212
+ main_window = self.media_player_widget.window()
213
+ if main_window.isFullScreen():
214
+ for widget in self.other_visible_widgets:
215
+ widget.show()
216
+ # show the window in the previous size
217
+ main_window.showMaximized()
218
+ self.fullscreen_button.setIcon(self.fullscreen_icon)
219
+ else:
220
+ for widget in self.other_visible_widgets:
221
+ widget.hide()
222
+ main_window.showFullScreen()
223
+ self.fullscreen_button.setIcon(self.exit_fullscreen_icon)
224
+
225
+ def add_progress_control(self) -> None:
226
+ """Add progress control."""
227
+ self.progress_slider = QSlider(Qt.Orientation.Horizontal)
228
+ self.media_controls_layout_below.addWidget(self.progress_slider)
229
+
230
+ # Connect media player signals to update the progress slider
231
+ self.positionChanged.connect(self.update_position)
232
+ self.durationChanged.connect(self.set_duration)
233
+
234
+ # Connect slider signals to update video position
235
+ self.progress_slider.sliderMoved.connect(self.set_position)
236
+ self.progress_slider.sliderReleased.connect(self.slider_released)
237
+
238
+ def update_position(self, position: int) -> None:
239
+ """Update the progress slider position."""
240
+ # Only update if not being dragged to prevent jumps during manual sliding
241
+ if not self.progress_slider.isSliderDown():
242
+ self.progress_slider.setValue(position)
243
+
244
+ def set_duration(self, duration: int) -> None:
245
+ """Set the progress slider range based on media duration."""
246
+ self.progress_slider.setRange(0, duration)
247
+
248
+ def set_position(self, position: int) -> None:
249
+ """Set the media position when slider is moved."""
250
+ self.setPosition(position)
251
+
252
+ def slider_released(self) -> None:
253
+ """Handle slider release event."""
254
+ self.setPosition(self.progress_slider.value())
255
+
256
+ def play_data(self, data: bytes, name: str) -> None:
257
+ """Play the video."""
258
+ self.stop()
259
+ self.buffer = QBuffer()
260
+ self.buffer.setData(QByteArray(data))
261
+ self.buffer.open(QBuffer.OpenModeFlag.ReadOnly)
262
+
263
+ self.setSourceDevice(self.buffer, name)
264
+
265
+ super().play()
@@ -0,0 +1,56 @@
1
+ """Notification module.
2
+
3
+ This module contains functions to show notifications.
4
+ """
5
+
6
+ from pyqttoast import Toast, ToastIcon, ToastPosition # type: ignore[import-untyped]
7
+ from PySide6.QtWidgets import QApplication
8
+
9
+ from winipedia_utils.text.string import value_to_truncated_string
10
+
11
+ Toast.setPosition(ToastPosition.TOP_MIDDLE)
12
+
13
+
14
+ class Notification(Toast): # type: ignore[misc]
15
+ """Notification class."""
16
+
17
+ def __init__(
18
+ self,
19
+ title: str,
20
+ text: str,
21
+ icon: ToastIcon = ToastIcon.INFORMATION,
22
+ duration: int = 10000,
23
+ ) -> None:
24
+ """Initialize the notification.
25
+
26
+ The notification is shown in the top middle of the screen.
27
+
28
+ Args:
29
+ parent (QWidget): The parent widget.
30
+ title (str): The title of the notification.
31
+ text (str): The text of the notification.
32
+ icon (ToastIcon, optional): The icon of the notification.
33
+ duration (int, optional): The duration of the notification in milliseconds.
34
+ """
35
+ super().__init__(QApplication.activeWindow())
36
+ self.setDuration(duration)
37
+ self.setIcon(icon)
38
+ self.set_title(title)
39
+ self.set_text(text)
40
+
41
+ def set_title(self, title: str) -> None:
42
+ """Set the title of the notification."""
43
+ title = self.str_to_half_window_width(title)
44
+ self.setTitle(title)
45
+
46
+ def set_text(self, text: str) -> None:
47
+ """Set the text of the notification."""
48
+ text = self.str_to_half_window_width(text)
49
+ self.setText(text)
50
+
51
+ def str_to_half_window_width(self, string: str) -> str:
52
+ """Truncate the string to the width of the active window."""
53
+ main_window = QApplication.activeWindow()
54
+ width = main_window.width() / 2 if main_window is not None else 500
55
+ width = int(width)
56
+ return value_to_truncated_string(string, width)
@@ -0,0 +1 @@
1
+ """__init__ module."""
@@ -0,0 +1 @@
1
+ """__init__ module."""
@@ -0,0 +1,56 @@
1
+ """Base window module.
2
+
3
+ This module contains the base window class for the VideoVault application.
4
+ """
5
+
6
+ from abc import abstractmethod
7
+ from collections.abc import Generator
8
+ from types import ModuleType
9
+ from typing import final
10
+
11
+ from PySide6.QtWidgets import QMainWindow, QStackedWidget
12
+
13
+ from winipedia_utils.pyside.ui.base.base import Base as BaseUI
14
+ from winipedia_utils.pyside.ui.pages.base.base import Base as BasePage
15
+
16
+
17
+ class Base(BaseUI, QMainWindow):
18
+ """Base window class for the VideoVault application."""
19
+
20
+ @abstractmethod
21
+ def start_page_cls(self) -> type[BasePage]:
22
+ """Get the start page class."""
23
+
24
+ @abstractmethod
25
+ def get_pages_package(self) -> ModuleType:
26
+ """The package to get the page classes from."""
27
+
28
+ @final
29
+ def base_setup(self) -> None:
30
+ """Get the Qt object of the UI."""
31
+ self.setWindowTitle(self.get_display_name())
32
+
33
+ self.stack = QStackedWidget()
34
+ self.setCentralWidget(self.stack)
35
+
36
+ self.add_pages()
37
+
38
+ self.set_start_page()
39
+
40
+ @final
41
+ def add_pages(self) -> None:
42
+ """Add the pages to the window."""
43
+ self.pages = list(self.make_pages())
44
+ for page in self.pages:
45
+ self.stack.addWidget(page)
46
+
47
+ @final
48
+ def make_pages(self) -> Generator[BasePage, None, None]:
49
+ """Get the pages to add to the window."""
50
+ for page_cls in BasePage.get_subclasses(self.get_pages_package()):
51
+ yield page_cls()
52
+
53
+ @final
54
+ def set_start_page(self) -> None:
55
+ """Set the start page."""
56
+ self.set_current_page(self.start_page_cls())
@@ -0,0 +1 @@
1
+ """__init__ module."""
@@ -0,0 +1 @@
1
+ """__init__ module."""
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="black" width="24" height="24">
2
+ <path d="M12 3v12m0 0l-5-5m5 5l5-5M5 21h14" stroke="black" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
3
+ </svg>
@@ -0,0 +1,7 @@
1
+ <?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
2
+ <svg width="800px" height="800px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
3
+ <path d="M1 6L6 6L6 1L4.2 1L4.2 4.2L1 4.2L1 6Z" fill="#000000"/>
4
+ <path d="M15 10L10 10L10 15L11.8 15L11.8 11.8L15 11.8L15 10Z" fill="#000000"/>
5
+ <path d="M6 15L6 10L1 10L1 11.8L4.2 11.8L4.2 15L6 15Z" fill="#000000"/>
6
+ <path d="M10 1L10 6L15 6L15 4.2L11.8 4.2L11.8 1L10 1Z" fill="#000000"/>
7
+ </svg>
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
2
+ <svg width="800px" height="800px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
3
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M10 15H15V10H13.2V13.2H10V15ZM6 15V13.2H2.8V10H1V15H6ZM10 2.8H12.375H13.2V6H15V1H10V2.8ZM6 1V2.8H2.8V6H1V1H6Z" fill="#000000"/>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
2
+ <rect x="4" y="4" width="6" height="16"/>
3
+ <rect x="14" y="4" width="6" height="16"/>
4
+ </svg>
@@ -0,0 +1,8 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 330 330" width="24" height="24">
2
+ <path d="M37.728,328.12c2.266,1.256,4.77,1.88,7.272,1.88
3
+ c2.763,0,5.522-0.763,7.95-2.28l240-149.999
4
+ c4.386-2.741,7.05-7.548,7.05-12.72c0-5.172-2.664-9.979-7.05-12.72
5
+ L52.95,2.28c-4.625-2.891-10.453-3.043-15.222-0.4
6
+ C32.959,4.524,30,9.547,30,15v300
7
+ C30,320.453,32.959,325.476,37.728,328.12z"/>
8
+ </svg>
@@ -0,0 +1,11 @@
1
+ """utils for svgs."""
2
+
3
+ from pathlib import Path
4
+
5
+ from winipedia_utils.modules.module import to_path
6
+ from winipedia_utils.resources import svgs
7
+
8
+
9
+ def get_svg_path(svg_name: str) -> Path:
10
+ """Get the path to a svg."""
11
+ return to_path(svgs, is_package=True) / svg_name
@@ -0,0 +1 @@
1
+ """__init__ module for winipedia_utils.security."""
@@ -0,0 +1,30 @@
1
+ """Keyring utilities for secure storage and retrieval of secrets.
2
+
3
+ This module provides utility functions for working with keyring,
4
+ including getting and creating secrets and fernets.
5
+ These utilities help with secure storage and retrieval of secrets.
6
+ """
7
+
8
+ import keyring
9
+ from cryptography.fernet import Fernet
10
+
11
+
12
+ def get_or_create_secret(service_name: str, username: str) -> str:
13
+ """Get the app secret using keyring.
14
+
15
+ If it does not exist, create it with a Fernet.
16
+ """
17
+ secret = keyring.get_password(service_name, username)
18
+ if secret is None:
19
+ secret = Fernet.generate_key().decode()
20
+ keyring.set_password(service_name, username, secret)
21
+ return secret
22
+
23
+
24
+ def get_or_create_fernet(service_name: str, username: str) -> Fernet:
25
+ """Get the app fernet using keyring.
26
+
27
+ If it does not exist, create it with a Fernet.
28
+ """
29
+ secret = get_or_create_secret(service_name, username)
30
+ return Fernet(secret.encode())
@@ -1,18 +1,22 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: winipedia-utils
3
- Version: 0.1.16
3
+ Version: 0.1.18
4
4
  Summary: A package with many utility functions
5
5
  License: MIT
6
6
  Author: Winipedia
7
7
  Author-email: win.steveker@gmx.de
8
- Requires-Python: >=3.12
8
+ Requires-Python: >=3.12,<3.14
9
9
  Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Programming Language :: Python :: 3
11
11
  Classifier: Programming Language :: Python :: 3.12
12
12
  Classifier: Programming Language :: Python :: 3.13
13
+ Requires-Dist: cryptography (>=45.0.5,<46.0.0)
13
14
  Requires-Dist: defusedxml (>=0.7.1,<0.8.0)
14
15
  Requires-Dist: django (>=5.2.1,<6.0.0)
16
+ Requires-Dist: keyring (>=25.6.0,<26.0.0)
15
17
  Requires-Dist: pathspec (>=0.12.1,<0.13.0)
18
+ Requires-Dist: pyqt-toast-notification (>=1.3.3,<2.0.0)
19
+ Requires-Dist: pyside6 (>=6.9.1,<7.0.0)
16
20
  Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
17
21
  Requires-Dist: setuptools (>=80.3.1,<81.0.0)
18
22
  Requires-Dist: tomlkit (>=0.13.2,<0.14.0)
@@ -26,7 +26,7 @@ winipedia_utils/logging/logger.py,sha256=tCcUwAVLVr3Bec7_i-SoKvBPNCJSEmyuvBW2gbq
26
26
  winipedia_utils/modules/__init__.py,sha256=e3CFaC3FhK4ibknFOv1bqOZxA7XeVwmLqWX7oajUm78,51
27
27
  winipedia_utils/modules/class_.py,sha256=RDjal6QYYs9z81DT1mIFrnEhQ9vgN2tCAYN1WWnN24Y,2462
28
28
  winipedia_utils/modules/function.py,sha256=cHLGiD7huPdfwUDdI0IlbB_Pd4woWGgbyvrsjivHBpc,2987
29
- winipedia_utils/modules/module.py,sha256=mxaAsRl02CAq_bTW2HsmzRWoyC9jKNM8Q4xdgdQlgd0,12719
29
+ winipedia_utils/modules/module.py,sha256=RAxEp_yLc7KyHke05ZmTkUCfKof_l4k6mRtLP4xBgL4,12884
30
30
  winipedia_utils/modules/package.py,sha256=kCm4pXQdllafo-2dmWZTvaAqRruzh3iF4hseHlCmTlU,12605
31
31
  winipedia_utils/oop/__init__.py,sha256=wGjsVwLbTVEQWOfDJvN9nlvC-3NmAi8Doc2xIrm6e78,47
32
32
  winipedia_utils/oop/mixins/__init__.py,sha256=PDK-cJcdRUfDUCz36qQ5pmMW07G133WtN49OpmILGNI,54
@@ -40,6 +40,32 @@ winipedia_utils/projects/poetry/config.py,sha256=ghJzlNDVrSfG5Mg0JjnL9Xmz6fVD-rA
40
40
  winipedia_utils/projects/poetry/poetry.py,sha256=5jyUSMxhCZ7pz9bOaz5E9r7Da9qIrGOp6wcBzI1y7Cg,932
41
41
  winipedia_utils/projects/project.py,sha256=2nz1Hh51A-shjgdPCgiDw-ODrVtOtiHEHQnMPjAJZ-A,1587
42
42
  winipedia_utils/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
43
+ winipedia_utils/pyside/__init__.py,sha256=knliQxknKWqxCEfxlI1K02OtbBfnTkYUZFqyYcLJNDI,52
44
+ winipedia_utils/pyside/ui/__init__.py,sha256=h5NJ6WyveMMqxIgt0x65_mqndtncxgNmx5816KD3ubU,55
45
+ winipedia_utils/pyside/ui/base/__init__.py,sha256=p-maJQh7gYbXwhkqKU4wL6UNGzRAy988JP8_qLoTZDk,24
46
+ winipedia_utils/pyside/ui/base/base.py,sha256=jOdOtKSjD5eEk8NGYwrRJfyt1UHFmDzkaWNKdqeuo4g,3525
47
+ winipedia_utils/pyside/ui/pages/__init__.py,sha256=p-maJQh7gYbXwhkqKU4wL6UNGzRAy988JP8_qLoTZDk,24
48
+ winipedia_utils/pyside/ui/pages/base/__init__.py,sha256=p-maJQh7gYbXwhkqKU4wL6UNGzRAy988JP8_qLoTZDk,24
49
+ winipedia_utils/pyside/ui/pages/base/base.py,sha256=heFDJswfzSepX7ib5NBpNv6Egm5hdPfRa-StcI_yO-k,2323
50
+ winipedia_utils/pyside/ui/pages/browser.py,sha256=SzX356snnmEdzCrtIXl5mPaUkM1-_REfUurOotpVIkY,696
51
+ winipedia_utils/pyside/ui/pages/player.py,sha256=SETJeeCm_m_LmnSTWMBrOuG4Gt_1kqVGzVgMgTxLJDQ,787
52
+ winipedia_utils/pyside/ui/widgets/__init__.py,sha256=p-maJQh7gYbXwhkqKU4wL6UNGzRAy988JP8_qLoTZDk,24
53
+ winipedia_utils/pyside/ui/widgets/browser.py,sha256=5oUyQKYOYu9Zzx6adbewTtAIfnn_RVs8CLoX3rU2g28,6160
54
+ winipedia_utils/pyside/ui/widgets/media_player.py,sha256=13sUNKQKm-JKPn_yl2cdnBzV14J0beEBNeGnU6brpVQ,10304
55
+ winipedia_utils/pyside/ui/widgets/notification.py,sha256=_zBq9Q6SDFu3fV2AkbvyCNQmOEOy1l9mKOFAAdyteg4,1941
56
+ winipedia_utils/pyside/ui/windows/__init__.py,sha256=p-maJQh7gYbXwhkqKU4wL6UNGzRAy988JP8_qLoTZDk,24
57
+ winipedia_utils/pyside/ui/windows/base/__init__.py,sha256=p-maJQh7gYbXwhkqKU4wL6UNGzRAy988JP8_qLoTZDk,24
58
+ winipedia_utils/pyside/ui/windows/base/base.py,sha256=FOZxn_IRa7a_UkwERhjraCs8vxHD8n7s2gKqCHrpzms,1653
59
+ winipedia_utils/resources/__init__.py,sha256=p-maJQh7gYbXwhkqKU4wL6UNGzRAy988JP8_qLoTZDk,24
60
+ winipedia_utils/resources/svgs/__init__.py,sha256=p-maJQh7gYbXwhkqKU4wL6UNGzRAy988JP8_qLoTZDk,24
61
+ winipedia_utils/resources/svgs/download_arrow.svg,sha256=R5WNz0JOxkRi5LOuDtc_eZESZN4W0JAuvr-5_ZRqeks,244
62
+ winipedia_utils/resources/svgs/exit_fullscreen_icon.svg,sha256=4wpP-pWg7z4c62c9MGUcU-rWcRVUEBSl1fGiJ1HDR3g,528
63
+ winipedia_utils/resources/svgs/fullscreen_icon.svg,sha256=nN4Y5CA7AuwOvyhgtJWZtBT5uSzO7NFJivVRFNqooe0,408
64
+ winipedia_utils/resources/svgs/pause_icon.svg,sha256=mNrEoAOhbvxsJmBg4xZ08kdahAG6gA4LqmHXYNOOKB0,182
65
+ winipedia_utils/resources/svgs/play_icon.svg,sha256=U0RQ-S_WbcyyjeWC55BkcOtG64GfXALXlnB683foZUY,416
66
+ winipedia_utils/resources/svgs/svg.py,sha256=Umf_dGLrGJJXUOfS-87mr1Paqj4v9dMkSqvgu_-UAr4,283
67
+ winipedia_utils/security/__init__.py,sha256=ZBa72J6MNtYumBFMoVc0ia4jsoS7oNgjaTCW0xDb6EI,53
68
+ winipedia_utils/security/keyring.py,sha256=5-5B7D5Ck50id5SOBdFMANbzBzIKnujajY8e7cr1iLo,984
43
69
  winipedia_utils/setup.py,sha256=F4NneO0wVTf7JCXLorWjTOdJl36N5fLSksoWMe4p86o,1650
44
70
  winipedia_utils/testing/__init__.py,sha256=kXhB5xw02ec5xpcW_KV--9CBKdyCjnuR-NZzAJ5tq0g,51
45
71
  winipedia_utils/testing/assertions.py,sha256=0JF4mqVTnLQ1qkAL_FuTwyN_idr00rvVlta7aDdnUXA,851
@@ -61,7 +87,7 @@ winipedia_utils/testing/tests/base/utils/utils.py,sha256=dUPDrgAxlfREQb33zz23Mfz
61
87
  winipedia_utils/testing/tests/conftest.py,sha256=8RounBlI8Jq1aLaLNpv84MW4ne8Qq0aavQextDOp5ng,920
62
88
  winipedia_utils/text/__init__.py,sha256=j2bwtK6kyeHI6SnoBjpRju0C1W2n2paXBDlNjNtaUxA,48
63
89
  winipedia_utils/text/string.py,sha256=1jbBftlgxffGgSlPnQh3aRPIr8XekEwpSenjFCW6JyM,3478
64
- winipedia_utils-0.1.16.dist-info/LICENSE,sha256=3PrKJ2CWNrnyyHaC_r0wPDSukVWgmjOxHr__eQVH7cw,1087
65
- winipedia_utils-0.1.16.dist-info/METADATA,sha256=BUZaofcjM2ONxS4qKN3TRABjtVoB2w5sFeOrENDMlN4,12385
66
- winipedia_utils-0.1.16.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
67
- winipedia_utils-0.1.16.dist-info/RECORD,,
90
+ winipedia_utils-0.1.18.dist-info/LICENSE,sha256=3PrKJ2CWNrnyyHaC_r0wPDSukVWgmjOxHr__eQVH7cw,1087
91
+ winipedia_utils-0.1.18.dist-info/METADATA,sha256=UoZkYlff6rCkgcdTzcLqSY0w633C1L-UcoXImnpV2O4,12576
92
+ winipedia_utils-0.1.18.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
93
+ winipedia_utils-0.1.18.dist-info/RECORD,,