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.
@@ -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
+ ]