pycascadeui 3.2.0__tar.gz → 3.3.0__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.
- {pycascadeui-3.2.0/pycascadeui.egg-info → pycascadeui-3.3.0}/PKG-INFO +9 -4
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/README.md +5 -3
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/__init__.py +8 -3
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/components/__init__.py +4 -1
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/components/patterns/__init__.py +2 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/components/patterns/v2.py +91 -14
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/components/types.py +18 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/persistence/__init__.py +5 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/persistence/backends/__init__.py +9 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/persistence/backends/memory.py +40 -1
- pycascadeui-3.3.0/cascadeui/persistence/backends/postgres.py +847 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/persistence/backends/sqlite.py +204 -7
- pycascadeui-3.3.0/cascadeui/persistence/protocols.py +301 -0
- pycascadeui-3.3.0/cascadeui/persistence/schema_postgres.py +125 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/utils/__init__.py +3 -1
- pycascadeui-3.3.0/cascadeui/utils/fetch.py +73 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/views/_navigation.py +6 -0
- pycascadeui-3.3.0/cascadeui/views/_placement.py +438 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/views/base.py +55 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/views/layout.py +40 -2
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/views/patterns/paginated.py +18 -2
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/views/patterns/roles.py +1 -1
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/views/persistent.py +11 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/views/view.py +23 -1
- {pycascadeui-3.2.0 → pycascadeui-3.3.0/pycascadeui.egg-info}/PKG-INFO +9 -4
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/pycascadeui.egg-info/SOURCES.txt +9 -1
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/pycascadeui.egg-info/requires.txt +4 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/pyproject.toml +5 -1
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_auto_defer.py +9 -6
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_backends.py +32 -21
- pycascadeui-3.3.0/tests/test_backends_postgres.py +426 -0
- pycascadeui-3.3.0/tests/test_backends_raw_sql.py +442 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_computed.py +1 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_ephemeral_refresh.py +9 -8
- pycascadeui-3.3.0/tests/test_fetch.py +146 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_five_pillars.py +4 -11
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_hooks.py +1 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_layout_view.py +64 -1
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_menu_patterns.py +11 -1
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_message_cleanup.py +5 -1
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_navigation_artifact_restore.py +0 -1
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_paginated_patterns.py +61 -11
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_pagination.py +52 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_persistence.py +19 -17
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_persistence_middleware.py +6 -19
- pycascadeui-3.3.0/tests/test_placement.py +534 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_scoping.py +26 -39
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_selects.py +0 -1
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_state_slots.py +10 -30
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_theming.py +3 -1
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_undo.py +6 -4
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_v2_helpers.py +115 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_validation.py +3 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_view_init.py +91 -2
- pycascadeui-3.2.0/cascadeui/persistence/protocols.py +0 -171
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/LICENSE +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/__main__.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/components/base.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/components/buttons.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/components/inputs.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/components/patterns/v1.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/components/selects.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/components/v1_composition.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/components/wrappers.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/devtools.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/exceptions.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/persistence/config.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/persistence/manager.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/persistence/migrations.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/persistence/schema.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/py.typed +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/setup.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/state/__init__.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/state/actions.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/state/computed.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/state/middleware/__init__.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/state/middleware/logging.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/state/middleware/persistence.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/state/middleware/undo.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/state/reducers.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/state/singleton.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/state/slots.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/state/store.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/state/types.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/theming/__init__.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/theming/context.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/theming/core.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/theming/themes.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/tracing.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/utils/coercion.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/utils/decorators.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/utils/errors.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/utils/logging.py +0 -0
- pycascadeui-3.2.0/cascadeui/utils/helpers.py → pycascadeui-3.3.0/cascadeui/utils/strings.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/utils/tasks.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/validation.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/views/__init__.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/views/_interaction.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/views/patterns/__init__.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/views/patterns/form.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/views/patterns/leaderboard.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/views/patterns/menu.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/views/patterns/tabs.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/views/patterns/types.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/cascadeui/views/patterns/wizard.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/pycascadeui.egg-info/dependency_links.txt +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/pycascadeui.egg-info/top_level.txt +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/setup.cfg +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_batching.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_button_grid.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_components.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_devtools.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_dynamic_persistent_button.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_emoji_grid.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_form_patterns.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_input_coercion.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_inputs.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_instance_limit.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_layout_pagination.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_layout_patterns.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_layout_persistent.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_leaderboard_patterns.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_middleware.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_nav_version.py +1 -1
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_navigation.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_owner_only.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_persistent_views.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_pillar_isolation.py +1 -1
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_rebuild_callback.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_reducers.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_roles_patterns.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_state_store.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_tab_patterns.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_update_coalescing.py +2 -2
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_wizard_patterns.py +0 -0
- {pycascadeui-3.2.0 → pycascadeui-3.3.0}/tests/test_wrappers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pycascadeui
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: Redux-inspired UI framework for discord.py
|
|
5
5
|
Author-email: HollowTheSilver <hollow@users.noreply.github.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -30,12 +30,15 @@ Requires-Dist: pytest>=7.0; extra == "dev"
|
|
|
30
30
|
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
31
31
|
Requires-Dist: black>=26.3; extra == "dev"
|
|
32
32
|
Requires-Dist: isort>=8.0; extra == "dev"
|
|
33
|
+
Requires-Dist: testcontainers[postgres]>=4.0; extra == "dev"
|
|
33
34
|
Provides-Extra: docs
|
|
34
35
|
Requires-Dist: mkdocs>=1.5; extra == "docs"
|
|
35
36
|
Requires-Dist: mkdocs-material>=9.0; extra == "docs"
|
|
36
37
|
Requires-Dist: pymdown-extensions>=10.0; extra == "docs"
|
|
37
38
|
Provides-Extra: sqlite
|
|
38
39
|
Requires-Dist: aiosqlite>=0.19; extra == "sqlite"
|
|
40
|
+
Provides-Extra: postgres
|
|
41
|
+
Requires-Dist: asyncpg>=0.30; extra == "postgres"
|
|
39
42
|
Dynamic: license-file
|
|
40
43
|
|
|
41
44
|
<p align="center">
|
|
@@ -149,7 +152,8 @@ pip install pycascadeui
|
|
|
149
152
|
Optional dependencies:
|
|
150
153
|
|
|
151
154
|
```bash
|
|
152
|
-
pip install pycascadeui[sqlite]
|
|
155
|
+
pip install pycascadeui[sqlite] # single-process persistence
|
|
156
|
+
pip install pycascadeui[postgres] # multi-process persistence with LISTEN/NOTIFY
|
|
153
157
|
```
|
|
154
158
|
|
|
155
159
|
Requirements:
|
|
@@ -647,7 +651,7 @@ for row in rows:
|
|
|
647
651
|
### Components
|
|
648
652
|
- Stateful buttons, selects, and modals with state integration
|
|
649
653
|
- Select callbacks can opt into a `values` second parameter
|
|
650
|
-
- V2 builders: `card()`, `stats_card()`, `action_section()`, `toggle_section()`, `image_section()`, `link_section()`, `confirm_section()`, `button_row()`, `cycle_button()`, `toggle_button()`, `tab_nav()`, `key_value()`, `alert()`, `progress_bar()`, `divider()`, `gap()`, `gallery()`
|
|
654
|
+
- V2 builders: `card()`, `stats_card()`, `action_section()`, `toggle_section()`, `image_section()`, `link_section()`, `confirm_section()`, `button_row()`, `cycle_button()`, `toggle_button()`, `tab_nav()`, `key_value()`, `alert()`, `progress_bar()`, `divider()`, `gap()`, `gallery()`, `file_attachment()`
|
|
651
655
|
- Grid helpers: `emoji_grid()` and `button_grid()`
|
|
652
656
|
- Typed modal fields (`text`, `integer`, `float`, `date`) with per-field validation
|
|
653
657
|
- Declarative `FormSchema` and `WizardSchema` base classes
|
|
@@ -674,7 +678,8 @@ for row in rows:
|
|
|
674
678
|
### Persistence
|
|
675
679
|
- Persistent views that survive bot restarts with automatic message re-attachment
|
|
676
680
|
- Opt-in per slot via `persistent_slots = (...)` (≈ `redux-persist`)
|
|
677
|
-
- Built-in SQLite and in-memory backends; custom backends via capability-flag `Protocol`
|
|
681
|
+
- Built-in SQLite, PostgreSQL, and in-memory backends; custom backends via capability-flag `Protocol`
|
|
682
|
+
- Cross-process scoped invalidation through PostgreSQL `LISTEN`/`NOTIFY` (multi-worker bots)
|
|
678
683
|
- Named scoped buckets via `scoped_slot` for per-subsystem persistence
|
|
679
684
|
- Two-namespace model (`registry` and `application`) with per-namespace debounce and retry backoff
|
|
680
685
|
- Undo and redo via snapshot-based state history (opt in with `enable_undo`)
|
|
@@ -109,7 +109,8 @@ pip install pycascadeui
|
|
|
109
109
|
Optional dependencies:
|
|
110
110
|
|
|
111
111
|
```bash
|
|
112
|
-
pip install pycascadeui[sqlite]
|
|
112
|
+
pip install pycascadeui[sqlite] # single-process persistence
|
|
113
|
+
pip install pycascadeui[postgres] # multi-process persistence with LISTEN/NOTIFY
|
|
113
114
|
```
|
|
114
115
|
|
|
115
116
|
Requirements:
|
|
@@ -607,7 +608,7 @@ for row in rows:
|
|
|
607
608
|
### Components
|
|
608
609
|
- Stateful buttons, selects, and modals with state integration
|
|
609
610
|
- Select callbacks can opt into a `values` second parameter
|
|
610
|
-
- V2 builders: `card()`, `stats_card()`, `action_section()`, `toggle_section()`, `image_section()`, `link_section()`, `confirm_section()`, `button_row()`, `cycle_button()`, `toggle_button()`, `tab_nav()`, `key_value()`, `alert()`, `progress_bar()`, `divider()`, `gap()`, `gallery()`
|
|
611
|
+
- V2 builders: `card()`, `stats_card()`, `action_section()`, `toggle_section()`, `image_section()`, `link_section()`, `confirm_section()`, `button_row()`, `cycle_button()`, `toggle_button()`, `tab_nav()`, `key_value()`, `alert()`, `progress_bar()`, `divider()`, `gap()`, `gallery()`, `file_attachment()`
|
|
611
612
|
- Grid helpers: `emoji_grid()` and `button_grid()`
|
|
612
613
|
- Typed modal fields (`text`, `integer`, `float`, `date`) with per-field validation
|
|
613
614
|
- Declarative `FormSchema` and `WizardSchema` base classes
|
|
@@ -634,7 +635,8 @@ for row in rows:
|
|
|
634
635
|
### Persistence
|
|
635
636
|
- Persistent views that survive bot restarts with automatic message re-attachment
|
|
636
637
|
- Opt-in per slot via `persistent_slots = (...)` (≈ `redux-persist`)
|
|
637
|
-
- Built-in SQLite and in-memory backends; custom backends via capability-flag `Protocol`
|
|
638
|
+
- Built-in SQLite, PostgreSQL, and in-memory backends; custom backends via capability-flag `Protocol`
|
|
639
|
+
- Cross-process scoped invalidation through PostgreSQL `LISTEN`/`NOTIFY` (multi-worker bots)
|
|
638
640
|
- Named scoped buckets via `scoped_slot` for per-subsystem persistence
|
|
639
641
|
- Two-namespace model (`registry` and `application`) with per-namespace debounce and retry backoff
|
|
640
642
|
- Undo and redo via snapshot-based state history (opt in with `enable_undo`)
|
|
@@ -28,6 +28,7 @@ from .components.patterns import (
|
|
|
28
28
|
cycle_button,
|
|
29
29
|
divider,
|
|
30
30
|
emoji_grid,
|
|
31
|
+
file_attachment,
|
|
31
32
|
gallery,
|
|
32
33
|
gap,
|
|
33
34
|
image_section,
|
|
@@ -39,7 +40,7 @@ from .components.patterns import (
|
|
|
39
40
|
toggle_button,
|
|
40
41
|
toggle_section,
|
|
41
42
|
)
|
|
42
|
-
from .components.types import EmojiInput
|
|
43
|
+
from .components.types import EmojiInput, MediaInput
|
|
43
44
|
from .components.v1_composition import CompositeComponent, get_component, register_component
|
|
44
45
|
from .components.wrappers import with_confirmation, with_cooldown, with_loading_state
|
|
45
46
|
from .devtools import DevToolsCog, InspectorView
|
|
@@ -82,8 +83,9 @@ from .theming.core import Theme, get_default_theme, get_theme, register_theme, s
|
|
|
82
83
|
from .theming.themes import dark_theme, default_theme, light_theme
|
|
83
84
|
from .utils.decorators import cascade_component, cascade_reducer
|
|
84
85
|
from .utils.errors import safe_execute, with_error_boundary, with_retry
|
|
85
|
-
from .utils.
|
|
86
|
+
from .utils.fetch import fetch_as_file
|
|
86
87
|
from .utils.logging import setup_logging
|
|
88
|
+
from .utils.strings import slugify
|
|
87
89
|
from .utils.tasks import get_task_manager
|
|
88
90
|
from .validation import (
|
|
89
91
|
ValidationResult,
|
|
@@ -130,7 +132,7 @@ if "SQLiteBackend" in _persistence_all:
|
|
|
130
132
|
# // ========================================( Script )======================================== // #
|
|
131
133
|
|
|
132
134
|
|
|
133
|
-
__version__ = "3.
|
|
135
|
+
__version__ = "3.3.0"
|
|
134
136
|
|
|
135
137
|
# Export public API
|
|
136
138
|
__all__ = [
|
|
@@ -197,6 +199,7 @@ __all__ = [
|
|
|
197
199
|
"with_cooldown",
|
|
198
200
|
# Type aliases
|
|
199
201
|
"EmojiInput",
|
|
202
|
+
"MediaInput",
|
|
200
203
|
# V2 Cards & Sections
|
|
201
204
|
"card",
|
|
202
205
|
"action_section",
|
|
@@ -220,6 +223,7 @@ __all__ = [
|
|
|
220
223
|
"tab_nav",
|
|
221
224
|
# V2 Media
|
|
222
225
|
"gallery",
|
|
226
|
+
"file_attachment",
|
|
223
227
|
# V2 Grids
|
|
224
228
|
"EmojiGrid",
|
|
225
229
|
"emoji_grid",
|
|
@@ -277,6 +281,7 @@ __all__ = [
|
|
|
277
281
|
"cascade_reducer",
|
|
278
282
|
"cascade_component",
|
|
279
283
|
"slugify",
|
|
284
|
+
"fetch_as_file",
|
|
280
285
|
# DevTools
|
|
281
286
|
"InspectorView",
|
|
282
287
|
"DevToolsCog",
|
|
@@ -23,6 +23,7 @@ from .patterns import (
|
|
|
23
23
|
confirm_section,
|
|
24
24
|
cycle_button,
|
|
25
25
|
divider,
|
|
26
|
+
file_attachment,
|
|
26
27
|
gallery,
|
|
27
28
|
gap,
|
|
28
29
|
image_section,
|
|
@@ -35,7 +36,7 @@ from .patterns import (
|
|
|
35
36
|
toggle_section,
|
|
36
37
|
)
|
|
37
38
|
from .selects import ChannelSelect, Dropdown, MentionableSelect, RoleSelect, UserSelect
|
|
38
|
-
from .types import EmojiInput
|
|
39
|
+
from .types import EmojiInput, MediaInput
|
|
39
40
|
from .v1_composition import CompositeComponent, get_component, register_component
|
|
40
41
|
from .wrappers import with_confirmation, with_cooldown, with_loading_state
|
|
41
42
|
|
|
@@ -102,6 +103,8 @@ __all__ = [
|
|
|
102
103
|
"tab_nav",
|
|
103
104
|
# V2 media
|
|
104
105
|
"gallery",
|
|
106
|
+
"file_attachment",
|
|
105
107
|
# Type aliases
|
|
106
108
|
"EmojiInput",
|
|
109
|
+
"MediaInput",
|
|
107
110
|
]
|
|
@@ -13,6 +13,7 @@ from .v2 import (
|
|
|
13
13
|
cycle_button,
|
|
14
14
|
divider,
|
|
15
15
|
emoji_grid,
|
|
16
|
+
file_attachment,
|
|
16
17
|
gallery,
|
|
17
18
|
gap,
|
|
18
19
|
image_section,
|
|
@@ -57,6 +58,7 @@ __all__ = [
|
|
|
57
58
|
"tab_nav",
|
|
58
59
|
# V2 media
|
|
59
60
|
"gallery",
|
|
61
|
+
"file_attachment",
|
|
60
62
|
# V2 grids
|
|
61
63
|
"EmojiGrid",
|
|
62
64
|
"emoji_grid",
|
|
@@ -9,6 +9,7 @@ from discord.enums import SeparatorSpacing
|
|
|
9
9
|
from discord.ui import (
|
|
10
10
|
ActionRow,
|
|
11
11
|
Container,
|
|
12
|
+
File,
|
|
12
13
|
MediaGallery,
|
|
13
14
|
Section,
|
|
14
15
|
Separator,
|
|
@@ -17,11 +18,33 @@ from discord.ui import (
|
|
|
17
18
|
)
|
|
18
19
|
|
|
19
20
|
from ..base import StatefulButton
|
|
20
|
-
from ..types import EmojiInput
|
|
21
|
+
from ..types import EmojiInput, MediaInput
|
|
21
22
|
|
|
22
23
|
# Discord content-length ceiling for a single TextDisplay component.
|
|
23
24
|
_TEXTDISPLAY_MAX_CHARS = 4000
|
|
24
25
|
|
|
26
|
+
|
|
27
|
+
def _coerce_media_ref(value: MediaInput) -> str:
|
|
28
|
+
"""Resolve a ``MediaInput`` to the URL string Discord's API consumes.
|
|
29
|
+
|
|
30
|
+
Strings pass through unchanged. A :class:`discord.File` resolves to
|
|
31
|
+
its ``.uri`` (the ``"attachment://<filename>"`` reference built from
|
|
32
|
+
the normalized filename). The builder emits only the reference
|
|
33
|
+
string; the bytes travel separately through
|
|
34
|
+
``view.send(files=[...])``.
|
|
35
|
+
|
|
36
|
+
Only the ``.uri`` is extracted -- the ``description`` and ``spoiler``
|
|
37
|
+
attributes of the source :class:`discord.File` are NOT forwarded.
|
|
38
|
+
Builders that wrap the reference in a component carrying those
|
|
39
|
+
fields (``image_section``, ``file_attachment``) accept them as
|
|
40
|
+
explicit kwargs, so the metadata is never silently lost on the
|
|
41
|
+
documented call paths.
|
|
42
|
+
"""
|
|
43
|
+
if isinstance(value, discord.File):
|
|
44
|
+
return value.uri
|
|
45
|
+
return value
|
|
46
|
+
|
|
47
|
+
|
|
25
48
|
# Regional indicator emoji for alpha axis preset (🇦 through 🇿, 26 glyphs).
|
|
26
49
|
_ALPHA_LABELS = [chr(0x1F1E6 + i) for i in range(26)]
|
|
27
50
|
|
|
@@ -181,7 +204,7 @@ def toggle_section(
|
|
|
181
204
|
def image_section(
|
|
182
205
|
text: str,
|
|
183
206
|
*,
|
|
184
|
-
url:
|
|
207
|
+
url: MediaInput,
|
|
185
208
|
description: Optional[str] = None,
|
|
186
209
|
spoiler: bool = False,
|
|
187
210
|
) -> Section:
|
|
@@ -192,7 +215,11 @@ def image_section(
|
|
|
192
215
|
|
|
193
216
|
Args:
|
|
194
217
|
text: Display text (supports markdown).
|
|
195
|
-
url: Image
|
|
218
|
+
url: Image reference for the thumbnail. Accepts either a URL
|
|
219
|
+
string (remote or ``attachment://`` form) or a
|
|
220
|
+
:class:`discord.File` instance whose ``.uri`` is used.
|
|
221
|
+
File-backed references require the same ``discord.File`` to
|
|
222
|
+
be passed via ``view.send(files=[...])``.
|
|
196
223
|
description: Optional alt text for the thumbnail (up to 256 chars).
|
|
197
224
|
spoiler: Whether the thumbnail is hidden behind a spoiler.
|
|
198
225
|
|
|
@@ -206,7 +233,7 @@ def image_section(
|
|
|
206
233
|
url=member.display_avatar.url,
|
|
207
234
|
)
|
|
208
235
|
"""
|
|
209
|
-
kwargs = {"media": url, "spoiler": spoiler}
|
|
236
|
+
kwargs = {"media": _coerce_media_ref(url), "spoiler": spoiler}
|
|
210
237
|
if description is not None:
|
|
211
238
|
kwargs["description"] = description
|
|
212
239
|
return Section(
|
|
@@ -790,18 +817,22 @@ def tab_nav(
|
|
|
790
817
|
|
|
791
818
|
|
|
792
819
|
def gallery(
|
|
793
|
-
*
|
|
820
|
+
*media: MediaInput,
|
|
794
821
|
descriptions: Optional[Sequence[Optional[str]]] = None,
|
|
795
822
|
) -> MediaGallery:
|
|
796
|
-
"""Build a MediaGallery from image
|
|
823
|
+
"""Build a MediaGallery from image references.
|
|
797
824
|
|
|
798
825
|
Simplifies the ``MediaGallery(MediaGalleryItem(...), ...)`` nesting
|
|
799
|
-
into a flat call
|
|
826
|
+
into a flat call.
|
|
800
827
|
|
|
801
828
|
Args:
|
|
802
|
-
*
|
|
829
|
+
*media: Image references (up to 10). Each accepts either a URL
|
|
830
|
+
string (remote or ``attachment://`` form) or a
|
|
831
|
+
:class:`discord.File` instance whose ``.uri`` is used.
|
|
832
|
+
File-backed references require the same ``discord.File``
|
|
833
|
+
objects to be passed via ``view.send(files=[...])``.
|
|
803
834
|
descriptions: Optional sequence of descriptions matching each
|
|
804
|
-
|
|
835
|
+
reference positionally. Use ``None`` for items without a
|
|
805
836
|
description.
|
|
806
837
|
|
|
807
838
|
Returns:
|
|
@@ -815,23 +846,69 @@ def gallery(
|
|
|
815
846
|
descriptions=["First image", None],
|
|
816
847
|
)
|
|
817
848
|
"""
|
|
818
|
-
if
|
|
849
|
+
if not media:
|
|
850
|
+
raise ValueError(
|
|
851
|
+
"gallery: at least one media reference is required. Discord "
|
|
852
|
+
"rejects empty MediaGallery components with HTTP 400 (1-10 "
|
|
853
|
+
"items required)."
|
|
854
|
+
)
|
|
855
|
+
if len(media) > 10:
|
|
856
|
+
raise ValueError(
|
|
857
|
+
f"gallery: too many media references ({len(media)}). Discord "
|
|
858
|
+
f"caps MediaGallery at 10 items. Split into multiple gallery() "
|
|
859
|
+
f"calls."
|
|
860
|
+
)
|
|
861
|
+
if descriptions is not None and len(descriptions) != len(media):
|
|
819
862
|
raise ValueError(
|
|
820
863
|
f"gallery: descriptions length ({len(descriptions)}) must match "
|
|
821
|
-
f"
|
|
822
|
-
f"have no description."
|
|
864
|
+
f"media length ({len(media)}). Pad with None for references that "
|
|
865
|
+
f"should have no description."
|
|
823
866
|
)
|
|
824
867
|
|
|
825
868
|
items = []
|
|
826
|
-
for i,
|
|
869
|
+
for i, ref in enumerate(media):
|
|
827
870
|
desc = descriptions[i] if descriptions is not None else None
|
|
828
|
-
kwargs = {"media":
|
|
871
|
+
kwargs = {"media": _coerce_media_ref(ref)}
|
|
829
872
|
if desc is not None:
|
|
830
873
|
kwargs["description"] = desc
|
|
831
874
|
items.append(MediaGalleryItem(**kwargs))
|
|
832
875
|
return MediaGallery(*items)
|
|
833
876
|
|
|
834
877
|
|
|
878
|
+
def file_attachment(
|
|
879
|
+
url: MediaInput,
|
|
880
|
+
*,
|
|
881
|
+
spoiler: bool = False,
|
|
882
|
+
) -> File:
|
|
883
|
+
"""Build a File component for attachment display.
|
|
884
|
+
|
|
885
|
+
Completes the V2 media family alongside ``gallery()``. Discord
|
|
886
|
+
renders the file as a downloadable card inline with the rest of the
|
|
887
|
+
V2 content.
|
|
888
|
+
|
|
889
|
+
Args:
|
|
890
|
+
url: File reference. Accepts either a remote URL, the
|
|
891
|
+
``attachment://<filename>`` form, or a
|
|
892
|
+
:class:`discord.File` instance whose ``.uri`` is used. When
|
|
893
|
+
a file-backed reference is supplied, the same
|
|
894
|
+
``discord.File`` must travel via ``view.send(files=[...])``.
|
|
895
|
+
spoiler: Whether to flag the file as a spoiler.
|
|
896
|
+
|
|
897
|
+
Returns:
|
|
898
|
+
A ``File`` component ready to be added to a Container or
|
|
899
|
+
``StatefulLayoutView``.
|
|
900
|
+
|
|
901
|
+
Example::
|
|
902
|
+
|
|
903
|
+
card(
|
|
904
|
+
"## Quarterly Report",
|
|
905
|
+
file_attachment("attachment://q1_2026.pdf"),
|
|
906
|
+
"Released April 15.",
|
|
907
|
+
)
|
|
908
|
+
"""
|
|
909
|
+
return File(media=_coerce_media_ref(url), spoiler=spoiler)
|
|
910
|
+
|
|
911
|
+
|
|
835
912
|
# // ========================================( Grids )======================================== // #
|
|
836
913
|
|
|
837
914
|
|
|
@@ -23,3 +23,21 @@ A live :class:`discord.Emoji` (returned by ``bot.get_emoji`` or
|
|
|
23
23
|
``bot.fetch_application_emoji``) and a :class:`discord.PartialEmoji`
|
|
24
24
|
instance are also accepted directly.
|
|
25
25
|
"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
MediaInput = Union[str, discord.File]
|
|
29
|
+
"""Anything CascadeUI accepts where a media reference is required.
|
|
30
|
+
|
|
31
|
+
Mirrors the union accepted by :class:`discord.ui.MediaGallery`,
|
|
32
|
+
:class:`discord.ui.Thumbnail`, and :class:`discord.ui.File`. Two forms:
|
|
33
|
+
|
|
34
|
+
* A URL string -- either an arbitrary remote URL
|
|
35
|
+
(``"https://cdn.example.com/img.png"``) or the
|
|
36
|
+
``"attachment://<filename>"`` reference scheme for files uploaded
|
|
37
|
+
alongside the same message.
|
|
38
|
+
* A :class:`discord.File` instance, in which case the underlying
|
|
39
|
+
``.uri`` (``"attachment://<filename>"``) is used. The same file
|
|
40
|
+
object must also be passed via ``view.send(files=[...])`` (or
|
|
41
|
+
``refresh(attachments=[...])`` for in-place swaps) so the bytes
|
|
42
|
+
travel with the message.
|
|
43
|
+
"""
|
|
@@ -29,3 +29,8 @@ if "SQLiteBackend" in _backends_all:
|
|
|
29
29
|
from .backends import SQLiteBackend # noqa: F401
|
|
30
30
|
|
|
31
31
|
__all__.append("SQLiteBackend")
|
|
32
|
+
|
|
33
|
+
if "PostgresBackend" in _backends_all:
|
|
34
|
+
from .backends import PostgresBackend # noqa: F401
|
|
35
|
+
|
|
36
|
+
__all__.append("PostgresBackend")
|
|
@@ -17,3 +17,12 @@ try:
|
|
|
17
17
|
except ImportError as _e:
|
|
18
18
|
if "aiosqlite" not in str(_e):
|
|
19
19
|
_logger.warning(f"Failed to import SQLiteBackend: {_e}")
|
|
20
|
+
|
|
21
|
+
# Optional backend -- imported lazily to avoid a hard dependency on asyncpg
|
|
22
|
+
try:
|
|
23
|
+
from .postgres import PostgresBackend
|
|
24
|
+
|
|
25
|
+
__all__.append("PostgresBackend")
|
|
26
|
+
except ImportError as _e:
|
|
27
|
+
if "asyncpg" not in str(_e):
|
|
28
|
+
_logger.warning(f"Failed to import PostgresBackend: {_e}")
|
|
@@ -45,9 +45,17 @@ class InMemoryBackend:
|
|
|
45
45
|
"""
|
|
46
46
|
|
|
47
47
|
capabilities: ClassVar[Capability] = (
|
|
48
|
-
Capability.KV
|
|
48
|
+
Capability.KV
|
|
49
|
+
| Capability.RELATIONAL
|
|
50
|
+
| Capability.TTL_INDEX
|
|
51
|
+
| Capability.SCHEMA_META
|
|
52
|
+
# Capability.RAW_SQL deliberately omitted -- in-memory storage
|
|
53
|
+
# has no SQL engine to escape to. Code paths that require raw SQL
|
|
54
|
+
# must check `Capability.RAW_SQL in backend.capabilities` first.
|
|
49
55
|
)
|
|
50
56
|
|
|
57
|
+
placeholder_style: ClassVar[str] = "n/a"
|
|
58
|
+
|
|
51
59
|
def __init__(self) -> None:
|
|
52
60
|
self._kv: dict[str, dict[str, bytes]] = {}
|
|
53
61
|
self._rows: dict[str, list[dict[str, Any]]] = {}
|
|
@@ -155,3 +163,34 @@ class InMemoryBackend:
|
|
|
155
163
|
|
|
156
164
|
async def set_schema_version(self, table: str, version: int) -> None:
|
|
157
165
|
self._schema_versions[table] = version
|
|
166
|
+
|
|
167
|
+
# // ========================================( Raw SQL opt-out stubs )======================================== // #
|
|
168
|
+
|
|
169
|
+
# Capability.RAW_SQL is deliberately not declared on this backend --
|
|
170
|
+
# in-memory storage has no SQL engine to escape to. The five methods
|
|
171
|
+
# below raise NotImplementedError with a clear remediation message
|
|
172
|
+
# rather than producing AttributeError, so callers who reach for the
|
|
173
|
+
# escape hatch against InMemoryBackend get a directed error pointing
|
|
174
|
+
# them at SQLiteBackend or PostgresBackend.
|
|
175
|
+
|
|
176
|
+
_RAW_SQL_ERROR: ClassVar[str] = (
|
|
177
|
+
"InMemoryBackend does not support Capability.RAW_SQL. "
|
|
178
|
+
"Use SQLiteBackend or PostgresBackend for raw-SQL operations, "
|
|
179
|
+
"or check `Capability.RAW_SQL in backend.capabilities` before "
|
|
180
|
+
"reaching for the escape hatch."
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
async def execute(self, sql: str, *params: Any) -> int:
|
|
184
|
+
raise NotImplementedError(self._RAW_SQL_ERROR)
|
|
185
|
+
|
|
186
|
+
async def fetch(self, sql: str, *params: Any) -> list[dict[str, Any]]:
|
|
187
|
+
raise NotImplementedError(self._RAW_SQL_ERROR)
|
|
188
|
+
|
|
189
|
+
async def executemany(self, sql: str, params_list: list[tuple]) -> int:
|
|
190
|
+
raise NotImplementedError(self._RAW_SQL_ERROR)
|
|
191
|
+
|
|
192
|
+
async def fetch_one(self, sql: str, *params: Any) -> dict[str, Any] | None:
|
|
193
|
+
raise NotImplementedError(self._RAW_SQL_ERROR)
|
|
194
|
+
|
|
195
|
+
def transaction(self) -> Any:
|
|
196
|
+
raise NotImplementedError(self._RAW_SQL_ERROR)
|