shinyhub-bookmarks 0.2.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.
- shinyhub_bookmarks-0.2.0/.gitignore +61 -0
- shinyhub_bookmarks-0.2.0/PKG-INFO +134 -0
- shinyhub_bookmarks-0.2.0/README.md +112 -0
- shinyhub_bookmarks-0.2.0/RELEASING.md +36 -0
- shinyhub_bookmarks-0.2.0/pyproject.toml +39 -0
- shinyhub_bookmarks-0.2.0/src/shinyhub_bookmarks/__init__.py +16 -0
- shinyhub_bookmarks-0.2.0/src/shinyhub_bookmarks/_adapter.py +547 -0
- shinyhub_bookmarks-0.2.0/src/shinyhub_bookmarks/_dependency.py +20 -0
- shinyhub_bookmarks-0.2.0/src/shinyhub_bookmarks/_restore.py +158 -0
- shinyhub_bookmarks-0.2.0/src/shinyhub_bookmarks/www/bridge.js +90 -0
- shinyhub_bookmarks-0.2.0/tests/test_adapter.py +246 -0
- shinyhub_bookmarks-0.2.0/tests/test_dependency.py +8 -0
- shinyhub_bookmarks-0.2.0/tests/test_restore.py +253 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
.superpowers/
|
|
2
|
+
docs/superpowers/
|
|
3
|
+
# Raw HTML under docs is an internal design/review artifact, not product
|
|
4
|
+
# documentation. Keep reports and their assets out of Git and deployments.
|
|
5
|
+
/docs/**/*.html
|
|
6
|
+
/docs/reports/
|
|
7
|
+
/scripts/docs-smoke.py
|
|
8
|
+
.claire/
|
|
9
|
+
.codex/
|
|
10
|
+
.impeccable/
|
|
11
|
+
go.work
|
|
12
|
+
go.work.sum
|
|
13
|
+
bin/
|
|
14
|
+
tmp/
|
|
15
|
+
.worktrees/
|
|
16
|
+
.claude/investigations/
|
|
17
|
+
CLAUDE.md
|
|
18
|
+
CLAUDE.local.md
|
|
19
|
+
|
|
20
|
+
# Product positioning and brand direction, maintained by local tooling rather
|
|
21
|
+
# than as a repo artifact. Nothing in the build or the docs site reads it.
|
|
22
|
+
PRODUCT.md
|
|
23
|
+
|
|
24
|
+
# JS toolchain (only used for JSDOM tests of UI assets)
|
|
25
|
+
node_modules/
|
|
26
|
+
|
|
27
|
+
# Compiled server binary from `go build` at the repo root.
|
|
28
|
+
/shinyhub
|
|
29
|
+
|
|
30
|
+
# Runtime artifacts created when running the server from a checkout
|
|
31
|
+
# (default config writes the SQLite DB and per-app data under ./data).
|
|
32
|
+
/shinyhub.yaml
|
|
33
|
+
/data/
|
|
34
|
+
/site/
|
|
35
|
+
/deploy/demo/.env
|
|
36
|
+
*.db
|
|
37
|
+
*.db-shm
|
|
38
|
+
*.db-wal
|
|
39
|
+
|
|
40
|
+
# PyPI wheel build artifacts
|
|
41
|
+
/packaging/python/src/shinyhub/_binary/shinyhub
|
|
42
|
+
/packaging/python/dist/
|
|
43
|
+
/packaging/python/build/
|
|
44
|
+
/packaging/python/src/shinyhub.egg-info/
|
|
45
|
+
/packaging/python/__pycache__/
|
|
46
|
+
|
|
47
|
+
# Python bytecode (repo-wide; the render-rig test suites generate it)
|
|
48
|
+
__pycache__/
|
|
49
|
+
|
|
50
|
+
# npm install artifact from the render-rig driver, not a deliverable
|
|
51
|
+
loadtest/render/driver/package-lock.json
|
|
52
|
+
|
|
53
|
+
# Local investigation scratch. Not shipped, and it carries customer names
|
|
54
|
+
# that must never reach a committed file.
|
|
55
|
+
research/
|
|
56
|
+
|
|
57
|
+
# Local prototypes kept beside the repo, never part of a build.
|
|
58
|
+
experiments/
|
|
59
|
+
|
|
60
|
+
# macOS Finder metadata.
|
|
61
|
+
.DS_Store
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: shinyhub-bookmarks
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Let ShinyHub visitors create selective URL bookmarks for Python Shiny apps.
|
|
5
|
+
Project-URL: Homepage, https://github.com/rvben/shinyhub
|
|
6
|
+
Project-URL: Repository, https://github.com/rvben/shinyhub
|
|
7
|
+
Project-URL: Issues, https://github.com/rvben/shinyhub/issues
|
|
8
|
+
Author-email: "Ruben J. Jongejan" <ruben.jongejan@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: bookmark,shiny,shinyhub,state
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: shiny<2,>=1.6.4
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Requires-Dist: pytest-asyncio>=1.2; extra == 'test'
|
|
20
|
+
Requires-Dist: pytest>=9; extra == 'test'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# shinyhub-bookmarks
|
|
24
|
+
|
|
25
|
+
Selective URL bookmarks for Python Shiny apps running in ShinyHub.
|
|
26
|
+
|
|
27
|
+
The package registers the filters an app author considers meaningful. ShinyHub
|
|
28
|
+
then adds a **Bookmark this view** control to its app switcher. Visitors see a
|
|
29
|
+
plain-language receipt, can copy the exact view immediately, or customise which
|
|
30
|
+
registered filters follow the link. Unselected filters return to the app's
|
|
31
|
+
defaults when the bookmark opens.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from shiny import App, render, ui
|
|
35
|
+
from shinyhub_bookmarks import ChoiceRestore, Field, bookmarking_dependency, register
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def app_ui(request):
|
|
39
|
+
return ui.page_fluid(
|
|
40
|
+
bookmarking_dependency(),
|
|
41
|
+
ui.input_select("region", "Region", ["Europe", "Americas", "Asia"]),
|
|
42
|
+
ui.input_slider("year", "Year", 2020, 2026, 2026),
|
|
43
|
+
ui.output_text_verbatim("summary"),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def server(input, output, session):
|
|
48
|
+
register(
|
|
49
|
+
session=session,
|
|
50
|
+
input=input,
|
|
51
|
+
fields={
|
|
52
|
+
"region": Field("Region"),
|
|
53
|
+
"year": Field("Year"),
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@render.text
|
|
58
|
+
def summary():
|
|
59
|
+
return f"{input.region()} · {input.year()}"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
app = App(app_ui, server, bookmark_store="url")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Both pieces are required: `bookmarking_dependency()` installs the tiny browser
|
|
66
|
+
bridge, and `register()` publishes the allow-list and creates links through
|
|
67
|
+
Shiny's native bookmarking API. The UI must be a function accepting `request`
|
|
68
|
+
so Shiny can restore URL state before rendering it.
|
|
69
|
+
|
|
70
|
+
## Bookmarks that outlive the app
|
|
71
|
+
|
|
72
|
+
Native Shiny restoration ignores a removed input, but a retired choice can
|
|
73
|
+
otherwise become empty or fall back differently across widgets. Declare current
|
|
74
|
+
choices when a bookmark should remain dependable across app releases:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
register(
|
|
78
|
+
session=session,
|
|
79
|
+
input=input,
|
|
80
|
+
schema_version=3,
|
|
81
|
+
legacy_fields={"segment": "Market segment"},
|
|
82
|
+
fields={
|
|
83
|
+
"region": Field(
|
|
84
|
+
"Region",
|
|
85
|
+
restore=ChoiceRestore(choices=REGIONS, default="Europe"),
|
|
86
|
+
renamed_from={"territory": "Territory"},
|
|
87
|
+
),
|
|
88
|
+
"product": Field(
|
|
89
|
+
"Product",
|
|
90
|
+
restore=ChoiceRestore(
|
|
91
|
+
choices=lambda: PRODUCTS,
|
|
92
|
+
default="All products",
|
|
93
|
+
aliases={"Legacy planning": "Planning"},
|
|
94
|
+
control="select",
|
|
95
|
+
),
|
|
96
|
+
),
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`ChoiceRestore` validates the saved value, applies aliases, and updates the
|
|
102
|
+
current Shiny choice input. Supported controls are `select`, `selectize`, and
|
|
103
|
+
`radio`. Multiple selections retain every choice that still exists. A missing
|
|
104
|
+
declared default falls back to the valid value Shiny already selected.
|
|
105
|
+
|
|
106
|
+
`renamed_from` maps an old input ID to its former label. `legacy_fields` names
|
|
107
|
+
removed inputs that should be reported as ignored. Every new link records the
|
|
108
|
+
app's `schema_version`; older links without metadata remain supported.
|
|
109
|
+
|
|
110
|
+
When anything changes, the switcher marks the bookmark action and presents a
|
|
111
|
+
plain-language **View adjusted** receipt. It names migrated, unavailable,
|
|
112
|
+
renamed, and removed filters and offers **Copy updated link**. The app still
|
|
113
|
+
opens; stale state is never promoted into a blocking error.
|
|
114
|
+
|
|
115
|
+
An input that is neither registered nor declared as renamed or removed is shown
|
|
116
|
+
with its URL-provided ID and saved value, labelled **Unknown**. Both are rendered
|
|
117
|
+
as bounded plain text. At most three unknown inputs are listed before a compact
|
|
118
|
+
overflow summary. Copying the updated link drops every unknown setting, so the
|
|
119
|
+
warning clears on the next visit.
|
|
120
|
+
|
|
121
|
+
## Privacy and behaviour
|
|
122
|
+
|
|
123
|
+
- The browser-local ShinyHub switcher receives the registered display values and
|
|
124
|
+
generated URL so it can show the receipt and copy the link. The ShinyHub
|
|
125
|
+
server neither receives nor persists bookmark state; selected values stay in
|
|
126
|
+
Shiny's URL.
|
|
127
|
+
- Every registered field is selected by default, including values equal to the
|
|
128
|
+
app's current defaults. The receipt makes that scope explicit before copying.
|
|
129
|
+
- App inputs not registered here are always excluded.
|
|
130
|
+
- A custom bookmark with no selected fields cannot be created.
|
|
131
|
+
- URLs over 8 KiB are rejected by default. Raise `max_url_length` only when the
|
|
132
|
+
complete delivery path is known to accept longer URLs.
|
|
133
|
+
- The control stays absent when the bridge is not installed, so unsupported apps
|
|
134
|
+
never show a dead action.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# shinyhub-bookmarks
|
|
2
|
+
|
|
3
|
+
Selective URL bookmarks for Python Shiny apps running in ShinyHub.
|
|
4
|
+
|
|
5
|
+
The package registers the filters an app author considers meaningful. ShinyHub
|
|
6
|
+
then adds a **Bookmark this view** control to its app switcher. Visitors see a
|
|
7
|
+
plain-language receipt, can copy the exact view immediately, or customise which
|
|
8
|
+
registered filters follow the link. Unselected filters return to the app's
|
|
9
|
+
defaults when the bookmark opens.
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from shiny import App, render, ui
|
|
13
|
+
from shinyhub_bookmarks import ChoiceRestore, Field, bookmarking_dependency, register
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def app_ui(request):
|
|
17
|
+
return ui.page_fluid(
|
|
18
|
+
bookmarking_dependency(),
|
|
19
|
+
ui.input_select("region", "Region", ["Europe", "Americas", "Asia"]),
|
|
20
|
+
ui.input_slider("year", "Year", 2020, 2026, 2026),
|
|
21
|
+
ui.output_text_verbatim("summary"),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def server(input, output, session):
|
|
26
|
+
register(
|
|
27
|
+
session=session,
|
|
28
|
+
input=input,
|
|
29
|
+
fields={
|
|
30
|
+
"region": Field("Region"),
|
|
31
|
+
"year": Field("Year"),
|
|
32
|
+
},
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@render.text
|
|
36
|
+
def summary():
|
|
37
|
+
return f"{input.region()} · {input.year()}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
app = App(app_ui, server, bookmark_store="url")
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Both pieces are required: `bookmarking_dependency()` installs the tiny browser
|
|
44
|
+
bridge, and `register()` publishes the allow-list and creates links through
|
|
45
|
+
Shiny's native bookmarking API. The UI must be a function accepting `request`
|
|
46
|
+
so Shiny can restore URL state before rendering it.
|
|
47
|
+
|
|
48
|
+
## Bookmarks that outlive the app
|
|
49
|
+
|
|
50
|
+
Native Shiny restoration ignores a removed input, but a retired choice can
|
|
51
|
+
otherwise become empty or fall back differently across widgets. Declare current
|
|
52
|
+
choices when a bookmark should remain dependable across app releases:
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
register(
|
|
56
|
+
session=session,
|
|
57
|
+
input=input,
|
|
58
|
+
schema_version=3,
|
|
59
|
+
legacy_fields={"segment": "Market segment"},
|
|
60
|
+
fields={
|
|
61
|
+
"region": Field(
|
|
62
|
+
"Region",
|
|
63
|
+
restore=ChoiceRestore(choices=REGIONS, default="Europe"),
|
|
64
|
+
renamed_from={"territory": "Territory"},
|
|
65
|
+
),
|
|
66
|
+
"product": Field(
|
|
67
|
+
"Product",
|
|
68
|
+
restore=ChoiceRestore(
|
|
69
|
+
choices=lambda: PRODUCTS,
|
|
70
|
+
default="All products",
|
|
71
|
+
aliases={"Legacy planning": "Planning"},
|
|
72
|
+
control="select",
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`ChoiceRestore` validates the saved value, applies aliases, and updates the
|
|
80
|
+
current Shiny choice input. Supported controls are `select`, `selectize`, and
|
|
81
|
+
`radio`. Multiple selections retain every choice that still exists. A missing
|
|
82
|
+
declared default falls back to the valid value Shiny already selected.
|
|
83
|
+
|
|
84
|
+
`renamed_from` maps an old input ID to its former label. `legacy_fields` names
|
|
85
|
+
removed inputs that should be reported as ignored. Every new link records the
|
|
86
|
+
app's `schema_version`; older links without metadata remain supported.
|
|
87
|
+
|
|
88
|
+
When anything changes, the switcher marks the bookmark action and presents a
|
|
89
|
+
plain-language **View adjusted** receipt. It names migrated, unavailable,
|
|
90
|
+
renamed, and removed filters and offers **Copy updated link**. The app still
|
|
91
|
+
opens; stale state is never promoted into a blocking error.
|
|
92
|
+
|
|
93
|
+
An input that is neither registered nor declared as renamed or removed is shown
|
|
94
|
+
with its URL-provided ID and saved value, labelled **Unknown**. Both are rendered
|
|
95
|
+
as bounded plain text. At most three unknown inputs are listed before a compact
|
|
96
|
+
overflow summary. Copying the updated link drops every unknown setting, so the
|
|
97
|
+
warning clears on the next visit.
|
|
98
|
+
|
|
99
|
+
## Privacy and behaviour
|
|
100
|
+
|
|
101
|
+
- The browser-local ShinyHub switcher receives the registered display values and
|
|
102
|
+
generated URL so it can show the receipt and copy the link. The ShinyHub
|
|
103
|
+
server neither receives nor persists bookmark state; selected values stay in
|
|
104
|
+
Shiny's URL.
|
|
105
|
+
- Every registered field is selected by default, including values equal to the
|
|
106
|
+
app's current defaults. The receipt makes that scope explicit before copying.
|
|
107
|
+
- App inputs not registered here are always excluded.
|
|
108
|
+
- A custom bookmark with no selected fields cannot be created.
|
|
109
|
+
- URLs over 8 KiB are rejected by default. Raise `max_url_length` only when the
|
|
110
|
+
complete delivery path is known to accept longer URLs.
|
|
111
|
+
- The control stays absent when the bridge is not installed, so unsupported apps
|
|
112
|
+
never show a dead action.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Releasing shinyhub-bookmarks
|
|
2
|
+
|
|
3
|
+
`shinyhub-bookmarks` is versioned independently from the ShinyHub server. A
|
|
4
|
+
release is built from an immutable package-specific tag and published through
|
|
5
|
+
PyPI Trusted Publishing with `uv publish`; no long-lived API token is used.
|
|
6
|
+
|
|
7
|
+
## First release only
|
|
8
|
+
|
|
9
|
+
Before pushing the first tag, add a pending GitHub publisher under the PyPI
|
|
10
|
+
account's **Publishing** settings:
|
|
11
|
+
|
|
12
|
+
- PyPI project name: `shinyhub-bookmarks`
|
|
13
|
+
- GitHub owner: `rvben`
|
|
14
|
+
- Repository: `shinyhub`
|
|
15
|
+
- Workflow: `publish-bookmarks.yml`
|
|
16
|
+
- Environment: `pypi`
|
|
17
|
+
|
|
18
|
+
The pending publisher creates the project on its first successful upload. It
|
|
19
|
+
does not reserve the name before then.
|
|
20
|
+
|
|
21
|
+
## Release checklist
|
|
22
|
+
|
|
23
|
+
1. Update `project.version` in `pyproject.toml` and `__version__` in
|
|
24
|
+
`src/shinyhub_bookmarks/__init__.py` in the same commit.
|
|
25
|
+
2. Run `make test-py-bookmarks` and the repository's JavaScript test suite.
|
|
26
|
+
3. Build both distributions with `uv build --no-sources` and validate their
|
|
27
|
+
metadata with a current Twine release.
|
|
28
|
+
4. Merge the release commit to `main`.
|
|
29
|
+
5. Tag that exact commit as `shinyhub-bookmarks-v<version>` and push the tag.
|
|
30
|
+
6. Wait for the **publish shinyhub-bookmarks** workflow to finish.
|
|
31
|
+
7. Verify the version, files, project links, and Trusted Publisher attestation
|
|
32
|
+
on PyPI.
|
|
33
|
+
|
|
34
|
+
The workflow rejects a tag whose version does not exactly match the package
|
|
35
|
+
metadata. PyPI also rejects re-uploading an existing filename, so published
|
|
36
|
+
versions are immutable; issue a patch release for any correction.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "shinyhub-bookmarks"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Let ShinyHub visitors create selective URL bookmarks for Python Shiny apps."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Ruben J. Jongejan", email = "ruben.jongejan@gmail.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["shinyhub", "shiny", "bookmark", "state"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"shiny>=1.6.4,<2",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/rvben/shinyhub"
|
|
29
|
+
Repository = "https://github.com/rvben/shinyhub"
|
|
30
|
+
Issues = "https://github.com/rvben/shinyhub/issues"
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
test = ["pytest>=9", "pytest-asyncio>=1.2"]
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["src/shinyhub_bookmarks"]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Selective URL bookmarking for Python Shiny apps hosted by ShinyHub."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.2.0"
|
|
4
|
+
|
|
5
|
+
from ._adapter import Field, Registration, register
|
|
6
|
+
from ._dependency import bookmarking_dependency
|
|
7
|
+
from ._restore import Adjustment, ChoiceRestore
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"Adjustment",
|
|
11
|
+
"ChoiceRestore",
|
|
12
|
+
"Field",
|
|
13
|
+
"Registration",
|
|
14
|
+
"bookmarking_dependency",
|
|
15
|
+
"register",
|
|
16
|
+
]
|