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