coffeehouse-ui 0.9.3__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.
Files changed (40) hide show
  1. coffeehouse_ui-0.9.3/LICENSE +21 -0
  2. coffeehouse_ui-0.9.3/PKG-INFO +185 -0
  3. coffeehouse_ui-0.9.3/README.md +130 -0
  4. coffeehouse_ui-0.9.3/coffeehouse_ui/__init__.py +36 -0
  5. coffeehouse_ui-0.9.3/coffeehouse_ui/permissions.py +141 -0
  6. coffeehouse_ui-0.9.3/coffeehouse_ui/static/css/coffeehouse-theme.css +314 -0
  7. coffeehouse_ui-0.9.3/coffeehouse_ui/static/css/coffeehouse.min.css +5 -0
  8. coffeehouse_ui-0.9.3/coffeehouse_ui/static/img/favicon.svg +5 -0
  9. coffeehouse_ui-0.9.3/coffeehouse_ui/static/js/csrf.js +35 -0
  10. coffeehouse_ui-0.9.3/coffeehouse_ui/static/js/culori.min.js +4 -0
  11. coffeehouse_ui-0.9.3/coffeehouse_ui/static/js/htmx.min.js +1 -0
  12. coffeehouse_ui-0.9.3/coffeehouse_ui/static/js/loading.js +91 -0
  13. coffeehouse_ui-0.9.3/coffeehouse_ui/static/js/permissions.js +155 -0
  14. coffeehouse_ui-0.9.3/coffeehouse_ui/static/js/theme.js +48 -0
  15. coffeehouse_ui-0.9.3/coffeehouse_ui/static/js/toast.js +88 -0
  16. coffeehouse_ui-0.9.3/coffeehouse_ui/templates/admin/macros/permissions.html +223 -0
  17. coffeehouse_ui-0.9.3/coffeehouse_ui/templates/admin/permissions_landing.html +67 -0
  18. coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/app_switcher.html +36 -0
  19. coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/confirm_modal.html +30 -0
  20. coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/data_table.html +57 -0
  21. coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/empty_state.html +32 -0
  22. coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/flash.html +36 -0
  23. coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/form_field.html +89 -0
  24. coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/loading.html +34 -0
  25. coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/navbar.html +94 -0
  26. coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/page_header.html +38 -0
  27. coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/pagination.html +52 -0
  28. coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/sidebar.html +146 -0
  29. coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/stale_roles_banner.html +31 -0
  30. coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/stat_card.html +17 -0
  31. coffeehouse_ui-0.9.3/coffeehouse_ui/templates/macros/icons.html +114 -0
  32. coffeehouse_ui-0.9.3/coffeehouse_ui/templates/macros/sortable.html +240 -0
  33. coffeehouse_ui-0.9.3/coffeehouse_ui/templates/ui_base.html +143 -0
  34. coffeehouse_ui-0.9.3/coffeehouse_ui.egg-info/PKG-INFO +185 -0
  35. coffeehouse_ui-0.9.3/coffeehouse_ui.egg-info/SOURCES.txt +38 -0
  36. coffeehouse_ui-0.9.3/coffeehouse_ui.egg-info/dependency_links.txt +1 -0
  37. coffeehouse_ui-0.9.3/coffeehouse_ui.egg-info/requires.txt +11 -0
  38. coffeehouse_ui-0.9.3/coffeehouse_ui.egg-info/top_level.txt +1 -0
  39. coffeehouse_ui-0.9.3/pyproject.toml +173 -0
  40. coffeehouse_ui-0.9.3/setup.cfg +4 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ruben Sukiasyan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,185 @@
1
+ Metadata-Version: 2.4
2
+ Name: coffeehouse-ui
3
+ Version: 0.9.3
4
+ Summary: Shared UI components, theme, and static assets for the Coffee House ecosystem
5
+ Author-email: Ruben Sukiasyan <rubsksn@gmail.com>
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Ruben Sukiasyan
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/coffeehouse-tools/coffeehouse-ui
29
+ Project-URL: Repository, https://github.com/coffeehouse-tools/coffeehouse-ui
30
+ Project-URL: Issues, https://github.com/coffeehouse-tools/coffeehouse-ui/issues
31
+ Project-URL: Changelog, https://github.com/coffeehouse-tools/coffeehouse-ui/blob/master/CHANGELOG.md
32
+ Classifier: Development Status :: 4 - Beta
33
+ Classifier: Intended Audience :: Developers
34
+ Classifier: License :: OSI Approved :: MIT License
35
+ Classifier: Programming Language :: Python :: 3
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Operating System :: OS Independent
39
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
40
+ Classifier: Typing :: Typed
41
+ Requires-Python: >=3.11
42
+ Description-Content-Type: text/markdown
43
+ License-File: LICENSE
44
+ Requires-Dist: jinja2>=3.1.0
45
+ Requires-Dist: starlette>=0.38.0
46
+ Provides-Extra: dev
47
+ Requires-Dist: pytest==9.0.3; extra == "dev"
48
+ Requires-Dist: pytest-cov==7.1.0; extra == "dev"
49
+ Requires-Dist: ruff==0.15.11; extra == "dev"
50
+ Requires-Dist: mypy==1.20.1; extra == "dev"
51
+ Requires-Dist: bandit==1.9.4; extra == "dev"
52
+ Requires-Dist: pip-audit==2.10.0; extra == "dev"
53
+ Requires-Dist: pre-commit==3.8.0; extra == "dev"
54
+ Dynamic: license-file
55
+
56
+ # coffeehouse-ui
57
+
58
+ Shared UI package for the [Coffee House digital ecosystem](https://github.com/rubencfh/coffeehouse-ecosystem). Pip-installable Python package providing Jinja2 base templates, shared components, CSS theme, JS utilities, brand assets, and the **unified permission UX** used by Digi, Grind, and Beany (`compute_effective(dept ∪ grants − revokes)` resolver + three shared Jinja macros for the dept-matrix / role-tri-state / simulate admin pages).
59
+
60
+ ## Quick Start
61
+
62
+ Install in your app's `requirements.txt`:
63
+
64
+ ```
65
+ coffeehouse-ui @ git+https://github.com/rubencfh/coffeehouse-ui.git@v0.4.0
66
+ ```
67
+
68
+ Integrate in your FastAPI/Starlette app:
69
+
70
+ ```python
71
+ from coffeehouse_ui import TEMPLATE_DIR as UI_TEMPLATE_DIR, STATIC_DIR as UI_STATIC_DIR
72
+ from starlette.templating import Jinja2Templates
73
+ from starlette.staticfiles import StaticFiles
74
+
75
+ templates = Jinja2Templates(directory=[str(app_templates_dir), str(UI_TEMPLATE_DIR)])
76
+ templates.env.globals["app_name"] = "YourApp"
77
+ app.mount("/ui", StaticFiles(directory=str(UI_STATIC_DIR)), name="ui_static")
78
+ ```
79
+
80
+ Your app's `base.html` extends `ui_base.html`:
81
+
82
+ ```html
83
+ {% extends "ui_base.html" %}
84
+ {% block title %}My Page{% endblock %}
85
+ {% block content %}...{% endblock %}
86
+ ```
87
+
88
+ ## What's Included
89
+
90
+ ### Templates
91
+
92
+ - **`ui_base.html`** — Canonical base template with Inter font, DaisyUI, Tailwind, custom theme, dark mode, HTMX, skip-to-content, toast container, CSRF/toast/loading JS
93
+ - **12 shared components** — navbar, sidebar, app_switcher, flash, page_header, data_table, empty_state, stat_card, pagination, form_field, confirm_modal, loading
94
+
95
+ ### Static Assets
96
+
97
+ - **CSS**: `coffeehouse.min.css` (purged Tailwind 3 + DaisyUI 4, 18KB gzip) + `coffeehouse-theme.css`
98
+ - **JS**: `theme.js`, `toast.js`, `loading.js`, `csrf.js`, `htmx.min.js` (v2.0.4), `culori.min.js` (color conversion for Auth brand admin)
99
+ - **Images**: `favicon.svg` (coffee cup)
100
+
101
+ ### Theme
102
+
103
+ Custom DaisyUI themes `coffeehouse` (light) and `coffeehouse-dark` (dark) with a warm espresso/brown palette. Dark mode toggle persists to localStorage.
104
+
105
+ ## CSS Build
106
+
107
+ CSS is built from `src/input.css` using Tailwind 3 + DaisyUI 4. The Tailwind config scans templates across all ecosystem apps.
108
+
109
+ ```bash
110
+ npm install # First time only
111
+ npm run build # Rebuild after adding new Tailwind classes
112
+ npm run watch # Watch mode for development
113
+ ```
114
+
115
+ Rebuild when a new Tailwind utility class is used in any app template that wasn't previously in the CSS bundle.
116
+
117
+ ## Template Linter
118
+
119
+ ```bash
120
+ python scripts/lint_templates.py --all
121
+ ```
122
+
123
+ Checks all ecosystem templates for CDN links, missing `role="alert"`, missing `scope="col"`, and hardcoded color classes.
124
+
125
+ ## Project Structure
126
+
127
+ ```
128
+ coffeehouse_ui/
129
+ ├── __init__.py # Exports TEMPLATE_DIR, STATIC_DIR, __version__
130
+ ├── templates/
131
+ │ ├── ui_base.html
132
+ │ └── components/ # 12 shared Jinja2 components
133
+ └── static/
134
+ ├── css/ # Built Tailwind + theme CSS
135
+ ├── js/ # Theme, toast, loading, CSRF, HTMX
136
+ └── img/ # Favicon
137
+ ```
138
+
139
+ ## Consumer Apps
140
+
141
+ | App | Repo |
142
+ |-----|------|
143
+ | Auth | [rubencfh/coffeehouse-auth](https://github.com/rubencfh/coffeehouse-auth) |
144
+ | Beany/KMS | [rubencfh/Beany](https://github.com/rubencfh/Beany) |
145
+ | Digi | [rubencfh/Digi](https://github.com/rubencfh/Digi) |
146
+ | Grind | [rubencfh/Grind](https://github.com/rubencfh/Grind) |
147
+ | CHQR | [rubencfh/CHQR](https://github.com/rubencfh/CHQR) |
148
+
149
+ ## Development
150
+
151
+ First-time setup:
152
+
153
+ ```bash
154
+ pip install -e ".[dev]"
155
+ pip install -r requirements-dev.txt # pulls coffeehouse-common[testing] + pinned tooling
156
+ pre-commit install # wires ruff-format + ruff + hygiene hooks on each commit
157
+ ```
158
+
159
+ Run the checks that CI runs:
160
+
161
+ ```bash
162
+ pytest # unit + integration tests with coverage
163
+ ruff format --check . && ruff check . # format + lint
164
+ mypy coffeehouse_ui # lenient typecheck
165
+ bandit -c pyproject.toml -r coffeehouse_ui scripts -ll # SAST
166
+ ```
167
+
168
+ `pre-commit run --all-files` runs the on-commit subset in one go. gitleaks runs in CI only (Windows Application Control blocks the Go-built local hook; install the native binary manually if you want local secret scanning).
169
+
170
+ The full pipeline (lint / typecheck / pytest w/ coverage / secret-scan / SAST / SCA) runs on every push and PR via `.github/workflows/ci.yml`, which is a thin caller of `rubencfh/coffeehouse-ecosystem/.github/workflows/reusable-python-ci.yml@master`. **As of the TBD cutover (2026-04-22), the suite is a merge gate on `master`**: 6 CI jobs + CodeRabbit review = 7 required status checks. Direct `git push origin master` is blocked; changes land through short-lived feature branches + PRs.
171
+
172
+ ## Branch Strategy — Trunk-Based Development
173
+
174
+ - `master` is the single long-lived branch (renamed from `dev` on 2026-04-22 for ecosystem-wide naming consistency; previously this repo shipped from `dev`).
175
+ - Every change goes through a short-lived feature branch off `master` + PR + squash merge. Feature branches auto-delete on merge.
176
+ - Tag releases as `v0.x.0` on `master` for consumer apps to pin.
177
+ - See `agents/shared/50-dev-workflow.md` and `agents/shared/97-pr-workflow.md` for the full flow.
178
+
179
+ ## Version
180
+
181
+ Current: **0.3.1**
182
+
183
+ ### Brand API (optional)
184
+
185
+ Set `templates.env.globals["auth_service_url"]` to your Auth base URL (e.g. `https://auth.coffeehouse.tools`). `ui_base.html` then loads active brand CSS and images from `GET {auth_service_url}/api/brand/active` on each session (cached in `sessionStorage` for 30 seconds). Omit or leave empty if you do not use the shared brand feature.
@@ -0,0 +1,130 @@
1
+ # coffeehouse-ui
2
+
3
+ Shared UI package for the [Coffee House digital ecosystem](https://github.com/rubencfh/coffeehouse-ecosystem). Pip-installable Python package providing Jinja2 base templates, shared components, CSS theme, JS utilities, brand assets, and the **unified permission UX** used by Digi, Grind, and Beany (`compute_effective(dept ∪ grants − revokes)` resolver + three shared Jinja macros for the dept-matrix / role-tri-state / simulate admin pages).
4
+
5
+ ## Quick Start
6
+
7
+ Install in your app's `requirements.txt`:
8
+
9
+ ```
10
+ coffeehouse-ui @ git+https://github.com/rubencfh/coffeehouse-ui.git@v0.4.0
11
+ ```
12
+
13
+ Integrate in your FastAPI/Starlette app:
14
+
15
+ ```python
16
+ from coffeehouse_ui import TEMPLATE_DIR as UI_TEMPLATE_DIR, STATIC_DIR as UI_STATIC_DIR
17
+ from starlette.templating import Jinja2Templates
18
+ from starlette.staticfiles import StaticFiles
19
+
20
+ templates = Jinja2Templates(directory=[str(app_templates_dir), str(UI_TEMPLATE_DIR)])
21
+ templates.env.globals["app_name"] = "YourApp"
22
+ app.mount("/ui", StaticFiles(directory=str(UI_STATIC_DIR)), name="ui_static")
23
+ ```
24
+
25
+ Your app's `base.html` extends `ui_base.html`:
26
+
27
+ ```html
28
+ {% extends "ui_base.html" %}
29
+ {% block title %}My Page{% endblock %}
30
+ {% block content %}...{% endblock %}
31
+ ```
32
+
33
+ ## What's Included
34
+
35
+ ### Templates
36
+
37
+ - **`ui_base.html`** — Canonical base template with Inter font, DaisyUI, Tailwind, custom theme, dark mode, HTMX, skip-to-content, toast container, CSRF/toast/loading JS
38
+ - **12 shared components** — navbar, sidebar, app_switcher, flash, page_header, data_table, empty_state, stat_card, pagination, form_field, confirm_modal, loading
39
+
40
+ ### Static Assets
41
+
42
+ - **CSS**: `coffeehouse.min.css` (purged Tailwind 3 + DaisyUI 4, 18KB gzip) + `coffeehouse-theme.css`
43
+ - **JS**: `theme.js`, `toast.js`, `loading.js`, `csrf.js`, `htmx.min.js` (v2.0.4), `culori.min.js` (color conversion for Auth brand admin)
44
+ - **Images**: `favicon.svg` (coffee cup)
45
+
46
+ ### Theme
47
+
48
+ Custom DaisyUI themes `coffeehouse` (light) and `coffeehouse-dark` (dark) with a warm espresso/brown palette. Dark mode toggle persists to localStorage.
49
+
50
+ ## CSS Build
51
+
52
+ CSS is built from `src/input.css` using Tailwind 3 + DaisyUI 4. The Tailwind config scans templates across all ecosystem apps.
53
+
54
+ ```bash
55
+ npm install # First time only
56
+ npm run build # Rebuild after adding new Tailwind classes
57
+ npm run watch # Watch mode for development
58
+ ```
59
+
60
+ Rebuild when a new Tailwind utility class is used in any app template that wasn't previously in the CSS bundle.
61
+
62
+ ## Template Linter
63
+
64
+ ```bash
65
+ python scripts/lint_templates.py --all
66
+ ```
67
+
68
+ Checks all ecosystem templates for CDN links, missing `role="alert"`, missing `scope="col"`, and hardcoded color classes.
69
+
70
+ ## Project Structure
71
+
72
+ ```
73
+ coffeehouse_ui/
74
+ ├── __init__.py # Exports TEMPLATE_DIR, STATIC_DIR, __version__
75
+ ├── templates/
76
+ │ ├── ui_base.html
77
+ │ └── components/ # 12 shared Jinja2 components
78
+ └── static/
79
+ ├── css/ # Built Tailwind + theme CSS
80
+ ├── js/ # Theme, toast, loading, CSRF, HTMX
81
+ └── img/ # Favicon
82
+ ```
83
+
84
+ ## Consumer Apps
85
+
86
+ | App | Repo |
87
+ |-----|------|
88
+ | Auth | [rubencfh/coffeehouse-auth](https://github.com/rubencfh/coffeehouse-auth) |
89
+ | Beany/KMS | [rubencfh/Beany](https://github.com/rubencfh/Beany) |
90
+ | Digi | [rubencfh/Digi](https://github.com/rubencfh/Digi) |
91
+ | Grind | [rubencfh/Grind](https://github.com/rubencfh/Grind) |
92
+ | CHQR | [rubencfh/CHQR](https://github.com/rubencfh/CHQR) |
93
+
94
+ ## Development
95
+
96
+ First-time setup:
97
+
98
+ ```bash
99
+ pip install -e ".[dev]"
100
+ pip install -r requirements-dev.txt # pulls coffeehouse-common[testing] + pinned tooling
101
+ pre-commit install # wires ruff-format + ruff + hygiene hooks on each commit
102
+ ```
103
+
104
+ Run the checks that CI runs:
105
+
106
+ ```bash
107
+ pytest # unit + integration tests with coverage
108
+ ruff format --check . && ruff check . # format + lint
109
+ mypy coffeehouse_ui # lenient typecheck
110
+ bandit -c pyproject.toml -r coffeehouse_ui scripts -ll # SAST
111
+ ```
112
+
113
+ `pre-commit run --all-files` runs the on-commit subset in one go. gitleaks runs in CI only (Windows Application Control blocks the Go-built local hook; install the native binary manually if you want local secret scanning).
114
+
115
+ The full pipeline (lint / typecheck / pytest w/ coverage / secret-scan / SAST / SCA) runs on every push and PR via `.github/workflows/ci.yml`, which is a thin caller of `rubencfh/coffeehouse-ecosystem/.github/workflows/reusable-python-ci.yml@master`. **As of the TBD cutover (2026-04-22), the suite is a merge gate on `master`**: 6 CI jobs + CodeRabbit review = 7 required status checks. Direct `git push origin master` is blocked; changes land through short-lived feature branches + PRs.
116
+
117
+ ## Branch Strategy — Trunk-Based Development
118
+
119
+ - `master` is the single long-lived branch (renamed from `dev` on 2026-04-22 for ecosystem-wide naming consistency; previously this repo shipped from `dev`).
120
+ - Every change goes through a short-lived feature branch off `master` + PR + squash merge. Feature branches auto-delete on merge.
121
+ - Tag releases as `v0.x.0` on `master` for consumer apps to pin.
122
+ - See `agents/shared/50-dev-workflow.md` and `agents/shared/97-pr-workflow.md` for the full flow.
123
+
124
+ ## Version
125
+
126
+ Current: **0.3.1**
127
+
128
+ ### Brand API (optional)
129
+
130
+ Set `templates.env.globals["auth_service_url"]` to your Auth base URL (e.g. `https://auth.coffeehouse.tools`). `ui_base.html` then loads active brand CSS and images from `GET {auth_service_url}/api/brand/active` on each session (cached in `sessionStorage` for 30 seconds). Omit or leave empty if you do not use the shared brand feature.
@@ -0,0 +1,36 @@
1
+ """
2
+ coffeehouse-ui: Shared UI components, theme, and static assets
3
+ for the Coffee House ecosystem.
4
+
5
+ Usage:
6
+ from coffeehouse_ui import TEMPLATE_DIR, STATIC_DIR
7
+
8
+ templates = Jinja2Templates(directory=["app/templates", str(TEMPLATE_DIR)])
9
+ app.mount("/ui", StaticFiles(directory=str(STATIC_DIR)), name="ui")
10
+ """
11
+
12
+ from pathlib import Path
13
+
14
+ from coffeehouse_ui.permissions import (
15
+ DimensionPolicy,
16
+ DimensionResolution,
17
+ compute_effective,
18
+ compute_effective_multi,
19
+ )
20
+
21
+ __version__ = "0.9.2"
22
+
23
+ _PKG = Path(__file__).resolve().parent
24
+
25
+ TEMPLATE_DIR = _PKG / "templates"
26
+ STATIC_DIR = _PKG / "static"
27
+
28
+ __all__ = [
29
+ "TEMPLATE_DIR",
30
+ "STATIC_DIR",
31
+ "compute_effective",
32
+ "compute_effective_multi",
33
+ "DimensionPolicy",
34
+ "DimensionResolution",
35
+ "__version__",
36
+ ]
@@ -0,0 +1,141 @@
1
+ """Unified dept + role grant/revoke resolver used by every onboarded app.
2
+
3
+ Each Coffee House app that does authorization implements the same pair:
4
+
5
+ - ``DepartmentAccess(dept_slug, value)`` -- the baseline a role inherits from
6
+ its department.
7
+ - ``RoleAccess(role_slug, value, mode)`` where ``mode in {"grant", "revoke"}``
8
+ -- role-level adjustments on top of the department baseline.
9
+
10
+ Effective access for a role on **one** dimension is::
11
+
12
+ effective = (dept_values | grants) - revokes
13
+
14
+ (set union, then set difference -- exactly the Python set operators.) The
15
+ only axis of access variation is role granularity: if two users need
16
+ different access, they need different roles.
17
+
18
+ For apps that gate access on **multiple attributes of the same entity**
19
+ (e.g. Grind gating branches on both ``branch_type`` and ``branch_brand``),
20
+ :func:`compute_effective_multi` extends the math: each dimension is
21
+ resolved independently with the same ``(dept | grants) - revokes`` rule,
22
+ and an entity is admitted iff **every constraining dimension** admits it
23
+ (AND across dimensions). A dimension with no policy at all (no baseline,
24
+ no grants, no revokes) does not constrain -- it drops out of the AND.
25
+
26
+ The AND combinator is the only correct combinator when the dimensions
27
+ describe attributes of one entity rather than orthogonal capabilities:
28
+ combining attribute filters with OR makes a ``revoke`` on dimension A
29
+ silently bypassable by a ``grant`` on dimension B, since the same row
30
+ gets readmitted via a different attribute. AND restores the principle
31
+ that an explicit revoke wins.
32
+
33
+ This helper is used on the backend to compute effective sets and on the
34
+ admin UI (via the matrix macros in
35
+ ``templates/admin/macros/permissions.html``) to render a live "Effective"
36
+ column as the operator edits role grants and revokes.
37
+ """
38
+
39
+ from __future__ import annotations
40
+
41
+ from collections.abc import Iterable
42
+ from dataclasses import dataclass, field
43
+
44
+
45
+ def compute_effective(
46
+ dept_values: Iterable[str],
47
+ grants: Iterable[str],
48
+ revokes: Iterable[str],
49
+ ) -> frozenset[str]:
50
+ """Return the effective value set for one role on one dimension.
51
+
52
+ Inputs are coerced to ``frozenset`` so callers can pass lists, generators,
53
+ ``dict_keys``, ORM attribute streams -- anything iterable. Whitespace-only
54
+ strings are filtered out so accidental empties don't become phantom members.
55
+
56
+ Example::
57
+
58
+ compute_effective(
59
+ dept_values=["view_salaries", "list_employees"],
60
+ grants=["delete_employee"],
61
+ revokes=["view_salaries"],
62
+ )
63
+ # -> frozenset({"list_employees", "delete_employee"})
64
+ """
65
+ dept_set = _clean(dept_values)
66
+ grant_set = _clean(grants)
67
+ revoke_set = _clean(revokes)
68
+ return frozenset((dept_set | grant_set) - revoke_set)
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class DimensionPolicy:
73
+ """Per-dimension access policy: dept baseline + role grants + role revokes.
74
+
75
+ Inputs are stored as-is and cleaned at resolution time (whitespace and
76
+ non-string entries dropped) -- same contract as :func:`compute_effective`.
77
+ """
78
+
79
+ dept_values: Iterable[str] = field(default_factory=tuple)
80
+ grants: Iterable[str] = field(default_factory=tuple)
81
+ revokes: Iterable[str] = field(default_factory=tuple)
82
+
83
+
84
+ @dataclass(frozen=True)
85
+ class DimensionResolution:
86
+ """Per-dimension resolution result.
87
+
88
+ ``effective`` is the ``(dept | grants) - revokes`` set computed from the
89
+ dimension's policy.
90
+
91
+ ``constrains`` is ``True`` when the dimension carries any policy at all
92
+ (any non-empty cleaned input). When ``False``, the caller MUST NOT use
93
+ this dimension as a filter -- it represents "unconfigured", not "denied".
94
+
95
+ The cross-dimension contract is::
96
+
97
+ entity is admitted iff for every dimension D where D.constrains:
98
+ entity.<D> in D.effective
99
+
100
+ A constraining dimension with empty ``effective`` (e.g. everything
101
+ revoked, or the role has zero baseline + zero grants but a revoke)
102
+ admits nothing -- combined with AND, the entity is rejected on that
103
+ dimension alone.
104
+ """
105
+
106
+ effective: frozenset[str]
107
+ constrains: bool
108
+
109
+
110
+ def compute_effective_multi(
111
+ dimensions: Iterable[DimensionPolicy],
112
+ ) -> list[DimensionResolution]:
113
+ """Resolve an ordered list of per-dimension policies.
114
+
115
+ For each dimension, returns:
116
+
117
+ - ``effective`` = ``(dept | grants) - revokes`` after cleaning.
118
+ - ``constrains`` = ``True`` if any of the three input iterables has at
119
+ least one cleaned entry; ``False`` otherwise.
120
+
121
+ The output list preserves input order, so callers can ``zip`` it with
122
+ their own per-dimension metadata (column references, display names) to
123
+ build the final filter.
124
+
125
+ Returns an empty list when ``dimensions`` is empty. Callers that want to
126
+ know "is this access fully unrestricted" should check
127
+ ``not any(r.constrains for r in result)``.
128
+ """
129
+ out: list[DimensionResolution] = []
130
+ for d in dimensions:
131
+ dept = _clean(d.dept_values)
132
+ grants = _clean(d.grants)
133
+ revokes = _clean(d.revokes)
134
+ constrains = bool(dept or grants or revokes)
135
+ effective = frozenset((dept | grants) - revokes)
136
+ out.append(DimensionResolution(effective=effective, constrains=constrains))
137
+ return out
138
+
139
+
140
+ def _clean(values: Iterable[str]) -> frozenset[str]:
141
+ return frozenset(v for v in values if isinstance(v, str) and v.strip())