stapel-docs 0.1.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.
Files changed (71) hide show
  1. stapel_docs-0.1.0/CONFIG.MD +65 -0
  2. stapel_docs-0.1.0/LICENSE +21 -0
  3. stapel_docs-0.1.0/PKG-INFO +193 -0
  4. stapel_docs-0.1.0/README.md +159 -0
  5. stapel_docs-0.1.0/__init__.py +31 -0
  6. stapel_docs-0.1.0/_capabilities.py +39 -0
  7. stapel_docs-0.1.0/_codegen.py +143 -0
  8. stapel_docs-0.1.0/_codegen_settings.py +132 -0
  9. stapel_docs-0.1.0/actions.py +100 -0
  10. stapel_docs-0.1.0/admin.py +53 -0
  11. stapel_docs-0.1.0/apps.py +30 -0
  12. stapel_docs-0.1.0/assets/DEJAVU-FONTS-LICENSE.txt +99 -0
  13. stapel_docs-0.1.0/assets/DejaVuSans-Bold.ttf +0 -0
  14. stapel_docs-0.1.0/assets/DejaVuSans.ttf +0 -0
  15. stapel_docs-0.1.0/authz.py +121 -0
  16. stapel_docs-0.1.0/checks.py +129 -0
  17. stapel_docs-0.1.0/codegen_urls.py +25 -0
  18. stapel_docs-0.1.0/conf.py +106 -0
  19. stapel_docs-0.1.0/conftest.py +107 -0
  20. stapel_docs-0.1.0/doc_types.py +163 -0
  21. stapel_docs-0.1.0/docs/capabilities.json +392 -0
  22. stapel_docs-0.1.0/docs/errors.json +450 -0
  23. stapel_docs-0.1.0/docs/flows.json +1 -0
  24. stapel_docs-0.1.0/docs/llms.txt +238 -0
  25. stapel_docs-0.1.0/dto.py +79 -0
  26. stapel_docs-0.1.0/errors.py +52 -0
  27. stapel_docs-0.1.0/events.py +72 -0
  28. stapel_docs-0.1.0/exporters.py +159 -0
  29. stapel_docs-0.1.0/functions.py +53 -0
  30. stapel_docs-0.1.0/gdpr.py +71 -0
  31. stapel_docs-0.1.0/management/__init__.py +0 -0
  32. stapel_docs-0.1.0/management/commands/__init__.py +0 -0
  33. stapel_docs-0.1.0/management/commands/docs_purge_expired.py +21 -0
  34. stapel_docs-0.1.0/migrations/0001_initial.py +126 -0
  35. stapel_docs-0.1.0/migrations/__init__.py +0 -0
  36. stapel_docs-0.1.0/models.py +216 -0
  37. stapel_docs-0.1.0/presenters.py +271 -0
  38. stapel_docs-0.1.0/py.typed +0 -0
  39. stapel_docs-0.1.0/pyproject.toml +73 -0
  40. stapel_docs-0.1.0/schemas/emits/document.created.json +16 -0
  41. stapel_docs-0.1.0/schemas/emits/document.deleted.json +12 -0
  42. stapel_docs-0.1.0/schemas/emits/document.storage_changed.json +12 -0
  43. stapel_docs-0.1.0/schemas/emits/document.updated.json +13 -0
  44. stapel_docs-0.1.0/schemas/functions/docs.create_document.json +18 -0
  45. stapel_docs-0.1.0/serializers.py +173 -0
  46. stapel_docs-0.1.0/services.py +817 -0
  47. stapel_docs-0.1.0/setup.cfg +4 -0
  48. stapel_docs-0.1.0/stapel_docs.egg-info/PKG-INFO +193 -0
  49. stapel_docs-0.1.0/stapel_docs.egg-info/SOURCES.txt +115 -0
  50. stapel_docs-0.1.0/stapel_docs.egg-info/dependency_links.txt +1 -0
  51. stapel_docs-0.1.0/stapel_docs.egg-info/requires.txt +11 -0
  52. stapel_docs-0.1.0/stapel_docs.egg-info/top_level.txt +1 -0
  53. stapel_docs-0.1.0/storage.py +380 -0
  54. stapel_docs-0.1.0/tests/test_checks.py +62 -0
  55. stapel_docs-0.1.0/tests/test_comm.py +149 -0
  56. stapel_docs-0.1.0/tests/test_content.py +178 -0
  57. stapel_docs-0.1.0/tests/test_contract.py +373 -0
  58. stapel_docs-0.1.0/tests/test_documents.py +240 -0
  59. stapel_docs-0.1.0/tests/test_export.py +77 -0
  60. stapel_docs-0.1.0/tests/test_folders.py +196 -0
  61. stapel_docs-0.1.0/tests/test_gdpr.py +119 -0
  62. stapel_docs-0.1.0/tests/test_ingest.py +117 -0
  63. stapel_docs-0.1.0/tests/test_public_api.py +46 -0
  64. stapel_docs-0.1.0/tests/test_revisions.py +183 -0
  65. stapel_docs-0.1.0/tests/test_storage_seam.py +58 -0
  66. stapel_docs-0.1.0/tests/test_trash.py +227 -0
  67. stapel_docs-0.1.0/tests/test_updates.py +178 -0
  68. stapel_docs-0.1.0/tests/test_uploads.py +135 -0
  69. stapel_docs-0.1.0/urls.py +16 -0
  70. stapel_docs-0.1.0/urls_v1.py +77 -0
  71. stapel_docs-0.1.0/views.py +855 -0
@@ -0,0 +1,65 @@
1
+ # CONFIG.MD — stapel-docs
2
+
3
+ Config registry for **stapel-docs** (`static-scaffold-and-config.md` §2).
4
+ One row per configuration key the module reads, its **source** (`env` = the
5
+ process environment / the `STAPEL_DOCS` settings dict), what it is for,
6
+ whether it is required, and its default.
7
+
8
+ All keys are read through `docs_settings`
9
+ (`stapel_docs.conf.AppSettings`, namespace `STAPEL_DOCS`).
10
+ Resolution order per key: `settings.STAPEL_DOCS` dict → a flat Django
11
+ setting of the same name → environment variable → the default below.
12
+ `STORAGE` is resolved with `import_string` (dotted path). `SHARING` is one
13
+ nested dict key; its sub-keys are listed as `SHARING.<KEY>` rows below.
14
+
15
+ ## stapel-docs
16
+
17
+ ### Storage seam & presigned URLs (source: env)
18
+
19
+ | Key | Source | Purpose | Required | Default |
20
+ |-----|--------|---------|----------|---------|
21
+ | STORAGE | env | dotted path to a DocsStorage implementation (REPLACE) — every byte of document content I/O goes through it. | no | stapel_docs.storage.DjangoStorageBackend |
22
+ | STORAGE_PREFIX | env | object-key prefix: `{STORAGE_PREFIX}/{workspace_id}/{document_id}/…`. | no | docs |
23
+ | UPLOAD_URL_EXPIRES_SECONDS | env | presigned PUT URL lifetime (upload tickets). | no | 900 |
24
+ | DOWNLOAD_URL_EXPIRES_SECONDS | env | presigned GET URL lifetime (downloads). | no | 3600 |
25
+
26
+ ### S3Backend (extra `[s3]`; read only when STORAGE points at it) (source: env)
27
+
28
+ | Key | Source | Purpose | Required | Default |
29
+ |-----|--------|---------|----------|---------|
30
+ | S3_ENDPOINT_URL | env | S3/MinIO endpoint URL (None = AWS default). | no | None |
31
+ | S3_PUBLIC_URL | env | public base URL substituted into presigned URLs (split-horizon deployments). | no | None |
32
+ | S3_ACCESS_KEY | env | access key id. | no | None |
33
+ | S3_SECRET_KEY | env | secret access key. | no | None |
34
+ | S3_REGION | env | bucket region. | no | us-east-1 |
35
+ | S3_BUCKET | env | bucket name. | no | stapel-docs |
36
+ | S3_CONNECT_TIMEOUT | env | botocore connect timeout (seconds) — tight on purpose; a fast failure beats a spinner. | no | 5 |
37
+ | S3_READ_TIMEOUT | env | botocore read timeout (seconds). | no | 15 |
38
+ | S3_MAX_ATTEMPTS | env | botocore retry ceiling. | no | 2 |
39
+
40
+ ### Registries & seams (source: env)
41
+
42
+ | Key | Source | Purpose | Required | Default |
43
+ |-----|--------|---------|----------|---------|
44
+ | DOC_TYPES | env | document-type registry overlay merged OVER the txt/md/csv/file builtins ({slug: dotted-path to DocTypeSpec \| None removes}). | no | {} |
45
+ | EXPORTERS | env | export-format registry overlay merged OVER the built-in pdf ({format: dotted-path \| None removes}). | no | {} |
46
+ | INGEST | env | event-driven ingest ({action_name: dotted-path mapper payload → create_document kwargs}); broken entries raise ImproperlyConfigured at wiring. | no | {} |
47
+
48
+ ### Journal / revisions / tree / trash (source: env)
49
+
50
+ | Key | Source | Purpose | Required | Default |
51
+ |-----|--------|---------|----------|---------|
52
+ | REPLAY_WINDOW | env | journal rows with seq ≤ snapshot_seq − window are compacted away (chat-pattern replay window). | no | 500 |
53
+ | AUTO_REVISION_INTERVAL_SECONDS | env | a snapshot save mints an `auto` Revision when the newest revision is older than this (0 = revision on every save). | no | 300 |
54
+ | FOLDER_MAX_DEPTH | env | folder tree depth ceiling (create/move/ingest-path all enforce it). | no | 10 |
55
+ | TRASH_RETENTION_DAYS | env | soft-deleted items become purgeable by `docs_purge_expired` after this many days; purge is irreversible. | no | 30 |
56
+
57
+ ### Sharing axis (closed defaults; v1 system-check guards E010-E014) (source: env)
58
+
59
+ | Key | Source | Purpose | Required | Default |
60
+ |-----|--------|---------|----------|---------|
61
+ | SHARING.MODES | env | additional grant sources over the immutable workspace baseline (subset of whitelist \| link). v1 implements neither — non-empty is a system-check ERROR. | no | [] |
62
+ | SHARING.RESOLVERS | env | {ref_kind: dotted-path} whitelist subject-reference resolvers — real-but-empty seam; entries are import-validated at check time (E014). | no | {} |
63
+ | SHARING.LINK.ANONYMOUS | env | anonymous link redemption. True in v1 = system-check ERROR (E012). | no | false |
64
+ | SHARING.LINK.MAX_LEVEL | env | ceiling for minted link level. Above `view` in v1 = system-check ERROR (E013); `manage` is never grantable by any share source. | no | view |
65
+ | SHARING.LINK.TTL_DAYS | env | link TTL; None (perpetual) only ever by explicit host choice. | no | 30 |
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stapel contributors
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 OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,193 @@
1
+ Metadata-Version: 2.4
2
+ Name: stapel-docs
3
+ Version: 0.1.0
4
+ Summary: Documents: storage, revisions and per-type editors for the Stapel framework
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/usestapel/stapel-docs
7
+ Project-URL: Repository, https://github.com/usestapel/stapel-docs
8
+ Project-URL: Documentation, https://github.com/usestapel/stapel-docs#readme
9
+ Project-URL: Changelog, https://github.com/usestapel/stapel-docs/blob/main/CHANGELOG.md
10
+ Project-URL: Issues, https://github.com/usestapel/stapel-docs/issues
11
+ Keywords: django,stapel,docs
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Framework :: Django
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.11
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Requires-Dist: stapel-core<0.4,>=0.3.0
26
+ Provides-Extra: s3
27
+ Requires-Dist: boto3>=1.26; extra == "s3"
28
+ Provides-Extra: pdf
29
+ Requires-Dist: fpdf2>=2.8; extra == "pdf"
30
+ Provides-Extra: all
31
+ Requires-Dist: boto3>=1.26; extra == "all"
32
+ Requires-Dist: fpdf2>=2.8; extra == "all"
33
+ Dynamic: license-file
34
+
35
+ <!-- Generated by stapel-readme from docs/readme.md + docs/*.json. Do not edit this file; edit docs/readme.md and re-run `make readme`. -->
36
+
37
+ # stapel-docs
38
+
39
+ [![CI](https://img.shields.io/github/actions/workflow/status/usestapel/stapel-docs/ci.yml?branch=main&logo=github&label=CI)](https://github.com/usestapel/stapel-docs/actions/workflows/ci.yml?query=branch%3Amain)
40
+ [![coverage](https://img.shields.io/codecov/c/github/usestapel/stapel-docs?branch=main&logo=codecov&label=coverage)](https://app.codecov.io/gh/usestapel/stapel-docs)
41
+ [![pypi](https://img.shields.io/pypi/v/stapel-docs?logo=pypi&logoColor=white&label=pypi)](https://pypi.org/project/stapel-docs/)
42
+ [![downloads](https://static.pepy.tech/badge/stapel-docs/month)](https://pepy.tech/project/stapel-docs)
43
+ [![python](https://img.shields.io/pypi/pyversions/stapel-docs?logo=python&logoColor=white)](https://pypi.org/project/stapel-docs/)
44
+ [![license](https://img.shields.io/github/license/usestapel/stapel-docs)](https://github.com/usestapel/stapel-docs/blob/main/LICENSE)
45
+ [![llms.txt](https://img.shields.io/badge/llms.txt-blue)](https://github.com/usestapel/stapel-docs/blob/main/docs/llms.txt)
46
+
47
+ > Google-Drive-style workspace documents: a folder tree, one-entity-per-type documents over an open type registry, content-addressed snapshot storage with an update journal and revision history, optimistic-lock editing (v1), trash with irreversible purge, a swappable object-storage seam, comm ingest (docs.create_document) and registry-driven export (PDF built in).
48
+
49
+ Part of the [Stapel framework](https://github.com/usestapel) — composable Django apps that deploy as a monolith or as microservices without changing module code.
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ pip install stapel-docs
55
+ ```
56
+
57
+ ## At a glance
58
+
59
+ | Fact | Value |
60
+ |---|---|
61
+ | Version | `0.1.0` |
62
+ | Python | `>=3.11` (3.11, 3.12, 3.13) |
63
+ | HTTP operations | 27 |
64
+ | Config axes | 1 |
65
+ | Usage surface | 47 |
66
+ | Extension points | 7 |
67
+ | Error codes | 60 |
68
+ | Fleet dependencies | [`stapel-auth`](https://github.com/usestapel/stapel-auth) (optional) · [`stapel-core`](https://github.com/usestapel/stapel-core) · [`stapel-workspaces`](https://github.com/usestapel/stapel-workspaces) (optional) |
69
+
70
+ ## Documentation
71
+
72
+ [OpenAPI](https://github.com/usestapel/stapel-docs/blob/main/docs/schema.json) · [capabilities.json](https://github.com/usestapel/stapel-docs/blob/main/docs/capabilities.json) · [llms.txt (for agents)](https://github.com/usestapel/stapel-docs/blob/main/docs/llms.txt)
73
+
74
+ ## What this is
75
+
76
+ Google-Drive-style **workspace documents**: a folder tree, documents that are
77
+ each ONE entity with a `type` from an open registry (`txt` / `md` / `csv` /
78
+ opaque `file` built in), a journal + revision versioning substrate, trash with
79
+ irreversible purge, and registry-driven export (PDF built in).
80
+
81
+ The versioning substrate is decided for both collaboration disciplines:
82
+ **snapshot** types save whole states under optimistic lock (`If-Match` carries
83
+ the client's `head_seq` — v1's editing model), **crdt** types accumulate an
84
+ append-only update journal between snapshots with chat-pattern replay/resync.
85
+ Which discipline applies is a property of the *type*, not the request.
86
+
87
+ Object storage is content-addressed and goes through a swappable seam
88
+ (`STORAGE`); the library is **body-blind** — the storage substrate never
89
+ parses a document body, only a type's own `text_extractor` may.
90
+
91
+ ## Quick start
92
+
93
+ The base install rides Django's `default_storage`; add extras for the
94
+ boto3 S3/MinIO backend and the PDF exporter:
95
+
96
+ ```bash
97
+ pip install "stapel-docs[s3,pdf]"
98
+ ```
99
+
100
+ ```python
101
+ INSTALLED_APPS = [
102
+ # ...
103
+ "stapel_docs",
104
+ ]
105
+
106
+ # urls.py
107
+ path("docs/", include("stapel_docs.urls")) # -> /docs/api/v1/...
108
+ ```
109
+
110
+ Authorization asks the `workspaces.check_capability` comm Function
111
+ (fail-closed, deny-by-default) — install stapel-workspaces or provide that
112
+ Function for any HTTP request to be allowed.
113
+
114
+ ## An open type registry, not an enum
115
+
116
+ ```python
117
+ STAPEL_DOCS = {
118
+ # Add or replace document types ({slug: dotted-path | None removes}):
119
+ "DOC_TYPES": {"sheet": "myproject.docs.SHEET_SPEC"},
120
+ # Add export formats over the built-in pdf:
121
+ "EXPORTERS": {"docx": "myproject.docs.DocxExporter"},
122
+ # Event-driven ingest without writing a subscriber:
123
+ "INGEST": {"meeting.summarized": "myproject.docs.map_summary"},
124
+ # Swap the object store:
125
+ "STORAGE": "stapel_docs.storage.S3Backend",
126
+ }
127
+ ```
128
+
129
+ A type whose spec vanishes from the registry degrades to `file` behavior —
130
+ read-only, never unreadable: revisions still list, snapshots still download,
131
+ trash/purge/export still work.
132
+
133
+ ## Ingest
134
+
135
+ Product glue dumps content in with one comm call — no HTTP, no import:
136
+
137
+ ```python
138
+ call("docs.create_document", {
139
+ "workspace_id": ws_id, "type": "md", "title": "Weekly sync",
140
+ "body": summary_text, "folder_path": "/Meetings/2026-08",
141
+ })
142
+ ```
143
+
144
+ `folder_path` materializes folders idempotently; an unknown `type` refuses
145
+ loudly so content never silently lands under a mistyped slug.
146
+
147
+ ## Sharing (v1: closed by default)
148
+
149
+ The sharing axis (`SHARING`: whitelist / link modes) ships its config surface
150
+ with **closed defaults** — v1 implements exactly the immutable workspace
151
+ baseline, and opening any sharing knob before the mechanism exists is a loud
152
+ system-check error (`stapel_docs.E010-E013`), never a silent no-op.
153
+
154
+ ## Settings
155
+
156
+ All configuration lives in the `STAPEL_DOCS` namespace (dict setting, flat
157
+ setting, or env var — resolved lazily). Full table in
158
+ [CONFIG.MD](https://github.com/usestapel/stapel-docs/blob/main/CONFIG.MD);
159
+ seam semantics in
160
+ [MODULE.md](https://github.com/usestapel/stapel-docs/blob/main/MODULE.md).
161
+ Highlights: `STORAGE`, `DOC_TYPES`, `EXPORTERS`, `INGEST`, `REPLAY_WINDOW`,
162
+ `AUTO_REVISION_INTERVAL_SECONDS`, `TRASH_RETENTION_DAYS`, `SHARING`.
163
+
164
+ ## comm surface
165
+
166
+ | Kind | Name | Contract |
167
+ |---|---|---|
168
+ | Function (provides) | `docs.create_document` | `schemas/functions/docs.create_document.json` |
169
+ | Action (emit) | `document.created`, `document.updated`, `document.deleted`, `document.storage_changed` | `schemas/emits/*.json` |
170
+ | Action (consume) | `user.deleted` | GDPR anonymize (authorship nulled, content survives) |
171
+ | Function (call) | `workspaces.check_capability` | provided by stapel-workspaces |
172
+
173
+ ## Operations
174
+
175
+ ```bash
176
+ python manage.py docs_purge_expired # purge trash older than TRASH_RETENTION_DAYS
177
+ ```
178
+
179
+ ## Development
180
+
181
+ ```bash
182
+ pip install -e . && pip install pytest pytest-django ruff jsonschema djangorestframework
183
+ ./setup-hooks.sh
184
+ pytest tests/
185
+ ```
186
+
187
+ ## License
188
+
189
+ MIT — see [LICENSE](https://github.com/usestapel/stapel-docs/blob/main/LICENSE).
190
+
191
+ ---
192
+
193
+ <sub>This page is assembled by `stapel-readme` from `docs/readme.md` plus the contract artifacts in `docs/`. Edit the prose in `docs/readme.md`; the badges, facts and links above and below it are generated — do not hand-edit `README.md`.</sub>
@@ -0,0 +1,159 @@
1
+ <!-- Generated by stapel-readme from docs/readme.md + docs/*.json. Do not edit this file; edit docs/readme.md and re-run `make readme`. -->
2
+
3
+ # stapel-docs
4
+
5
+ [![CI](https://img.shields.io/github/actions/workflow/status/usestapel/stapel-docs/ci.yml?branch=main&logo=github&label=CI)](https://github.com/usestapel/stapel-docs/actions/workflows/ci.yml?query=branch%3Amain)
6
+ [![coverage](https://img.shields.io/codecov/c/github/usestapel/stapel-docs?branch=main&logo=codecov&label=coverage)](https://app.codecov.io/gh/usestapel/stapel-docs)
7
+ [![pypi](https://img.shields.io/pypi/v/stapel-docs?logo=pypi&logoColor=white&label=pypi)](https://pypi.org/project/stapel-docs/)
8
+ [![downloads](https://static.pepy.tech/badge/stapel-docs/month)](https://pepy.tech/project/stapel-docs)
9
+ [![python](https://img.shields.io/pypi/pyversions/stapel-docs?logo=python&logoColor=white)](https://pypi.org/project/stapel-docs/)
10
+ [![license](https://img.shields.io/github/license/usestapel/stapel-docs)](https://github.com/usestapel/stapel-docs/blob/main/LICENSE)
11
+ [![llms.txt](https://img.shields.io/badge/llms.txt-blue)](https://github.com/usestapel/stapel-docs/blob/main/docs/llms.txt)
12
+
13
+ > Google-Drive-style workspace documents: a folder tree, one-entity-per-type documents over an open type registry, content-addressed snapshot storage with an update journal and revision history, optimistic-lock editing (v1), trash with irreversible purge, a swappable object-storage seam, comm ingest (docs.create_document) and registry-driven export (PDF built in).
14
+
15
+ Part of the [Stapel framework](https://github.com/usestapel) — composable Django apps that deploy as a monolith or as microservices without changing module code.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install stapel-docs
21
+ ```
22
+
23
+ ## At a glance
24
+
25
+ | Fact | Value |
26
+ |---|---|
27
+ | Version | `0.1.0` |
28
+ | Python | `>=3.11` (3.11, 3.12, 3.13) |
29
+ | HTTP operations | 27 |
30
+ | Config axes | 1 |
31
+ | Usage surface | 47 |
32
+ | Extension points | 7 |
33
+ | Error codes | 60 |
34
+ | Fleet dependencies | [`stapel-auth`](https://github.com/usestapel/stapel-auth) (optional) · [`stapel-core`](https://github.com/usestapel/stapel-core) · [`stapel-workspaces`](https://github.com/usestapel/stapel-workspaces) (optional) |
35
+
36
+ ## Documentation
37
+
38
+ [OpenAPI](https://github.com/usestapel/stapel-docs/blob/main/docs/schema.json) · [capabilities.json](https://github.com/usestapel/stapel-docs/blob/main/docs/capabilities.json) · [llms.txt (for agents)](https://github.com/usestapel/stapel-docs/blob/main/docs/llms.txt)
39
+
40
+ ## What this is
41
+
42
+ Google-Drive-style **workspace documents**: a folder tree, documents that are
43
+ each ONE entity with a `type` from an open registry (`txt` / `md` / `csv` /
44
+ opaque `file` built in), a journal + revision versioning substrate, trash with
45
+ irreversible purge, and registry-driven export (PDF built in).
46
+
47
+ The versioning substrate is decided for both collaboration disciplines:
48
+ **snapshot** types save whole states under optimistic lock (`If-Match` carries
49
+ the client's `head_seq` — v1's editing model), **crdt** types accumulate an
50
+ append-only update journal between snapshots with chat-pattern replay/resync.
51
+ Which discipline applies is a property of the *type*, not the request.
52
+
53
+ Object storage is content-addressed and goes through a swappable seam
54
+ (`STORAGE`); the library is **body-blind** — the storage substrate never
55
+ parses a document body, only a type's own `text_extractor` may.
56
+
57
+ ## Quick start
58
+
59
+ The base install rides Django's `default_storage`; add extras for the
60
+ boto3 S3/MinIO backend and the PDF exporter:
61
+
62
+ ```bash
63
+ pip install "stapel-docs[s3,pdf]"
64
+ ```
65
+
66
+ ```python
67
+ INSTALLED_APPS = [
68
+ # ...
69
+ "stapel_docs",
70
+ ]
71
+
72
+ # urls.py
73
+ path("docs/", include("stapel_docs.urls")) # -> /docs/api/v1/...
74
+ ```
75
+
76
+ Authorization asks the `workspaces.check_capability` comm Function
77
+ (fail-closed, deny-by-default) — install stapel-workspaces or provide that
78
+ Function for any HTTP request to be allowed.
79
+
80
+ ## An open type registry, not an enum
81
+
82
+ ```python
83
+ STAPEL_DOCS = {
84
+ # Add or replace document types ({slug: dotted-path | None removes}):
85
+ "DOC_TYPES": {"sheet": "myproject.docs.SHEET_SPEC"},
86
+ # Add export formats over the built-in pdf:
87
+ "EXPORTERS": {"docx": "myproject.docs.DocxExporter"},
88
+ # Event-driven ingest without writing a subscriber:
89
+ "INGEST": {"meeting.summarized": "myproject.docs.map_summary"},
90
+ # Swap the object store:
91
+ "STORAGE": "stapel_docs.storage.S3Backend",
92
+ }
93
+ ```
94
+
95
+ A type whose spec vanishes from the registry degrades to `file` behavior —
96
+ read-only, never unreadable: revisions still list, snapshots still download,
97
+ trash/purge/export still work.
98
+
99
+ ## Ingest
100
+
101
+ Product glue dumps content in with one comm call — no HTTP, no import:
102
+
103
+ ```python
104
+ call("docs.create_document", {
105
+ "workspace_id": ws_id, "type": "md", "title": "Weekly sync",
106
+ "body": summary_text, "folder_path": "/Meetings/2026-08",
107
+ })
108
+ ```
109
+
110
+ `folder_path` materializes folders idempotently; an unknown `type` refuses
111
+ loudly so content never silently lands under a mistyped slug.
112
+
113
+ ## Sharing (v1: closed by default)
114
+
115
+ The sharing axis (`SHARING`: whitelist / link modes) ships its config surface
116
+ with **closed defaults** — v1 implements exactly the immutable workspace
117
+ baseline, and opening any sharing knob before the mechanism exists is a loud
118
+ system-check error (`stapel_docs.E010-E013`), never a silent no-op.
119
+
120
+ ## Settings
121
+
122
+ All configuration lives in the `STAPEL_DOCS` namespace (dict setting, flat
123
+ setting, or env var — resolved lazily). Full table in
124
+ [CONFIG.MD](https://github.com/usestapel/stapel-docs/blob/main/CONFIG.MD);
125
+ seam semantics in
126
+ [MODULE.md](https://github.com/usestapel/stapel-docs/blob/main/MODULE.md).
127
+ Highlights: `STORAGE`, `DOC_TYPES`, `EXPORTERS`, `INGEST`, `REPLAY_WINDOW`,
128
+ `AUTO_REVISION_INTERVAL_SECONDS`, `TRASH_RETENTION_DAYS`, `SHARING`.
129
+
130
+ ## comm surface
131
+
132
+ | Kind | Name | Contract |
133
+ |---|---|---|
134
+ | Function (provides) | `docs.create_document` | `schemas/functions/docs.create_document.json` |
135
+ | Action (emit) | `document.created`, `document.updated`, `document.deleted`, `document.storage_changed` | `schemas/emits/*.json` |
136
+ | Action (consume) | `user.deleted` | GDPR anonymize (authorship nulled, content survives) |
137
+ | Function (call) | `workspaces.check_capability` | provided by stapel-workspaces |
138
+
139
+ ## Operations
140
+
141
+ ```bash
142
+ python manage.py docs_purge_expired # purge trash older than TRASH_RETENTION_DAYS
143
+ ```
144
+
145
+ ## Development
146
+
147
+ ```bash
148
+ pip install -e . && pip install pytest pytest-django ruff jsonschema djangorestframework
149
+ ./setup-hooks.sh
150
+ pytest tests/
151
+ ```
152
+
153
+ ## License
154
+
155
+ MIT — see [LICENSE](https://github.com/usestapel/stapel-docs/blob/main/LICENSE).
156
+
157
+ ---
158
+
159
+ <sub>This page is assembled by `stapel-readme` from `docs/readme.md` plus the contract artifacts in `docs/`. Edit the prose in `docs/readme.md`; the badges, facts and links above and below it are generated — do not hand-edit `README.md`.</sub>
@@ -0,0 +1,31 @@
1
+ """stapel-docs — Documents: storage, revisions and per-type editors for the Stapel framework.
2
+
3
+ Public API (lazily exported, PEP 562 — importing this package never pulls
4
+ in Django or requires configured settings):
5
+
6
+ - ``docs_settings`` — resolved app settings (``stapel_docs.conf``).
7
+ """
8
+
9
+ __all__ = [
10
+ "docs_settings",
11
+ ]
12
+
13
+ # name -> submodule that defines it. Resolution is deferred until first
14
+ # attribute access so that `import stapel_docs` stays Django-free.
15
+ _LAZY_EXPORTS = {
16
+ "docs_settings": ".conf",
17
+ }
18
+
19
+
20
+ def __getattr__(name):
21
+ if name in _LAZY_EXPORTS:
22
+ from importlib import import_module
23
+
24
+ value = getattr(import_module(_LAZY_EXPORTS[name], __name__), name)
25
+ globals()[name] = value # cache for subsequent lookups
26
+ return value
27
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
28
+
29
+
30
+ def __dir__():
31
+ return sorted(set(globals()) | set(__all__))
@@ -0,0 +1,39 @@
1
+ """stapel-docs capabilities.json emitter — thin shim over stapel_tools.capabilities."""
2
+ from pathlib import Path
3
+
4
+ from stapel_tools.capabilities import axis_group_rules, run_capabilities_cli
5
+
6
+
7
+ def main(argv=None):
8
+ from stapel_docs._codegen import _configure
9
+
10
+ _configure()
11
+ from stapel_docs.conf import DEFAULTS
12
+ from stapel_docs.urls_v1 import GATE_REGISTRY
13
+
14
+ # SHARING is the one CTO-facing axis: does the product grant access
15
+ # beyond the immutable workspace baseline (MODES / LINK.* — closed by
16
+ # default, v1 system checks E010-E013 refuse opening it before the
17
+ # mechanism exists). The emitter expresses axes as top-level DEFAULTS
18
+ # keys only, so the nested MODES/LINK.* knobs surface as ONE composite
19
+ # SHARING axis (derived kind "enum", default = the closed dict) rather
20
+ # than per-key axes — honest within the mechanism, detailed in the
21
+ # curated summary. STORAGE/DOC_TYPES/EXPORTERS/INGEST/SHARING RESOLVERS
22
+ # are extension seams (curated in docs/capabilities.meta.json);
23
+ # timeouts, URL lifetimes, REPLAY_WINDOW, AUTO_REVISION_INTERVAL,
24
+ # FOLDER_MAX_DEPTH, TRASH_RETENTION_DAYS and the S3_* block are tuning
25
+ # — neither axes nor extension points.
26
+ return run_capabilities_cli(
27
+ argv,
28
+ repo=Path(__file__).resolve().parent,
29
+ canonical_prefix="/docs/api/v1",
30
+ defaults=DEFAULTS,
31
+ registry=GATE_REGISTRY,
32
+ is_axis=lambda k: k == "SHARING",
33
+ axis_group=axis_group_rules(exact={"SHARING": "docs.sharing"}),
34
+ prog="stapel-docs-capabilities",
35
+ )
36
+
37
+
38
+ if __name__ == "__main__":
39
+ raise SystemExit(main())
@@ -0,0 +1,143 @@
1
+ """stapel-docs contract-emission harness (contract-pipeline.md §2-3).
2
+
3
+ Emits the module's own contract triad into ``docs/`` from a single-module
4
+ ``{docs + core}`` Django instance mounted at the canonical ``/docs/api/v1``
5
+ prefix:
6
+
7
+ docs/schema.json drf-spectacular OpenAPI, this module only, canonical prefix
8
+ docs/flows.json generate_flow_docs machine artifact — empty here, this
9
+ module has no ``@flow_step`` annotations
10
+ docs/errors.json generate_error_keys registry (the per-module etalon)
11
+
12
+ Copied from stapel-recordings' adaptation of the stapel-auth reference
13
+ implementation; the *mechanism* is stapel_tools.codegen (unchanged, shared),
14
+ this file is the thin per-module *config* that wires the module's settings +
15
+ canonical mount into it.
16
+
17
+ Like recordings, stapel-docs is **not mounted in stapel-example-monolith**
18
+ (grep-confirmed: no route for it in ``svc-app/core/urls.py``), so there is no
19
+ monolith aggregate slice to diff this artifact against for byte-identity.
20
+ Validation is standalone instead — see ``tests/test_contract.py``.
21
+
22
+ Usage:
23
+ python -m stapel_docs._codegen --out docs # `make contract`
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import argparse
28
+ import os
29
+ import sys
30
+ from pathlib import Path
31
+
32
+
33
+ def _configure() -> None:
34
+ """Configure + boot the single-module Django instance for emission."""
35
+ # `python -m` prepends cwd to sys.path; strip the repo root the same way
36
+ # a flat-layout conftest does (defensively — docs has no colliding
37
+ # subpackage today, but the guard costs nothing and keeps the harness
38
+ # identical in shape to auth/profiles/recordings).
39
+ repo_root = os.path.dirname(os.path.abspath(__file__))
40
+ sys.path[:] = [p for p in sys.path if os.path.abspath(p or os.getcwd()) != repo_root]
41
+
42
+ from django.conf import settings
43
+
44
+ if not settings.configured:
45
+ from stapel_docs._codegen_settings import settings_kwargs
46
+
47
+ settings.configure(
48
+ **settings_kwargs(root_urlconf="stapel_docs.codegen_urls", contract=True)
49
+ )
50
+
51
+ import django
52
+
53
+ django.setup()
54
+
55
+ # drf-spectacular froze its settings singleton at import time (before this
56
+ # harness ran configure()), so it is on drf defaults — the same state
57
+ # every other pair-backend's harness emits under. The one knob to force is
58
+ # SCHEMA_PATH_PREFIX: left None, drf derives the operationId prefix from
59
+ # the common path of all endpoints — "/" across a multi-module aggregate
60
+ # (operationIds keep the mount segment, docs_api_*), but "/docs/api" in a
61
+ # single-module harness (which would strip it to bare anonymous names).
62
+ # Pin it to the aggregate convention so the operationIds match every other
63
+ # module's harness; SCHEMA_PATH_PREFIX_TRIM stays False (default) so the
64
+ # path *keys* keep /docs/api/v1/ on both sides.
65
+ from drf_spectacular.settings import spectacular_settings
66
+
67
+ from stapel_docs._codegen_settings import CODEGEN_SCHEMA_PATH_PREFIX
68
+
69
+ spectacular_settings.SCHEMA_PATH_PREFIX = CODEGEN_SCHEMA_PATH_PREFIX
70
+
71
+ # A real all-modules deployment registers drf-spectacular's JWT-cookie
72
+ # security-scheme extension as a side effect of its own dev-only Swagger
73
+ # URLs (DJANGO_ENV=local) — a *global* registration on drf-spectacular's
74
+ # extension registry, not tied to any one module's urls.py. stapel-docs
75
+ # has no co-mounted sibling to trigger it (the profiles finding —
76
+ # contract-pipeline.md brief), so without registering it explicitly here,
77
+ # docs' protected endpoints (every view is
78
+ # ``permission_classes = [IsNotAnonymousUser]``) would emit without their
79
+ # `security: [{"JWTCookieAuth": []}]` entry.
80
+ from stapel_core.django.openapi.swagger import _register_jwt_auth_extension
81
+
82
+ _register_jwt_auth_extension()
83
+
84
+
85
+ def _require_python_312() -> None:
86
+ """Abort emission if not running the pinned 3.12 interpreter.
87
+
88
+ drf-spectacular's rendering of component descriptions (``Optional[X]`` vs
89
+ ``X | None``) depends on the Python **minor** version — contracts emitted
90
+ on anything other than 3.12 (the CI/monolith pin) produce false diffs
91
+ against the committed docs/*.json. Emission must never proceed on the
92
+ wrong minor.
93
+ """
94
+ if sys.version_info[:2] != (3, 12):
95
+ got = f"{sys.version_info.major}.{sys.version_info.minor}"
96
+ raise SystemExit(
97
+ f"stapel-docs contract emission ABORTED: running Python {got}, "
98
+ "but contracts must be emitted on Python 3.12 (the CI/monolith "
99
+ "pin). drf-spectacular renders component descriptions "
100
+ "(Optional[X] vs X | None) differently across Python minor "
101
+ "versions, so emitting on any other minor produces false diffs "
102
+ "against the committed docs/*.json. Re-run under a 3.12 "
103
+ "interpreter."
104
+ )
105
+
106
+
107
+ def main(argv: list[str] | None = None) -> int:
108
+ _require_python_312()
109
+
110
+ parser = argparse.ArgumentParser(
111
+ prog="stapel-docs-contract",
112
+ description="Emit this module's contract triad (schema.json + flows.json "
113
+ "+ errors.json) into --out, canonical /docs/api/v1 prefix.",
114
+ )
115
+ parser.add_argument(
116
+ "--out",
117
+ default="docs",
118
+ help="Output directory for the triad (default: docs).",
119
+ )
120
+ args = parser.parse_args(argv)
121
+
122
+ _configure()
123
+
124
+ # Reuse the shared mechanism's byte-stable emitters (contract-pipeline.md §2:
125
+ # "the single-module harness already exists").
126
+ from stapel_tools.codegen import emit_errors, emit_flows, emit_schema
127
+
128
+ out = Path(args.out)
129
+ out.mkdir(parents=True, exist_ok=True)
130
+ paths = emit_schema(out / "schema.json")
131
+ flows = emit_flows(out / "flows.json")
132
+ errors = emit_errors(out / "errors.json")
133
+
134
+ print(
135
+ f"stapel-docs contract: {paths} paths, {flows} flows, {errors} error keys "
136
+ f"→ {out}/",
137
+ file=sys.stderr,
138
+ )
139
+ return 0
140
+
141
+
142
+ if __name__ == "__main__":
143
+ raise SystemExit(main())