cal-docs-client 1.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,265 @@
1
+ # ────────────────────────────────────────────────────────────────────────────────────────
2
+ # client.py
3
+ # ─────────
4
+ #
5
+ # HTTP client for cal-docs-server API. Uses only Python stdlib (urllib.request).
6
+ #
7
+ # (c) 2026 Cyber Assessment Labs — MIT License; see LICENSE in the project root.
8
+ #
9
+ # Authors
10
+ # ───────
11
+ # bena (via Claude)
12
+ #
13
+ # Version History
14
+ # ───────────────
15
+ # Feb 2026 - Created
16
+ # ────────────────────────────────────────────────────────────────────────────────────────
17
+
18
+ # ────────────────────────────────────────────────────────────────────────────────────────
19
+ # Imports
20
+ # ────────────────────────────────────────────────────────────────────────────────────────
21
+
22
+ import json
23
+ import urllib.error
24
+ import urllib.parse
25
+ import urllib.request
26
+ from typing import Any
27
+
28
+ # ────────────────────────────────────────────────────────────────────────────────────────
29
+ # Exceptions
30
+ # ────────────────────────────────────────────────────────────────────────────────────────
31
+
32
+
33
+ class ClientError(Exception):
34
+ """Error from the API client."""
35
+
36
+
37
+ # ────────────────────────────────────────────────────────────────────────────────────────
38
+ # Client
39
+ # ────────────────────────────────────────────────────────────────────────────────────────
40
+
41
+
42
+ class DocsClient:
43
+ """HTTP client for cal-docs-server API."""
44
+
45
+ def __init__(self, server_url: str, token: str | None = None) -> None:
46
+ """Initialize the client.
47
+
48
+ Args:
49
+ server_url: Base URL of the cal-docs-server (e.g., "http://localhost:8080")
50
+ token: Optional API token for authenticated endpoints (upload)
51
+ """
52
+ self.server_url = server_url.rstrip("/")
53
+ self.token = token
54
+ self._verified = False
55
+ self._server_version: str | None = None
56
+ self._api_version: str | None = None
57
+
58
+ # ────────────────────────────────────────────────────────────────────────────────────
59
+ # HTTP Methods
60
+ # ────────────────────────────────────────────────────────────────────────────────────
61
+
62
+ def _request(
63
+ self,
64
+ method: str,
65
+ path: str,
66
+ data: bytes | None = None,
67
+ headers: dict[str, str] | None = None,
68
+ timeout: float = 30.0,
69
+ ) -> bytes:
70
+ """Make an HTTP request.
71
+
72
+ Args:
73
+ method: HTTP method (GET, POST, etc.)
74
+ path: URL path (e.g., "/api/version")
75
+ data: Optional request body
76
+ headers: Optional headers
77
+ timeout: Request timeout in seconds
78
+
79
+ Returns:
80
+ Response body as bytes
81
+
82
+ Raises:
83
+ ClientError: On HTTP or connection error
84
+ """
85
+ url = f"{self.server_url}{path}"
86
+ req_headers = headers.copy() if headers else {}
87
+
88
+ if self.token:
89
+ req_headers["X-Token"] = self.token
90
+
91
+ request = urllib.request.Request(
92
+ url,
93
+ data=data,
94
+ headers=req_headers,
95
+ method=method,
96
+ )
97
+
98
+ try:
99
+ with urllib.request.urlopen(request, timeout=timeout) as response:
100
+ return response.read()
101
+ except urllib.error.HTTPError as e:
102
+ body = e.read().decode("utf-8", errors="replace")
103
+ try:
104
+ error_data = json.loads(body)
105
+ message = error_data.get("message", body)
106
+ except json.JSONDecodeError:
107
+ message = body
108
+ raise ClientError(f"HTTP {e.code}: {message}") from e
109
+ except urllib.error.URLError as e:
110
+ raise ClientError(f"Connection failed: {e.reason}") from e
111
+ except TimeoutError as e:
112
+ raise ClientError(f"Request timed out after {timeout}s") from e
113
+
114
+ def _get(self, path: str) -> bytes:
115
+ """Make a GET request."""
116
+ return self._request("GET", path)
117
+
118
+ def _get_json(self, path: str) -> Any:
119
+ """Make a GET request and parse JSON response."""
120
+ data = self._get(path)
121
+ try:
122
+ return json.loads(data)
123
+ except json.JSONDecodeError as e:
124
+ raise ClientError(f"Invalid JSON response: {e}") from e
125
+
126
+ def _post(
127
+ self,
128
+ path: str,
129
+ data: bytes,
130
+ content_type: str,
131
+ filename: str | None = None,
132
+ ) -> bytes:
133
+ """Make a POST request."""
134
+ headers = {"Content-Type": content_type}
135
+ if filename:
136
+ headers["X-Filename"] = filename
137
+ return self._request("POST", path, data, headers)
138
+
139
+ # ────────────────────────────────────────────────────────────────────────────────────
140
+ # Server Verification
141
+ # ────────────────────────────────────────────────────────────────────────────────────
142
+
143
+ def verify_server(self) -> None:
144
+ """Verify the server is cal-docs-server with a compatible API version.
145
+
146
+ Accepts API version 1.x, rejects 2.x and above.
147
+
148
+ Raises:
149
+ ClientError: If server is not cal-docs-server or API version is incompatible
150
+ """
151
+ if self._verified:
152
+ return
153
+
154
+ data = self._get_json("/api/version")
155
+
156
+ product = data.get("product", "")
157
+ if product != "cal-docs-server":
158
+ raise ClientError(
159
+ f"Server is not cal-docs-server (product: {product or 'unknown'})"
160
+ )
161
+
162
+ api_version = data.get("apiVersion", "")
163
+ if not api_version:
164
+ raise ClientError("Server did not report an API version")
165
+
166
+ # Parse major version
167
+ try:
168
+ major = int(api_version.split(".")[0])
169
+ except ValueError, IndexError:
170
+ raise ClientError(f"Invalid API version format: {api_version}") from None
171
+
172
+ if major != 1:
173
+ raise ClientError(
174
+ f"Incompatible API version: {api_version} (this client requires 1.x)"
175
+ )
176
+
177
+ self._verified = True
178
+ self._server_version = data.get("version", "unknown")
179
+ self._api_version = api_version
180
+
181
+ @property
182
+ def server_version(self) -> str:
183
+ """Get the server version (requires verify_server to be called first)."""
184
+ return self._server_version or "unknown"
185
+
186
+ @property
187
+ def api_version(self) -> str:
188
+ """Get the API version (requires verify_server to be called first)."""
189
+ return self._api_version or "unknown"
190
+
191
+ # ────────────────────────────────────────────────────────────────────────────────────
192
+ # API Methods
193
+ # ────────────────────────────────────────────────────────────────────────────────────
194
+
195
+ def get_version(self) -> dict[str, str]:
196
+ """Get server version information.
197
+
198
+ Returns:
199
+ Dict with 'product', 'version', and 'apiVersion' keys
200
+ """
201
+ return self._get_json("/api/version")
202
+
203
+ def get_help(self) -> str:
204
+ """Get the server's API help text.
205
+
206
+ Returns:
207
+ Plain text API documentation
208
+ """
209
+ return self._get("/api/help").decode("utf-8")
210
+
211
+ def get_spec(self) -> dict[str, Any]:
212
+ """Get the OpenAPI specification.
213
+
214
+ Returns:
215
+ OpenAPI spec as a dict
216
+ """
217
+ return self._get_json("/api/spec")
218
+
219
+ def get_projects(self, search: str | None = None) -> dict[str, Any]:
220
+ """List documentation projects.
221
+
222
+ Args:
223
+ search: Optional search term to filter projects
224
+
225
+ Returns:
226
+ Dict with 'projects' list and 'count' key
227
+ """
228
+ path = "/api/projects"
229
+ if search:
230
+ path = f"{path}?search={urllib.parse.quote(search)}"
231
+ return self._get_json(path)
232
+
233
+ def download(self, project: str, version: str = "latest") -> bytes:
234
+ """Download a documentation package.
235
+
236
+ Args:
237
+ project: Project name
238
+ version: Version to download (default: "latest")
239
+
240
+ Returns:
241
+ Zip file contents as bytes
242
+ """
243
+ path = (
244
+ f"/api/download/{urllib.parse.quote(project)}/{urllib.parse.quote(version)}"
245
+ )
246
+ return self._get(path)
247
+
248
+ def upload(self, filename: str, data: bytes) -> dict[str, Any]:
249
+ """Upload a documentation package.
250
+
251
+ Args:
252
+ filename: Name of the zip file (e.g., "myproject-1.0.0-docs.zip")
253
+ data: Zip file contents
254
+
255
+ Returns:
256
+ Server response with upload details
257
+
258
+ Raises:
259
+ ClientError: If upload fails (auth error, invalid file, etc.)
260
+ """
261
+ response = self._post("/api/upload", data, "application/zip", filename)
262
+ try:
263
+ return json.loads(response)
264
+ except json.JSONDecodeError as e:
265
+ raise ClientError(f"Invalid JSON response: {e}") from e
@@ -0,0 +1,32 @@
1
+ # ────────────────────────────────────────────────────────────────────────────────────────
2
+ # common/__init__.py
3
+ # ──────────────────
4
+ #
5
+ # Common utilities for cal-docs-client.
6
+ #
7
+ # (c) 2026 Cyber Assessment Labs — MIT License; see LICENSE in the project root.
8
+ #
9
+ # Authors
10
+ # ───────
11
+ # bena (via Claude)
12
+ #
13
+ # Version History
14
+ # ───────────────
15
+ # Feb 2026 - Created
16
+ # ────────────────────────────────────────────────────────────────────────────────────────
17
+
18
+ from .colour import bold
19
+ from .colour import cyan
20
+ from .colour import green
21
+ from .colour import red
22
+ from .colour import set_colours_enabled
23
+ from .colour import yellow
24
+
25
+ __all__ = [
26
+ "bold",
27
+ "cyan",
28
+ "green",
29
+ "red",
30
+ "set_colours_enabled",
31
+ "yellow",
32
+ ]
@@ -0,0 +1,107 @@
1
+ # ────────────────────────────────────────────────────────────────────────────────────────
2
+ # colour.py
3
+ # ─────────
4
+ #
5
+ # ANSI colour output for terminal. Only applies colours when stdout is a TTY
6
+ # and colours are not disabled.
7
+ #
8
+ # Uses colours that work well on both light and dark backgrounds.
9
+ #
10
+ # (c) 2026 Cyber Assessment Labs — MIT License; see LICENSE in the project root.
11
+ #
12
+ # Authors
13
+ # ───────
14
+ # bena (via Claude)
15
+ #
16
+ # Version History
17
+ # ───────────────
18
+ # Feb 2026 - Created
19
+ # ────────────────────────────────────────────────────────────────────────────────────────
20
+
21
+ # ────────────────────────────────────────────────────────────────────────────────────────
22
+ # Imports
23
+ # ────────────────────────────────────────────────────────────────────────────────────────
24
+
25
+ import sys
26
+
27
+ # ────────────────────────────────────────────────────────────────────────────────────────
28
+ # ANSI Codes
29
+ # ────────────────────────────────────────────────────────────────────────────────────────
30
+
31
+ # These colours work well on both light and dark backgrounds
32
+ RESET = "\033[0m"
33
+ BOLD = "\033[1m"
34
+ GREEN = "\033[32m"
35
+ YELLOW = "\033[33m"
36
+ RED = "\033[31m"
37
+ CYAN = "\033[36m"
38
+
39
+ # ────────────────────────────────────────────────────────────────────────────────────────
40
+ # State
41
+ # ────────────────────────────────────────────────────────────────────────────────────────
42
+
43
+ _colours_enabled: bool | None = None
44
+
45
+ # ────────────────────────────────────────────────────────────────────────────────────────
46
+ # Functions
47
+ # ────────────────────────────────────────────────────────────────────────────────────────
48
+
49
+
50
+ # ────────────────────────────────────────────────────────────────────────────────────────
51
+ def set_colours_enabled(enabled: bool) -> None:
52
+ """Explicitly enable or disable colours."""
53
+ global _colours_enabled
54
+ _colours_enabled = enabled
55
+
56
+
57
+ # ────────────────────────────────────────────────────────────────────────────────────────
58
+ def _should_use_colours() -> bool:
59
+ """Determine if colours should be used."""
60
+ global _colours_enabled
61
+
62
+ # If explicitly set, use that
63
+ if _colours_enabled is not None:
64
+ return _colours_enabled
65
+
66
+ # Auto-detect: use colours if stdout is a TTY
67
+ return sys.stdout.isatty()
68
+
69
+
70
+ # ────────────────────────────────────────────────────────────────────────────────────────
71
+ def green(text: str) -> str:
72
+ """Return text in green (for success)."""
73
+ if _should_use_colours():
74
+ return f"{GREEN}{text}{RESET}"
75
+ return text
76
+
77
+
78
+ # ────────────────────────────────────────────────────────────────────────────────────────
79
+ def yellow(text: str) -> str:
80
+ """Return text in yellow (for warning)."""
81
+ if _should_use_colours():
82
+ return f"{YELLOW}{text}{RESET}"
83
+ return text
84
+
85
+
86
+ # ────────────────────────────────────────────────────────────────────────────────────────
87
+ def red(text: str) -> str:
88
+ """Return text in red (for error)."""
89
+ if _should_use_colours():
90
+ return f"{RED}{text}{RESET}"
91
+ return text
92
+
93
+
94
+ # ────────────────────────────────────────────────────────────────────────────────────────
95
+ def cyan(text: str) -> str:
96
+ """Return text in cyan (for info)."""
97
+ if _should_use_colours():
98
+ return f"{CYAN}{text}{RESET}"
99
+ return text
100
+
101
+
102
+ # ────────────────────────────────────────────────────────────────────────────────────────
103
+ def bold(text: str) -> str:
104
+ """Return text in bold."""
105
+ if _should_use_colours():
106
+ return f"{BOLD}{text}{RESET}"
107
+ return text
@@ -0,0 +1,32 @@
1
+ # ────────────────────────────────────────────────────────────────────────────────────────
2
+ # version.py
3
+ # ──────────
4
+ #
5
+ # Version handling for cal-docs-client.
6
+ #
7
+ # (c) 2026 Cyber Assessment Labs — MIT License; see LICENSE in the project root.
8
+ #
9
+ # Authors
10
+ # ───────
11
+ # bena (via Claude)
12
+ #
13
+ # Version History
14
+ # ───────────────
15
+ # Feb 2026 - Created
16
+ # ────────────────────────────────────────────────────────────────────────────────────────
17
+
18
+
19
+ # ────────────────────────────────────────────────────────────────────────────────────────
20
+ def _get_version() -> str:
21
+ """Get the version string from the generated _version.py or return DEV."""
22
+ try:
23
+ # fmt: off
24
+ from ._version import __version__ as _v # pyright: ignore[reportMissingImports,reportUnknownVariableType] # noqa: I001
25
+ # fmt: on
26
+
27
+ return str(_v) # pyright: ignore[reportUnknownArgumentType]
28
+ except ImportError:
29
+ return "DEV"
30
+
31
+
32
+ VERSION_STR: str = _get_version()
@@ -0,0 +1,120 @@
1
+ Metadata-Version: 2.4
2
+ Name: cal-docs-client
3
+ Version: 1.0.0b1
4
+ Summary: CLI client for cal-docs-server documentation API
5
+ Project-URL: Repository, https://gitlab.com/cyberassessmentlabs/public/tools/cal-docs-client
6
+ Project-URL: Documentation, https://cyberassessmentlabs.gitlab.io/public/docs/cal-docs-client/latest
7
+ Author: Cyber Assessment Labs
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: api,cli,client,docs,documentation
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Requires-Python: >=3.14
19
+ Description-Content-Type: text/markdown
20
+
21
+ # cal-docs-client
22
+
23
+ CLI client for [cal-docs-server](https://gitlab.com/cyberassessmentlabs/public/tools/cal-docs-server) documentation API.
24
+
25
+ ## Requirements
26
+
27
+ - Python 3.14+
28
+ - No external dependencies (stdlib only)
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install cal-docs-client
34
+ ```
35
+
36
+ Or install from source:
37
+
38
+ ```bash
39
+ git clone https://gitlab.com/cyberassessmentlabs/public/tools/cal-docs-client.git
40
+ cd cal-docs-client
41
+ pip install .
42
+ ```
43
+
44
+ ## Configuration
45
+
46
+ Configuration is optional. You can provide settings via command-line options, environment variables, or a config file.
47
+
48
+ ### Config File (Optional)
49
+
50
+ For convenience, create a config file at `~/.config/cal-docs-client/config.json`:
51
+
52
+ ```json
53
+ {
54
+ "server": "https://docs.example.com",
55
+ "token": "your-api-token",
56
+ "no_colour": false
57
+ }
58
+ ```
59
+
60
+ You can also specify a different config file with `-c`/`--config`.
61
+
62
+ ### Configuration Priority
63
+
64
+ Settings are resolved in this order (first wins):
65
+ 1. Command-line arguments (`-s`, `--no-colour`)
66
+ 2. Environment variables (`CAL_DOCS_SERVER`, `CAL_DOCS_TOKEN`)
67
+ 3. Config file
68
+
69
+ ## Usage
70
+
71
+ ```bash
72
+ # Show help
73
+ cal-docs-client --help
74
+
75
+ # Show server and client version
76
+ cal-docs-client version
77
+
78
+ # List all projects
79
+ cal-docs-client projects
80
+
81
+ # Search for projects
82
+ cal-docs-client projects --search myproject
83
+
84
+ # Download documentation
85
+ cal-docs-client download myproject latest
86
+ cal-docs-client download myproject 1.0.0 -o docs.zip
87
+
88
+ # Upload documentation (requires token in config or -t)
89
+ cal-docs-client upload myproject-1.0.0-docs.zip
90
+
91
+ # Show server API help
92
+ cal-docs-client help
93
+
94
+ # Get OpenAPI specification
95
+ cal-docs-client spec -o openapi.json
96
+ ```
97
+
98
+ ### Command-Line Options
99
+
100
+ You can pass all settings directly on the command line:
101
+
102
+ ```bash
103
+ cal-docs-client -s https://docs.example.com projects
104
+ cal-docs-client -s https://docs.example.com upload -t YOUR_TOKEN docs.zip
105
+ ```
106
+
107
+ ### Environment Variables
108
+
109
+ You can also use environment variables:
110
+
111
+ - `CAL_DOCS_SERVER` - Server URL
112
+ - `CAL_DOCS_TOKEN` - Authentication token
113
+
114
+ ## API Version Compatibility
115
+
116
+ This client requires cal-docs-server API version 1.x. It will refuse to connect to servers with incompatible API versions.
117
+
118
+ ## License
119
+
120
+ MIT License - (c) 2026 Cyber Assessment Labs
@@ -0,0 +1,14 @@
1
+ cal_docs_client/__init__.py,sha256=-bKdz7ZdnfWxn04KMgNzpwUdxcKqRcR8DpcNM0NTBQA,929
2
+ cal_docs_client/__main__.py,sha256=fuTDiIx-veSOTEVhxxyKG3poYiQ2rk87Ko2Wwvpjj-4,965
3
+ cal_docs_client/_version.py,sha256=JQkLQAME1HDp3QVEh9RrTjuwWvqAl4Ya_t--FXT4jms,173
4
+ cal_docs_client/argbuilder.py,sha256=5cWtNrndIgp31hT_Za6pKQ60CBFtOdaLrPakjlDR9zA,64965
5
+ cal_docs_client/cli.py,sha256=ahGVrLz2LXYsPwKP-hA46hadoI77AOX1UV8G3WVNQ5Q,16895
6
+ cal_docs_client/client.py,sha256=kqSrGUF6RPUXQ_FX4pzf6pv4n3vICQZKjFpPOl4_DqU,11089
7
+ cal_docs_client/version.py,sha256=265arrq2YMLxGA3YJqtnoqiTSgY8LQBJJsu0XAmbHJ8,1573
8
+ cal_docs_client/common/__init__.py,sha256=NtGfBqAAZKrZ1Il4ceHIlOxIZtybV5ApBeZDBsw6D_Q,1176
9
+ cal_docs_client/common/colour.py,sha256=yIU4xb9HHXQSSMdZrJUyfTpij58CUHgO4YLsWVzVAn8,6527
10
+ cal_docs_client-1.0.0b1.dist-info/METADATA,sha256=R8lAKPMiVYpbSXvoV75dxeLz4TsyBpWEk6mIDs8zl8s,3022
11
+ cal_docs_client-1.0.0b1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
+ cal_docs_client-1.0.0b1.dist-info/entry_points.txt,sha256=IYL7hCO3QJ6Idq8_zAplfnRmoaMOaCNzg-3y4T_dQYs,61
13
+ cal_docs_client-1.0.0b1.dist-info/licenses/LICENSE,sha256=zIXdXMPhkY8xLlrhw7lOsWiFOecEYRukxgZIjMTKPuE,1078
14
+ cal_docs_client-1.0.0b1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ cal-docs-client = cal_docs_client.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cyber Assessment Labs
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.