cal-docs-server 3.0.0b1__py3-none-any.whl
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.
- cal_docs_server/.gitignore +1 -0
- cal_docs_server/__init__.py +1 -0
- cal_docs_server/__main__.py +64 -0
- cal_docs_server/_version.py +4 -0
- cal_docs_server/api.py +643 -0
- cal_docs_server/async_file.py +112 -0
- cal_docs_server/auth.py +253 -0
- cal_docs_server/cache_render_index.py +106 -0
- cal_docs_server/index_docs.py +449 -0
- cal_docs_server/main.py +191 -0
- cal_docs_server/render_index.py +174 -0
- cal_docs_server/render_md.py +90 -0
- cal_docs_server/resources/__init__.py +1 -0
- cal_docs_server/resources/help.md +171 -0
- cal_docs_server/resources/index_template.html +612 -0
- cal_docs_server/resources/md_template.html +244 -0
- cal_docs_server/resources/openapi.yaml +281 -0
- cal_docs_server/version.py +38 -0
- cal_docs_server/web_server.py +217 -0
- cal_docs_server-3.0.0b1.dist-info/METADATA +12 -0
- cal_docs_server-3.0.0b1.dist-info/RECORD +24 -0
- cal_docs_server-3.0.0b1.dist-info/WHEEL +4 -0
- cal_docs_server-3.0.0b1.dist-info/entry_points.txt +2 -0
- cal_docs_server-3.0.0b1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------------------------
|
|
2
|
+
# render_index
|
|
3
|
+
# ------------
|
|
4
|
+
#
|
|
5
|
+
# Renders the `IndexList` produced in `index_docs.make_index` into an HTML page
|
|
6
|
+
#
|
|
7
|
+
# License
|
|
8
|
+
# -------
|
|
9
|
+
# MIT License - Copyright 2025-2026 Cyber Assessment Labs
|
|
10
|
+
#
|
|
11
|
+
# Authors
|
|
12
|
+
# -------
|
|
13
|
+
# bena
|
|
14
|
+
#
|
|
15
|
+
# Version History
|
|
16
|
+
# ---------------
|
|
17
|
+
# Mar 2024 - Created
|
|
18
|
+
# Dec 2025 - New version 2
|
|
19
|
+
# ----------------------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
# ----------------------------------------------------------------------------------------
|
|
22
|
+
# Imports
|
|
23
|
+
# ----------------------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
import importlib.resources
|
|
26
|
+
import os
|
|
27
|
+
import string
|
|
28
|
+
from typing import TYPE_CHECKING
|
|
29
|
+
from . import async_file
|
|
30
|
+
from . import index_docs
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from .index_docs import IndexItemDict
|
|
34
|
+
from .index_docs import IndexList
|
|
35
|
+
|
|
36
|
+
# ----------------------------------------------------------------------------------------
|
|
37
|
+
# Functions
|
|
38
|
+
# ----------------------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
g_template_file: str | None = None
|
|
41
|
+
|
|
42
|
+
# ----------------------------------------------------------------------------------------
|
|
43
|
+
# Functions
|
|
44
|
+
# ----------------------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ----------------------------------------------------------------------------------------
|
|
48
|
+
async def render_index(
|
|
49
|
+
root_directory: str, server_name: str = "Documents Server"
|
|
50
|
+
) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Builds index from `root_directory` and renders it into HTML
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
index_list: IndexList = await index_docs.make_index(root_directory=root_directory)
|
|
56
|
+
return await render_index_from_list(index_list, server_name)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ----------------------------------------------------------------------------------------
|
|
60
|
+
async def render_index_from_list(
|
|
61
|
+
index_list: IndexList, server_name: str = "Documents Server"
|
|
62
|
+
) -> str:
|
|
63
|
+
"""
|
|
64
|
+
Renders an already-built index list into HTML
|
|
65
|
+
"""
|
|
66
|
+
template: str = await _get_template()
|
|
67
|
+
|
|
68
|
+
content = await _create_generated_content(index_list)
|
|
69
|
+
template_subs = {"INSERT_DATA": content, "SERVER_NAME": server_name}
|
|
70
|
+
html = string.Template(template).substitute(template_subs)
|
|
71
|
+
|
|
72
|
+
return html
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ----------------------------------------------------------------------------------------
|
|
76
|
+
# Private Functions
|
|
77
|
+
# ----------------------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ----------------------------------------------------------------------------------------
|
|
81
|
+
async def _get_template() -> str:
|
|
82
|
+
"""
|
|
83
|
+
Reads the template HTML file. It is stored as a resource within the server.
|
|
84
|
+
Caches it so it only reads once.
|
|
85
|
+
"""
|
|
86
|
+
global g_template_file
|
|
87
|
+
|
|
88
|
+
if not g_template_file:
|
|
89
|
+
file_folder = importlib.resources.files("cal_docs_server.resources")
|
|
90
|
+
file_path: str = os.path.join(str(file_folder), "index_template.html")
|
|
91
|
+
async with async_file.open_text(file_path, "rt") as f:
|
|
92
|
+
g_template_file = await f.read()
|
|
93
|
+
return g_template_file
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# ----------------------------------------------------------------------------------------
|
|
97
|
+
async def _create_generated_content(index_list: IndexList) -> str:
|
|
98
|
+
"""
|
|
99
|
+
Takes the json string of the scan and produces the html content from it. This does not
|
|
100
|
+
include the template this is the "middle" section that gets inserted into the template
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
text: str = ""
|
|
104
|
+
index_item: IndexItemDict
|
|
105
|
+
for index_item in index_list:
|
|
106
|
+
item_text = await _single_item_content(index_item)
|
|
107
|
+
text += item_text
|
|
108
|
+
return text
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ----------------------------------------------------------------------------------------
|
|
112
|
+
async def _single_item_content(index_item: IndexItemDict) -> str:
|
|
113
|
+
"""
|
|
114
|
+
Generates the HTML for a specific index item
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
# Build the card HTML
|
|
118
|
+
text = '<div class="doc-card">\n'
|
|
119
|
+
text += ' <div class="doc-header">\n'
|
|
120
|
+
|
|
121
|
+
# Add logo if available
|
|
122
|
+
if logo_ref := index_item["logo_ref"]:
|
|
123
|
+
text += (
|
|
124
|
+
f' <img src="{logo_ref}" class="doc-logo" alt="{index_item["name"]}'
|
|
125
|
+
' logo">\n'
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Add title
|
|
129
|
+
text += ' <div class="doc-title">\n'
|
|
130
|
+
# Always use the unversioned directory_name for the link
|
|
131
|
+
text += (
|
|
132
|
+
" <h3><a"
|
|
133
|
+
f' href="{index_item["directory_name"]}">{index_item["name"]}</a></h3>\n'
|
|
134
|
+
)
|
|
135
|
+
text += " </div>\n"
|
|
136
|
+
text += " </div>\n"
|
|
137
|
+
|
|
138
|
+
# Add description if available
|
|
139
|
+
if index_item["description"]:
|
|
140
|
+
text += f' <div class="doc-description">{index_item["description"]}</div>\n'
|
|
141
|
+
|
|
142
|
+
# Add version links
|
|
143
|
+
text += ' <div class="doc-versions">\n'
|
|
144
|
+
# Always use the unversioned directory_name for the "Latest" link
|
|
145
|
+
text += (
|
|
146
|
+
f' <a href="{index_item["directory_name"]}" class="version-link'
|
|
147
|
+
' latest">Latest</a>\n'
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if index_item["versions"]:
|
|
151
|
+
version_count = len(index_item["versions"])
|
|
152
|
+
version_text = f"{version_count} version" + ("s" if version_count != 1 else "")
|
|
153
|
+
|
|
154
|
+
text += f""" <button class="versions-toggle">
|
|
155
|
+
<span class="version-count">{version_text}</span>
|
|
156
|
+
<svg fill="currentColor" viewBox="0 0 20 20">
|
|
157
|
+
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
|
158
|
+
</svg>
|
|
159
|
+
</button>
|
|
160
|
+
</div>
|
|
161
|
+
<div class="versions-list">
|
|
162
|
+
"""
|
|
163
|
+
for version in index_item["versions"]:
|
|
164
|
+
text += (
|
|
165
|
+
f' <a href="{version["directory_name"]}"'
|
|
166
|
+
f' class="version-link">{version["version"]}</a>\n'
|
|
167
|
+
)
|
|
168
|
+
text += " </div>\n"
|
|
169
|
+
else:
|
|
170
|
+
text += " </div>\n"
|
|
171
|
+
|
|
172
|
+
text += "</div>\n"
|
|
173
|
+
|
|
174
|
+
return text
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# ----------------------------------------------------------------------------------------
|
|
2
|
+
# render_md
|
|
3
|
+
# ---------
|
|
4
|
+
#
|
|
5
|
+
# Renders a MarkDown file into HTML
|
|
6
|
+
#
|
|
7
|
+
# License
|
|
8
|
+
# -------
|
|
9
|
+
# MIT License - Copyright 2025-2026 Cyber Assessment Labs
|
|
10
|
+
#
|
|
11
|
+
# Authors
|
|
12
|
+
# -------
|
|
13
|
+
# bena
|
|
14
|
+
#
|
|
15
|
+
# Version History
|
|
16
|
+
# ---------------
|
|
17
|
+
# Mar 2024 - Created
|
|
18
|
+
# Dec 2025 - New version 2
|
|
19
|
+
# ----------------------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
# ----------------------------------------------------------------------------------------
|
|
22
|
+
# Imports
|
|
23
|
+
# ----------------------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
26
|
+
import importlib.resources
|
|
27
|
+
import os
|
|
28
|
+
import string
|
|
29
|
+
import markdown
|
|
30
|
+
from . import async_file
|
|
31
|
+
|
|
32
|
+
# ----------------------------------------------------------------------------------------
|
|
33
|
+
# Globals
|
|
34
|
+
# ----------------------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
g_md_template: str | None = None
|
|
37
|
+
"""Cache the template file"""
|
|
38
|
+
|
|
39
|
+
# ----------------------------------------------------------------------------------------
|
|
40
|
+
# Functions
|
|
41
|
+
# ----------------------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ----------------------------------------------------------------------------------------
|
|
45
|
+
async def md_to_html(md: bytes) -> bytes:
|
|
46
|
+
"""
|
|
47
|
+
Converts the MarkDown file data to html and returns it.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
contents_html: str = await _async_md_convert(md)
|
|
51
|
+
|
|
52
|
+
template = await _get_template()
|
|
53
|
+
template_subs = {"INSERT_DATA": contents_html}
|
|
54
|
+
html = string.Template(template).substitute(template_subs)
|
|
55
|
+
|
|
56
|
+
return html.encode()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ----------------------------------------------------------------------------------------
|
|
60
|
+
# Private Functions
|
|
61
|
+
# ----------------------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ----------------------------------------------------------------------------------------
|
|
65
|
+
async def _get_template() -> str:
|
|
66
|
+
"""
|
|
67
|
+
Reads the template HTML file. It is stored as a resource within the server.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
global g_md_template
|
|
71
|
+
|
|
72
|
+
if not g_md_template:
|
|
73
|
+
file_folder = importlib.resources.files("cal_docs_server.resources")
|
|
74
|
+
file_path: str = os.path.join(str(file_folder), "md_template.html")
|
|
75
|
+
async with async_file.open_text(file_path, "rt") as f:
|
|
76
|
+
file_contents = await f.read()
|
|
77
|
+
g_md_template = file_contents
|
|
78
|
+
|
|
79
|
+
return g_md_template
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ----------------------------------------------------------------------------------------
|
|
83
|
+
async def _async_md_convert(md: bytes) -> str:
|
|
84
|
+
"""
|
|
85
|
+
Wraps markdown.markdown in a thread,
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
md_text = md.decode(errors="replace")
|
|
89
|
+
html = await asyncio.to_thread(markdown.markdown, md_text, extensions=["extra"])
|
|
90
|
+
return html
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Documentation Server Help
|
|
2
|
+
|
|
3
|
+
The CAL Documentation Server provides a central location to store project documentation
|
|
4
|
+
with automatic indexing and support for versioned documentation.
|
|
5
|
+
|
|
6
|
+
Anyone can easily add their documentation by choosing a unique project name.
|
|
7
|
+
|
|
8
|
+
## Supported Documentation Types
|
|
9
|
+
|
|
10
|
+
The documentation server supports:
|
|
11
|
+
|
|
12
|
+
- **Static HTML sites** - Pre-built HTML documentation (e.g., from Sphinx, MkDocs, pdoc)
|
|
13
|
+
- **Markdown files** - `.md` files rendered on-the-fly with syntax highlighting
|
|
14
|
+
- **Mixed content** - Combination of HTML and markdown files
|
|
15
|
+
|
|
16
|
+
### Requirements
|
|
17
|
+
|
|
18
|
+
Documentation must be self-contained in a directory with either `index.html` or `index.md`
|
|
19
|
+
at the root.
|
|
20
|
+
|
|
21
|
+
## Configuration with docinfo
|
|
22
|
+
|
|
23
|
+
Add a `docinfo` file to your documentation root to customize how it appears in the index.
|
|
24
|
+
|
|
25
|
+
**Supported formats:** `docinfo.json`, `docinfo.json5`, `docinfo.yaml`, `docinfo.yml`
|
|
26
|
+
|
|
27
|
+
**Available fields:**
|
|
28
|
+
|
|
29
|
+
- `name` - Display name shown in index (defaults to directory name)
|
|
30
|
+
- `description` - Description text shown under the project name
|
|
31
|
+
- `noindex` - Set to `true` to hide from index (still accessible via direct URL)
|
|
32
|
+
|
|
33
|
+
**Logo support:**
|
|
34
|
+
|
|
35
|
+
Place an image file named `docinfo.png`, `docinfo.jpg`, or `docinfo.svg` in the same
|
|
36
|
+
directory to display a logo on the index page.
|
|
37
|
+
|
|
38
|
+
**Example docinfo.json:**
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"name": "My Project",
|
|
43
|
+
"description": "A comprehensive guide to using My Project",
|
|
44
|
+
"noindex": false
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Version Management
|
|
49
|
+
|
|
50
|
+
The server supports versioned documentation with automatic "Latest" link handling.
|
|
51
|
+
|
|
52
|
+
### Directory Naming Convention
|
|
53
|
+
|
|
54
|
+
- **Unversioned:** `my-project/` (optional, can be omitted if only versioned dirs exist)
|
|
55
|
+
- **Versioned:** `my-project-1.2.3/`, `my-project-2.0.0/`, etc.
|
|
56
|
+
|
|
57
|
+
The server automatically:
|
|
58
|
+
|
|
59
|
+
- Detects all versions based on directory names (e.g., `my-project-*`)
|
|
60
|
+
- Filters out beta/alpha/rc versions when determining "Latest"
|
|
61
|
+
- Redirects the unversioned URL (`/my-project/`) to the latest stable version
|
|
62
|
+
- Sorts versions using semantic versioning
|
|
63
|
+
|
|
64
|
+
**Example directory structure:**
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
docs/
|
|
68
|
+
├── my-project-1.0.0/
|
|
69
|
+
├── my-project-2.0.0/
|
|
70
|
+
├── my-project-2.1.0/
|
|
71
|
+
└── my-project-3.0.0b1/ (excluded from "Latest")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
In this example, accessing `/my-project/` serves content from `my-project-2.1.0/` (the
|
|
75
|
+
latest non-beta version).
|
|
76
|
+
|
|
77
|
+
## REST API
|
|
78
|
+
|
|
79
|
+
The server provides a REST API for programmatic access to documentation.
|
|
80
|
+
|
|
81
|
+
### cal-docs-client
|
|
82
|
+
|
|
83
|
+
For command-line access to the API, use the `cal-docs-client` tool:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
# Install the client
|
|
87
|
+
pip install cal-docs-client
|
|
88
|
+
|
|
89
|
+
# List projects
|
|
90
|
+
cal-docs-client list
|
|
91
|
+
|
|
92
|
+
# Upload documentation
|
|
93
|
+
cal-docs-client upload my-project-1.0.0-docs.zip
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
See the [cal-docs-client documentation](https://example.com/) for full usage.
|
|
97
|
+
|
|
98
|
+
### Endpoints
|
|
99
|
+
|
|
100
|
+
| Method | Endpoint | Description |
|
|
101
|
+
|--------|----------|-------------|
|
|
102
|
+
| GET | `/api/version` | Returns product name and version |
|
|
103
|
+
| GET | `/api/spec` | Returns OpenAPI 3.0 specification |
|
|
104
|
+
| GET | `/api/projects` | Lists all projects and versions |
|
|
105
|
+
| GET | `/api/projects?search=term` | Filter projects by name |
|
|
106
|
+
| GET | `/api/download/{project}/{version}` | Download project as zip |
|
|
107
|
+
| POST | `/api/upload` | Upload documentation (requires auth) |
|
|
108
|
+
|
|
109
|
+
### Authentication
|
|
110
|
+
|
|
111
|
+
Upload requires token authentication via the `X-Token` header. Contact your administrator
|
|
112
|
+
to obtain a token.
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
X-Token: your-secret-token
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Examples
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
# List all projects
|
|
122
|
+
curl http://example.com/api/projects
|
|
123
|
+
|
|
124
|
+
# Search projects
|
|
125
|
+
curl "http://example.com/api/projects?search=my-project"
|
|
126
|
+
|
|
127
|
+
# Download latest version as zip
|
|
128
|
+
curl -o docs.zip http://example.com/api/download/my-project/latest
|
|
129
|
+
|
|
130
|
+
# Download specific version
|
|
131
|
+
curl -o docs.zip http://example.com/api/download/my-project/1.2.3
|
|
132
|
+
|
|
133
|
+
# Upload documentation (requires token)
|
|
134
|
+
curl -X POST -H "X-Token: your-token" \
|
|
135
|
+
-F "file=@my-project-1.0.0-docs.zip" \
|
|
136
|
+
http://example.com/api/upload
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Uploading Documentation
|
|
140
|
+
|
|
141
|
+
### Zip File Naming
|
|
142
|
+
|
|
143
|
+
When uploading via the API, name your zip file to indicate the project and version:
|
|
144
|
+
|
|
145
|
+
- `my-project-1.2.3.zip`
|
|
146
|
+
- `my-project-1.2.3-docs.zip`
|
|
147
|
+
|
|
148
|
+
The server extracts the project name and version from the filename.
|
|
149
|
+
|
|
150
|
+
### Using GitLab CI
|
|
151
|
+
|
|
152
|
+
Upload documentation automatically as part of your CI/CD pipeline:
|
|
153
|
+
|
|
154
|
+
```yaml
|
|
155
|
+
upload_docs:
|
|
156
|
+
stage: deploy
|
|
157
|
+
only:
|
|
158
|
+
- tags
|
|
159
|
+
script:
|
|
160
|
+
- zip -r my-project-${CI_COMMIT_TAG}-docs.zip ./html
|
|
161
|
+
- curl -X POST -H "X-Token: ${DOC_SERVER_TOKEN}" \
|
|
162
|
+
-F "file=@my-project-${CI_COMMIT_TAG}-docs.zip" \
|
|
163
|
+
"${DOC_SERVER_URL}/api/upload"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Tips
|
|
167
|
+
|
|
168
|
+
- Only upload docs from tagged releases or manual jobs, not every commit
|
|
169
|
+
- Use semantic versioning for directory names (e.g., `1.2.3`, not `v1.2.3`)
|
|
170
|
+
- Beta/alpha versions should include the suffix (e.g., `2.0.0b1`, `3.0.0-alpha`)
|
|
171
|
+
- The server caches the index for 20 seconds, so changes may take a moment to appear
|