ppui 2026.1.20.2__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.
- ppui/__init__.py +27 -0
- ppui/base.py +47 -0
- ppui/core.py +316 -0
- ppui-2026.1.20.2.dist-info/METADATA +62 -0
- ppui-2026.1.20.2.dist-info/RECORD +6 -0
- ppui-2026.1.20.2.dist-info/WHEEL +4 -0
ppui/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from .core import (
|
|
2
|
+
console,
|
|
3
|
+
prompt_yes_no,
|
|
4
|
+
copy_to_clipboard,
|
|
5
|
+
prompt_toolkit_menu,
|
|
6
|
+
format_menu_choices,
|
|
7
|
+
Menu
|
|
8
|
+
)
|
|
9
|
+
from .base import (
|
|
10
|
+
UIElement,
|
|
11
|
+
Selection,
|
|
12
|
+
Option,
|
|
13
|
+
Presentable
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"console",
|
|
18
|
+
"prompt_yes_no",
|
|
19
|
+
"copy_to_clipboard",
|
|
20
|
+
"prompt_toolkit_menu",
|
|
21
|
+
"format_menu_choices",
|
|
22
|
+
"Menu",
|
|
23
|
+
"UIElement",
|
|
24
|
+
"Selection",
|
|
25
|
+
"Option",
|
|
26
|
+
"Presentable"
|
|
27
|
+
]
|
ppui/base.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base abstractions for the PPUI (Python Process User Intent) system.
|
|
3
|
+
"""
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Any, List, Optional, Callable, Dict, Union
|
|
6
|
+
|
|
7
|
+
class Presentable(ABC):
|
|
8
|
+
"""Base class for anything that can be presented to a user (TUI, Web, Voice, etc.)"""
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def present(self) -> Any:
|
|
11
|
+
"""Present the element to the user and return the result if applicable."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
class UIElement(Presentable):
|
|
15
|
+
"""A generic UI element."""
|
|
16
|
+
def __init__(self, title: str = "", style: str = ""):
|
|
17
|
+
self.title = title
|
|
18
|
+
self.style = style
|
|
19
|
+
|
|
20
|
+
class Option:
|
|
21
|
+
"""A single choice within a Selection."""
|
|
22
|
+
def __init__(self, label: str, value: Any = None, callback: Optional[Callable] = None, description: str = ""):
|
|
23
|
+
self.label = label
|
|
24
|
+
self.value = value if value is not None else label
|
|
25
|
+
self.callback = callback
|
|
26
|
+
self.description = description
|
|
27
|
+
|
|
28
|
+
class Selection(UIElement):
|
|
29
|
+
"""Abstract concept of providing options for a user to choose from."""
|
|
30
|
+
def __init__(self, title: str = "", style: str = ""):
|
|
31
|
+
super().__init__(title, style)
|
|
32
|
+
self.options: List[Union[Option, 'Selection']] = []
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def add_option(self, label: str, value_or_callback: Any = None, description: str = ""):
|
|
36
|
+
"""Add a single option to the selection."""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def add_submenu(self, label: str, submenu: 'Selection', behavior: str = "push"):
|
|
41
|
+
"""Add a nested selection (submenu). behavior can be 'push' or 'inline'."""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
@abstractmethod
|
|
45
|
+
def run(self, loop: bool = True) -> Any:
|
|
46
|
+
"""Start the interaction loop."""
|
|
47
|
+
pass
|
ppui/core.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
import uuid
|
|
5
|
+
from typing import Optional, Dict, Any, List, Callable, Union
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from .base import Selection, Option, UIElement
|
|
10
|
+
|
|
11
|
+
# Shared console instance for all Rich output
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
class Menu(Selection):
|
|
15
|
+
"""
|
|
16
|
+
TUI implementation of the abstract Selection concept.
|
|
17
|
+
Encapsulates state management, boilerplate loops, and UI logic.
|
|
18
|
+
"""
|
|
19
|
+
def __init__(self, title: str, style: str = "bold blue"):
|
|
20
|
+
super().__init__(title, style)
|
|
21
|
+
self.callbacks: Dict[str, Callable] = {}
|
|
22
|
+
self._items: List[Dict[str, Any]] = []
|
|
23
|
+
|
|
24
|
+
def add_option(self, label: str, value_or_callback: Any = None, description: str = ""):
|
|
25
|
+
"""
|
|
26
|
+
Adds a menu option.
|
|
27
|
+
If a callable is passed, it's stored as a callback and executed automatically in run().
|
|
28
|
+
If a string/value is passed, run() returns it upon selection.
|
|
29
|
+
"""
|
|
30
|
+
if callable(value_or_callback):
|
|
31
|
+
action_id = f"cb_{uuid.uuid4().hex[:8]}"
|
|
32
|
+
self.callbacks[action_id] = value_or_callback
|
|
33
|
+
self._items.append({"title": label, "value": action_id, "description": description})
|
|
34
|
+
else:
|
|
35
|
+
self._items.append({"title": label, "value": value_or_callback, "description": description})
|
|
36
|
+
|
|
37
|
+
def add_item(self, label: str, action_or_callback: Any):
|
|
38
|
+
"""Backward compatibility alias for add_option."""
|
|
39
|
+
self.add_option(label, action_or_callback)
|
|
40
|
+
|
|
41
|
+
def add_submenu(self, label: str, submenu: Selection, behavior: str = "push"):
|
|
42
|
+
"""
|
|
43
|
+
Adds a nested selection.
|
|
44
|
+
'push': Standard drill-down (opens a new menu).
|
|
45
|
+
'inline': Flattens submenu options into the current menu with indentation.
|
|
46
|
+
"""
|
|
47
|
+
if behavior == "push":
|
|
48
|
+
self.add_option(label, lambda: submenu.run())
|
|
49
|
+
elif behavior == "inline":
|
|
50
|
+
# For inline, we store a reference to the submenu object
|
|
51
|
+
# and handle it during formatting in the run loop.
|
|
52
|
+
self._items.append({
|
|
53
|
+
"title": label,
|
|
54
|
+
"value": submenu,
|
|
55
|
+
"behavior": "inline",
|
|
56
|
+
"is_submenu": True,
|
|
57
|
+
"expanded": False
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
def add_back_item(self, label: str = "[Back]"):
|
|
61
|
+
"""Convenience method to add a standard Back option."""
|
|
62
|
+
self.add_option(label, "back")
|
|
63
|
+
|
|
64
|
+
def add_quit_item(self, label: str = "[Quit]"):
|
|
65
|
+
"""Convenience method to add a standard Quit option."""
|
|
66
|
+
self.add_option(label, "quit")
|
|
67
|
+
|
|
68
|
+
def _get_flattened_items(self) -> List[Dict[str, Any]]:
|
|
69
|
+
"""Flatten inline submenus if they are expanded."""
|
|
70
|
+
flat_list = []
|
|
71
|
+
for item in self._items:
|
|
72
|
+
flat_list.append(item)
|
|
73
|
+
if item.get("is_submenu") and item.get("behavior") == "inline" and item.get("expanded"):
|
|
74
|
+
submenu = item["value"]
|
|
75
|
+
# Recursively get items from submenu
|
|
76
|
+
sub_items = submenu._get_flattened_items() if isinstance(submenu, Menu) else []
|
|
77
|
+
for sub_item in sub_items:
|
|
78
|
+
indented_item = sub_item.copy()
|
|
79
|
+
indented_item["title"] = f" {sub_item['title']}"
|
|
80
|
+
flat_list.append(indented_item)
|
|
81
|
+
return flat_list
|
|
82
|
+
|
|
83
|
+
def _get_callback(self, action_id: str) -> Optional[Callable]:
|
|
84
|
+
"""Resolve a callback ID, searching through inline submenus if necessary."""
|
|
85
|
+
if action_id in self.callbacks:
|
|
86
|
+
return self.callbacks[action_id]
|
|
87
|
+
|
|
88
|
+
# Search in inline submenus
|
|
89
|
+
for item in self._items:
|
|
90
|
+
if item.get("is_submenu") and item.get("behavior") == "inline":
|
|
91
|
+
submenu = item["value"]
|
|
92
|
+
if isinstance(submenu, Menu):
|
|
93
|
+
cb = submenu._get_callback(action_id)
|
|
94
|
+
if cb:
|
|
95
|
+
return cb
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
def run(self, loop: bool = True) -> Any:
|
|
99
|
+
"""
|
|
100
|
+
Starts the interactive menu interaction.
|
|
101
|
+
Returns the selected value (or 'quit'/'back' for standard items).
|
|
102
|
+
"""
|
|
103
|
+
while True:
|
|
104
|
+
console.print(Panel(self.title, style=self.style))
|
|
105
|
+
|
|
106
|
+
# Flatten items for display (handles inline expansion)
|
|
107
|
+
display_items = self._get_flattened_items()
|
|
108
|
+
|
|
109
|
+
choices = format_menu_choices(display_items, title_field="title", value_field="value")
|
|
110
|
+
selection = prompt_toolkit_menu(choices)
|
|
111
|
+
|
|
112
|
+
if selection is None: # Esc or Ctrl-C
|
|
113
|
+
return "quit"
|
|
114
|
+
|
|
115
|
+
# Handle inline submenu toggle
|
|
116
|
+
if isinstance(selection, Menu):
|
|
117
|
+
for item in self._items:
|
|
118
|
+
if item.get("value") == selection:
|
|
119
|
+
item["expanded"] = not item.get("expanded")
|
|
120
|
+
break
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
# Execute callback if it exists (resolving via hierarchy)
|
|
124
|
+
callback = self._get_callback(str(selection))
|
|
125
|
+
if callback:
|
|
126
|
+
res = callback()
|
|
127
|
+
if not loop:
|
|
128
|
+
return selection # Return the ID that triggered the callback
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
# Standard navigation handling
|
|
132
|
+
if selection in ["quit", "exit"]:
|
|
133
|
+
return "quit"
|
|
134
|
+
if selection == "back":
|
|
135
|
+
return "back"
|
|
136
|
+
|
|
137
|
+
if not loop:
|
|
138
|
+
return selection
|
|
139
|
+
|
|
140
|
+
def present(self) -> Any:
|
|
141
|
+
"""Presentable implementation."""
|
|
142
|
+
return self.run()
|
|
143
|
+
|
|
144
|
+
def prompt_yes_no(message: str, default: bool = False) -> bool:
|
|
145
|
+
"""Standardize yes/no confirmation prompts."""
|
|
146
|
+
suffix = "[Y/n]" if default else "[y/N]"
|
|
147
|
+
while True:
|
|
148
|
+
choice = input(f"{message} {suffix}: ").strip().lower()
|
|
149
|
+
if choice == "":
|
|
150
|
+
return default
|
|
151
|
+
if choice in {"y", "yes"}:
|
|
152
|
+
return True
|
|
153
|
+
if choice in {"n", "no"}:
|
|
154
|
+
return False
|
|
155
|
+
console.print("[yellow]Please enter y or n.[/yellow]")
|
|
156
|
+
|
|
157
|
+
def copy_to_clipboard(text: str) -> bool:
|
|
158
|
+
"""Copy text to the system clipboard across different platforms."""
|
|
159
|
+
try:
|
|
160
|
+
if shutil.which("pbcopy"):
|
|
161
|
+
subprocess.run(["pbcopy"], input=text, text=True, check=True)
|
|
162
|
+
return True
|
|
163
|
+
elif shutil.which("xclip"):
|
|
164
|
+
subprocess.run(["xclip", "-selection", "clipboard"], input=text, text=True, check=True)
|
|
165
|
+
return True
|
|
166
|
+
elif shutil.which("xsel"):
|
|
167
|
+
subprocess.run(["xsel", "--clipboard", "--input"], input=text, text=True, check=True)
|
|
168
|
+
return True
|
|
169
|
+
except Exception:
|
|
170
|
+
pass
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
def prompt_toolkit_menu(choices, style=None, hotkeys=None, default_idx=0):
|
|
174
|
+
"""Interactive selection menu supporting arrow keys and immediate hotkeys."""
|
|
175
|
+
from prompt_toolkit.key_binding import KeyBindings
|
|
176
|
+
from prompt_toolkit.application import Application
|
|
177
|
+
from prompt_toolkit.layout import Layout
|
|
178
|
+
from prompt_toolkit.layout.containers import Window, HSplit
|
|
179
|
+
from prompt_toolkit.layout.controls import FormattedTextControl
|
|
180
|
+
|
|
181
|
+
idx = max(0, min(default_idx, len(choices) - 1)) if choices else 0
|
|
182
|
+
kb = KeyBindings()
|
|
183
|
+
|
|
184
|
+
@kb.add('up')
|
|
185
|
+
def _(event):
|
|
186
|
+
nonlocal idx
|
|
187
|
+
idx = (idx - 1) % len(choices)
|
|
188
|
+
|
|
189
|
+
@kb.add('down')
|
|
190
|
+
def _(event):
|
|
191
|
+
nonlocal idx
|
|
192
|
+
idx = (idx + 1) % len(choices)
|
|
193
|
+
|
|
194
|
+
@kb.add('enter')
|
|
195
|
+
def _(event):
|
|
196
|
+
event.app.exit(result=choices[idx].value)
|
|
197
|
+
|
|
198
|
+
@kb.add('escape')
|
|
199
|
+
@kb.add('c-c')
|
|
200
|
+
def _(event):
|
|
201
|
+
event.app.exit(result=None)
|
|
202
|
+
|
|
203
|
+
def make_hotkey_handler(h_val):
|
|
204
|
+
keys = list(str(h_val))
|
|
205
|
+
@kb.add(*keys)
|
|
206
|
+
def _(event):
|
|
207
|
+
for choice in choices:
|
|
208
|
+
clean_title = choice.title.strip().lower()
|
|
209
|
+
title_prefix = clean_title.split('.')[0].strip().lstrip('0')
|
|
210
|
+
h_prefix = str(h_val).lstrip('0')
|
|
211
|
+
|
|
212
|
+
if clean_title.startswith(f"{h_val}."):
|
|
213
|
+
event.app.exit(result=choice.value)
|
|
214
|
+
return
|
|
215
|
+
if title_prefix == h_prefix and title_prefix != "":
|
|
216
|
+
event.app.exit(result=choice.value)
|
|
217
|
+
return
|
|
218
|
+
if str(choice.value) == str(h_val):
|
|
219
|
+
event.app.exit(result=choice.value)
|
|
220
|
+
return
|
|
221
|
+
return _
|
|
222
|
+
|
|
223
|
+
if not hotkeys:
|
|
224
|
+
hotkeys = []
|
|
225
|
+
for choice in choices:
|
|
226
|
+
title = choice.title.strip()
|
|
227
|
+
if '.' in title:
|
|
228
|
+
prefix = title.split('.')[0].strip().lower()
|
|
229
|
+
if prefix:
|
|
230
|
+
hotkeys.append(prefix)
|
|
231
|
+
if prefix.isdigit() and prefix.startswith('0') and prefix != '0':
|
|
232
|
+
hotkeys.append(prefix.lstrip('0'))
|
|
233
|
+
if not hotkeys:
|
|
234
|
+
hotkeys = [str(i) for i in range(1, 10)] + ['q', 'b']
|
|
235
|
+
|
|
236
|
+
seen = set()
|
|
237
|
+
unique_hotkeys = []
|
|
238
|
+
for h in hotkeys:
|
|
239
|
+
if h not in seen:
|
|
240
|
+
unique_hotkeys.append(h)
|
|
241
|
+
seen.add(h)
|
|
242
|
+
|
|
243
|
+
for h in unique_hotkeys:
|
|
244
|
+
make_hotkey_handler(h)
|
|
245
|
+
|
|
246
|
+
def get_text():
|
|
247
|
+
result = []
|
|
248
|
+
for i, choice in enumerate(choices):
|
|
249
|
+
if i == idx:
|
|
250
|
+
result.append(('class:selected', f" » {choice.title}\n"))
|
|
251
|
+
else:
|
|
252
|
+
result.append(('', f" {choice.title}\n"))
|
|
253
|
+
return result
|
|
254
|
+
|
|
255
|
+
layout = Layout(HSplit([
|
|
256
|
+
Window(content=FormattedTextControl(get_text)),
|
|
257
|
+
]))
|
|
258
|
+
|
|
259
|
+
from prompt_toolkit.styles import Style
|
|
260
|
+
if not style:
|
|
261
|
+
style = Style([('selected', 'fg:#cc9900')])
|
|
262
|
+
|
|
263
|
+
app = Application(layout=layout, key_bindings=kb, style=style, full_screen=False)
|
|
264
|
+
return app.run()
|
|
265
|
+
|
|
266
|
+
def format_menu_choices(items: List[Any], title_field: Optional[str] = None, value_field: Optional[str] = None) -> List[Any]:
|
|
267
|
+
"""Prepare a list of items for `prompt_toolkit_menu` by adding index numbers and hotkeys."""
|
|
268
|
+
import questionary
|
|
269
|
+
|
|
270
|
+
special_keywords = {
|
|
271
|
+
"quit": "q", "[quit]": "q", "(q) [quit]": "q", "q": "q",
|
|
272
|
+
"back": "b", "[back]": "b", "(b) [back]": "b", "b": "b",
|
|
273
|
+
"menu": "m", "[menu]": "m", "return to menu": "m"
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
regular_items = []
|
|
277
|
+
special_items = []
|
|
278
|
+
|
|
279
|
+
for item in items:
|
|
280
|
+
title = ""
|
|
281
|
+
if isinstance(item, dict):
|
|
282
|
+
if title_field: title = str(item.get(title_field))
|
|
283
|
+
elif "title" in item: title = str(item["title"])
|
|
284
|
+
else: title = str(item)
|
|
285
|
+
else:
|
|
286
|
+
title = str(item)
|
|
287
|
+
|
|
288
|
+
lower_title = title.strip().lower()
|
|
289
|
+
if lower_title in special_keywords:
|
|
290
|
+
special_items.append((item, special_keywords[lower_title]))
|
|
291
|
+
else:
|
|
292
|
+
regular_items.append(item)
|
|
293
|
+
|
|
294
|
+
pad = len(str(len(regular_items)))
|
|
295
|
+
choices = []
|
|
296
|
+
|
|
297
|
+
for i, item in enumerate(regular_items, 1):
|
|
298
|
+
idx_str = str(i).zfill(pad)
|
|
299
|
+
if isinstance(item, dict):
|
|
300
|
+
title = item.get(title_field) if title_field else item.get("title", str(item))
|
|
301
|
+
value = item.get(value_field) if value_field else item.get("value", item)
|
|
302
|
+
else:
|
|
303
|
+
title = str(item)
|
|
304
|
+
value = item
|
|
305
|
+
choices.append(questionary.Choice(f"{idx_str}. {title}", value=value))
|
|
306
|
+
|
|
307
|
+
for item, key in special_items:
|
|
308
|
+
if isinstance(item, dict):
|
|
309
|
+
title = item.get(title_field) if title_field else item.get("title", str(item))
|
|
310
|
+
value = item.get(value_field) if value_field else item.get("value", item)
|
|
311
|
+
else:
|
|
312
|
+
title = str(item)
|
|
313
|
+
value = item
|
|
314
|
+
choices.append(questionary.Choice(f"{' ' * (pad - 1)}{key}. {title}", value=value))
|
|
315
|
+
|
|
316
|
+
return choices
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ppui
|
|
3
|
+
Version: 2026.1.20.2
|
|
4
|
+
Summary: Python Process User Intent: A high-level abstract UI system (TUI, Web, Voice)
|
|
5
|
+
Project-URL: Homepage, https://github.com/nextoken/pas
|
|
6
|
+
Author-email: Next Token <nextoken@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Requires-Dist: prompt-toolkit>=3.0.0
|
|
10
|
+
Requires-Dist: questionary>=2.0.0
|
|
11
|
+
Requires-Dist: rich>=13.0.0
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# PPUI: Python Process User Intent 🎭
|
|
15
|
+
|
|
16
|
+
A high-level abstract UI system for Python scripts.
|
|
17
|
+
|
|
18
|
+
PPUI (pronounced "pui") is the Python implementation of the **PUI** (**Process User Intent**) philosophy.
|
|
19
|
+
|
|
20
|
+
The goal isn't just to "draw a menu"—it's to capture what the user wants to do next. Whether that intent is captured via a terminal hotkey, a web click, or a voice command, the script's logic remains the same.
|
|
21
|
+
|
|
22
|
+
*PPUI: Because your scripts should care about what you want, not just how you say it.*
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
- **High-level Abstractions**: Use concepts like `Selection` and `Input` without caring about the rendering engine.
|
|
26
|
+
- **TUI Implementation**: Professional, hotkey-driven terminal menus.
|
|
27
|
+
- **Submenu Support**: Nested selections with `push` (drill-down) or `inline` (expandable) behaviors.
|
|
28
|
+
- **Rich Integration**: Full support for Rich-formatted output and panels.
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install ppui
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
### High-Level Menu Class
|
|
39
|
+
|
|
40
|
+
The `Menu` class provides a declarative way to build interactive loops with callback support.
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from ppui import Menu
|
|
44
|
+
|
|
45
|
+
def run_setup():
|
|
46
|
+
print("Running setup...")
|
|
47
|
+
|
|
48
|
+
def main():
|
|
49
|
+
menu = Menu("Project Manager", style="bold green")
|
|
50
|
+
menu.add_option("Setup Project", run_setup)
|
|
51
|
+
menu.add_option("View Dashboard", "view_dash")
|
|
52
|
+
|
|
53
|
+
# Submenu support
|
|
54
|
+
advanced = Menu("Advanced Settings")
|
|
55
|
+
advanced.add_option("DNS Config", lambda: print("Configuring DNS..."))
|
|
56
|
+
menu.add_submenu("Advanced...", advanced, behavior="push")
|
|
57
|
+
|
|
58
|
+
menu.add_back_item()
|
|
59
|
+
menu.add_quit_item()
|
|
60
|
+
|
|
61
|
+
selection = menu.run()
|
|
62
|
+
```
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
ppui/__init__.py,sha256=cq47TICGZ34Vj2jEHnH19Xi8K-9oA_HdMJU7gTTKkIk,424
|
|
2
|
+
ppui/base.py,sha256=BGjaE7i188vIu3K3QHnlo0tw6vgfVZydhlgeWiWZx2c,1711
|
|
3
|
+
ppui/core.py,sha256=gJAEF65Ly-qSmYURtD1xsZ7AduTz1WErv6a4Yz9J9Gs,11760
|
|
4
|
+
ppui-2026.1.20.2.dist-info/METADATA,sha256=idFznS6IKvRi0-ebh1fvkFfe3gBZkc_P8gmvyhMo3Uk,2022
|
|
5
|
+
ppui-2026.1.20.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
6
|
+
ppui-2026.1.20.2.dist-info/RECORD,,
|