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.
- coffeehouse_ui-0.9.3/LICENSE +21 -0
- coffeehouse_ui-0.9.3/PKG-INFO +185 -0
- coffeehouse_ui-0.9.3/README.md +130 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/__init__.py +36 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/permissions.py +141 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/static/css/coffeehouse-theme.css +314 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/static/css/coffeehouse.min.css +5 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/static/img/favicon.svg +5 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/static/js/csrf.js +35 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/static/js/culori.min.js +4 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/static/js/htmx.min.js +1 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/static/js/loading.js +91 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/static/js/permissions.js +155 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/static/js/theme.js +48 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/static/js/toast.js +88 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/templates/admin/macros/permissions.html +223 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/templates/admin/permissions_landing.html +67 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/app_switcher.html +36 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/confirm_modal.html +30 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/data_table.html +57 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/empty_state.html +32 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/flash.html +36 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/form_field.html +89 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/loading.html +34 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/navbar.html +94 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/page_header.html +38 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/pagination.html +52 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/sidebar.html +146 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/stale_roles_banner.html +31 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/templates/components/stat_card.html +17 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/templates/macros/icons.html +114 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/templates/macros/sortable.html +240 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui/templates/ui_base.html +143 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui.egg-info/PKG-INFO +185 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui.egg-info/SOURCES.txt +38 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui.egg-info/dependency_links.txt +1 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui.egg-info/requires.txt +11 -0
- coffeehouse_ui-0.9.3/coffeehouse_ui.egg-info/top_level.txt +1 -0
- coffeehouse_ui-0.9.3/pyproject.toml +173 -0
- 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())
|