Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## Version 0.3.0

- New option `--delete-project` to delete al project versions from a docat server
- New option `--system-certs` to use the system SSL certificate bundle instead of [certifi](https://github.com/certifi/python-certifi).

## Version 0.2.0

- New option `--verbose` for some debug information
Expand Down
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# docat-upload

[![Release](https://img.shields.io/github/v/release/palto42/docat-upload)](https://github.com/palto42/docat-upload/releases)
![python version](https://img.shields.io/badge/python-3.10+-blue.svg)
[![PyPI](https://img.shields.io/pypi/v/girsh)](https://pypi.org/project/docat-upload)
[![Build status](https://img.shields.io/github/actions/workflow/status/palto42/docat-upload/main.yml?branch=main)](https://github.com/palto42/docat-upload/actions/workflows/main.yml?query=branch%3Amain)
[![codecov](https://codecov.io/gh/palto42/docat-upload/branch/main/graph/badge.svg)](https://codecov.io/gh/palto42/docat-upload)
[![Commit activity](https://img.shields.io/github/commit-activity/m/palto42/docat-upload)](https://github.com/palto42/docat-upload/graphs/commit-activity)
Expand All @@ -18,7 +19,9 @@ Extra features:
- For Python documentation the script can extract the document version from the Python module with the same name as `project`.
- Limit the number of published version with `-max-versions`
- The script will automatically delete older versions if the number of versions is exceeded
- Delete all current versions of a project with `--delete-project`
- Specify custom SSL certificate path if an in-house CA is used.
- Use the system CA bundle instead of requests' default certifi bundle with `--system-cert`.
- Support insecure SSL for use with self signed certificates

## Usage
Expand All @@ -42,13 +45,40 @@ options:
-m NUM, --max-versions NUM
Cut number of versions to max. NUM
-d, --delete Delete the specified version
--delete-project Delete the entire project by removing all versions
-V, --version show program's version number and exit
-i, --insecure Don't check SSL cert
-c SSL_CERT, --ssl-cert SSL_CERT
Path to SSL cert or cert bundle, e.g. /etc/ssl/certs/ca-certificates.crt
--system-cert Use the system CA bundle instead of requests default certifi bundle
-v, --verbose Verbose output
```

### Example

Upload a new documentation version to a project:

```bash
uv run python -m docat_upload.docat_upload \
-p my-project \
-s https://docat.example.com \
-a YOUR_API_KEY \
-f docs/_build/html \
-r 1.2.0 \
-t latest \
-m 5
```

Delete all versions of a project from the docat server:

```bash
uv run python -m docat_upload.docat_upload \
-p my-project \
-s https://docat.example.com \
-a YOUR_API_KEY \
--delete-project
```

### `.env` settings

Instead of passing the options via CLI, they can be provided in an `.env` file.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "docat-upload"
version = "0.2.0"
version = "0.3.0"
description = "Tool for uploading HTML documentation to a docat server, as an alternative to docatl."
authors = [{ name = "Matthias Homann", email = "palto@mailbox.org" }]
readme = "README.md"
Expand Down
111 changes: 107 additions & 4 deletions src/docat_upload/docat_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import importlib
import logging
import re
import ssl
from importlib.metadata import version
from json import JSONDecodeError
from pathlib import Path
Expand Down Expand Up @@ -268,6 +269,78 @@ def delete_version(project: str, api_key: str | None, release: str, server: str,
return False


def delete_project(project: str, api_key: str | None, server: str, verify_ssl: str | bool = True) -> bool:
"""Delete all versions of a project from the docat server.

Parameters
----------
project : str
Name of the project on the docat server
api_key : str | None
API key of the project
server : str
Dcat server URL
verify_ssl : str | bool, optional
Verify SSL (True), path to certs or accept insecure SSL (False), by default True

Returns
-------
bool
True = successful
"""
project_url = f"{server}/api/projects/{project}"
logger.debug("Fetching project versions from %s", project_url)
try:
response = requests.get(
project_url,
timeout=60,
verify=verify_ssl,
)
except requests.exceptions.SSLError as e:
logger.error("SSL error during project deletion: %s", e) # noqa: TRY400
return False
except requests.exceptions.ConnectionError as e:
logger.error("Connection error during project deletion: %s", e) # noqa: TRY400
return False
try:
project_data = response.json()
except JSONDecodeError:
logger.exception("Failed to decode project version data for %s", project)
return False

versions = project_data.get("versions", [])
if not versions:
logger.info("No versions found for project %s", project)
return True

# Delete each version
for doc_version in versions:
name = doc_version.get("name")
if not name:
continue
logger.debug("Deleting version %s of project %s", name, project)
ok = delete_version(project=project, api_key=api_key, release=name, server=server, verify_ssl=verify_ssl)
if not ok:
logger.error("Failed to delete version %s of project %s", name, project)
return False

logger.info("Deleted all %d versions of project %s", len(versions), project)
return True


def get_system_ca_bundle() -> str:
"""Return the system CA bundle path for SSL verification."""
verify_paths = ssl.get_default_verify_paths()
if verify_paths.cafile:
logger.debug("Using system CA file %s", verify_paths.cafile)
return verify_paths.cafile
if verify_paths.capath:
logger.debug("Using system CA path %s", verify_paths.capath)
return verify_paths.capath
logger.error("Could not locate the system CA bundle")
raise FileNotFoundError("System CA bundle not found") # noqa: TRY003


def get_args() -> argparse.Namespace:
"""Parse CLI arguments

Expand Down Expand Up @@ -346,6 +419,11 @@ def greater_zero(value):
help="Delete the specified version",
action="store_true",
)
parser.add_argument(
"--delete-project",
help="Delete the entire project by removing all versions",
action="store_true",
)
parser.add_argument(
"-V",
"--version",
Expand All @@ -363,7 +441,12 @@ def greater_zero(value):
"--ssl-cert",
help="Path to SSL cert or cert bundle, e.g. /etc/ssl/certs/ca-certificates.crt",
type=str,
default=config.get("CERT_PATH"),
default=None,
)
parser.add_argument(
"--system-cert",
help="Use the system CA bundle instead of requests default certifi bundle",
action="store_true",
)
parser.add_argument(
"-v",
Expand All @@ -373,15 +456,18 @@ def greater_zero(value):
)
args = parser.parse_args()

if (args.delete or args.max_versions) and not args.api_key:
if args.ssl_cert is None and not args.system_cert:
args.ssl_cert = config.get("CERT_PATH")

if (args.delete or args.max_versions or args.delete_project) and not args.api_key:
parser.error(
"No API key provided as argument, environment variable 'DOCAT_API_KEY' or in '.env' file, but required when --max-versions is used"
)

return args


def main():
def main(): # noqa: C901
"""Package documents and upload them to docat server"""
args = get_args()
configure_logging(args.verbose)
Expand All @@ -390,7 +476,24 @@ def main():
if not args.insecure:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

verify_ssl = args.ssl_cert if args.ssl_cert else args.insecure
if args.system_cert:
verify_ssl = get_system_ca_bundle()
elif args.ssl_cert:
verify_ssl = args.ssl_cert
else:
verify_ssl = args.insecure

if args.delete_project:
return (
0
if delete_project(
project=args.project,
api_key=args.api_key,
server=args.server,
verify_ssl=verify_ssl,
)
else 1
)

if args.release is None:
module = importlib.import_module(args.project)
Expand Down
Loading
Loading