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
2 changes: 1 addition & 1 deletion .github/actions/setup-python-env/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ inputs:
uv-version:
description: "uv version to use"
required: true
default: "0.11.7"
default: "0.11.17"

runs:
using: "composite"
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Version 0.2.0

- New option `--verbose` for some debug information
- Updated dependencies
- Increase test coverage

## Version 0.1.0

- First release
12 changes: 6 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ Please note this documentation assumes you already have `uv` and `Git` installed

- Then, install and activate the environment with:

```bash
uv sync
```
```bash
uv sync
```

4. Install pre-commit to run linters/formatters at commit time:

Expand All @@ -90,9 +90,9 @@ Please note this documentation assumes you already have `uv` and `Git` installed

- Now, validate that all unit tests are passing:

```bash
make test
```
```bash
make test
```

8. Before raising a pull request you should also run tox.
This will run the tests across different versions of Python:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ options:
-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
-v, --verbose Verbose output
```

### `.env` settings
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.1.0"
version = "0.2.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
155 changes: 84 additions & 71 deletions src/docat_upload/docat_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import argparse
import importlib
import os
import logging
import re
from importlib.metadata import version
from json import JSONDecodeError
Expand All @@ -13,6 +13,21 @@
import urllib3
from dotenv import dotenv_values

logger = logging.getLogger(__name__)


def configure_logging(verbose: bool = False) -> None:
"""Configure logging for CLI output.

Parameters
----------
verbose : bool
Whether to enable debug-level logging.
"""
logging.basicConfig(level=logging.DEBUG if verbose else logging.INFO, format="%(levelname)s: %(message)s")
logger.debug("Logging configured; verbose=%s", verbose)


__version__ = version("docat_upload")


Expand Down Expand Up @@ -48,44 +63,43 @@ def upload_docs(
"""
folder = Path(docs_folder)
zip_file = folder.parent / Path("docs.zip")

# create a ZipFile object
zipper = ZipFile(zip_file, "w")
# iterate over the folder and its subfolders
for file_path in folder.glob("**/*"):
# check if the path is a file (not a directory)
if file_path.is_file():
# write the file to the zip file with its relative path
zipper.write(file_path, file_path.relative_to(folder))
# close the zip file
zipper.close()

print(f"Upload documentation for {project} v{release}")
logger.debug("Preparing documentation archive for folder %s", folder)

file_count = 0
with ZipFile(zip_file, "w") as zipper:
for file_path in folder.glob("**/*"):
if file_path.is_file():
zipper.write(file_path, file_path.relative_to(folder))
file_count += 1
logger.debug("Created zip archive %s containing %d files", zip_file, file_count)

logger.info("Upload documentation for %s v%s", project, release)
post_url = f"{server}/api/{project}/{release}"
headers = {"Docat-Api-Key": api_key} if api_key else {}
logger.debug("Sending upload request to %s with headers=%s verify_ssl=%s", post_url, headers, verify_ssl)
try:
# Send the POST request to the API with the file attached
response = requests.post(
f"{server}/api/{project}/{release}",
post_url,
files={"file": zip_file.open("rb")},
timeout=60,
headers={"Docat-Api-Key": api_key} if api_key else {},
headers=headers,
verify=verify_ssl,
)
except requests.exceptions.SSLError as e:
print(f"SSL error: {e}")
logger.error("SSL error during upload: %s", e) # noqa: TRY400
return False
except requests.exceptions.ConnectionError as e:
print(f"Connection error: {e}")
logger.error("Connection error during upload: %s", e) # noqa: TRY400
return False

# Delete zip file
zip_file.unlink()
logger.debug("Deleted temporary zip archive %s", zip_file)

# check the status code of the response
if not response.ok:
print(f"Failed to upload documentation: {response.reason}")
logger.error("Failed to upload documentation: %s", response.reason)
return False

print(f"Documentation version {release} for {project} uploaded successfully")
logger.info("Documentation version %s for %s uploaded successfully", release, project)
return True


Expand Down Expand Up @@ -114,23 +128,26 @@ def tag_release(
bool
True = successful
"""
tag_url = f"{server}/api/{project}/{release}/tags/{tag}"
logger.debug("Tagging release %s at %s", release, tag_url)
try:
response = requests.put(
f"{server}/api/{project}/{release}/tags/{tag}",
tag_url,
timeout=60,
headers={"Docat-Api-Key": api_key} if api_key else None,
verify=verify_ssl,
)
except requests.exceptions.SSLError as e:
print(f"SSL error: {e}")
logger.error("SSL error during tagging: %s", e) # noqa: TRY400
return False
except requests.exceptions.ConnectionError as e:
print(f"Connection error: {e}")
logger.error("Connection error during tagging: %s", e) # noqa: TRY400
return False
logger.debug("Tag request returned status code %s", response.status_code)
if response.status_code == 201:
print(f"Tagged {project} version {release} of as '{tag}'")
logger.info("Tagged %s version %s as '%s'", project, release, tag)
else:
print(f"Failed to tag version {release} of project {project}: {response.reason}")
logger.error("Failed to tag version %s of project %s: %s", release, project, response.reason)
return False
return True

Expand Down Expand Up @@ -158,39 +175,52 @@ def prune_versions(
bool
True = successful
"""
project_url = f"{server}/api/projects/{project}"
logger.debug("Fetching project versions from %s", project_url)
try:
response = requests.get(
f"{server}/api/projects/{project}",
project_url,
timeout=60,
verify=verify_ssl,
)
except requests.exceptions.SSLError as e:
print(f"SSL error: {e}")
logger.error("SSL error during version pruning: %s", e) # noqa: TRY400
return False
except requests.exceptions.ConnectionError as e:
print(f"Connection error: {e}")
logger.error("Connection error during version pruning: %s", e) # noqa: TRY400
return False
try:
project_data = response.json()
except JSONDecodeError:
print(f"Failed to fetch versions for project {project}")
logger.exception("Failed to decode project version data for %s", project)
return False
versions = project_data["versions"]
version_names = [version_info["name"] for version_info in versions]
logger.debug("Received versions for project %s: %s", project, version_names)
sorted_versions = sorted(versions, key=lambda x: tuple(map(int, x["name"].split("."))))
if len(versions) <= max_versions:
print(f"Nothing to delete, only {len(versions)} available")
logger.info("Nothing to delete, only %d available", len(versions))
return True
delete_urls = [f"{server}/api/{project}/{doc_version['name']}" for doc_version in sorted_versions[:-max_versions]]
logger.debug("Pruning versions: %s", delete_urls)
for doc_version in sorted_versions[:-max_versions]:
delete_url = f"{server}/api/{project}/{doc_version['name']}"
logger.debug("Deleting version %s via %s", doc_version["name"], delete_url)
response = requests.delete(
f"{server}api/{project}/{doc_version['name']}",
delete_url,
headers={"Docat-Api-Key": api_key} if api_key else None,
timeout=60,
verify=verify_ssl,
)
if response.status_code == 200:
print(f"Deleted version {doc_version['name']} of project {project}")
logger.info("Deleted version %s of project %s", doc_version["name"], project)
else:
print(f"Failed to delete version {doc_version['name']} of project {project}: {response.reason}")
logger.error(
"Failed to delete version %s of project %s: %s",
doc_version["name"],
project,
response.reason,
)
return False
return True

Expand All @@ -216,54 +246,28 @@ def delete_version(project: str, api_key: str | None, release: str, server: str,
bool
True = successful
"""
delete_url = f"{server}/api/{project}/{release}"
logger.debug("Deleting version %s for project %s at %s", release, project, delete_url)
try:
response = requests.delete(
f"{server}api/{project}/{release}",
delete_url,
headers={"Docat-Api-Key": api_key} if api_key else None,
timeout=60,
verify=verify_ssl,
)
except requests.exceptions.SSLError as e:
print(f"SSL error: {e}")
logger.error("SSL error during deletion: %s", e) # noqa: TRY400
return False
except requests.exceptions.ConnectionError as e:
print(f"Connection error: {e}")
logger.error("Connection error during deletion: %s", e) # noqa: TRY400
return False
if response.status_code == 200:
print(f"Deleted {project} version {release}.")
logger.info("Deleted %s version %s.", project, release)
return True
print(f"Failed to delete version {release} of project {project}: {response.reason}")
logger.error("Failed to delete version %s of project %s: %s", release, project, response.reason)
return False


def get_env(env_key: str) -> str | None:
"""Get environment variable from .env file or environment

Parameters
----------
env_key : str
Name of the environment variable

Returns
-------
str | None
Value of the variable or None if not defined.
"""
try:
with open(".env", encoding="utf-8") as file:
for line in file:
if line.startswith(f"{env_key}="):
try:
return re.split(r"=|\s", line)[1]
except IndexError:
return None
except FileNotFoundError:
pass
except PermissionError:
print("WARNING: No permission to read '.env' file.")
return os.getenv(env_key)


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

Expand Down Expand Up @@ -361,6 +365,12 @@ def greater_zero(value):
type=str,
default=config.get("CERT_PATH"),
)
parser.add_argument(
"-v",
"--verbose",
help="Verbose output",
action="store_true",
)
args = parser.parse_args()

if (args.delete or args.max_versions) and not args.api_key:
Expand All @@ -374,6 +384,8 @@ def greater_zero(value):
def main():
"""Package documents and upload them to docat server"""
args = get_args()
configure_logging(args.verbose)
logger.debug("Parsed command line arguments: %s", args)

if not args.insecure:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
Expand All @@ -386,9 +398,10 @@ def main():
args.release = module.__version__
except AttributeError:
args.release = "unknown"
# Check if new release should be published
logger.debug("Using release version %s", args.release)

if not re.match(r"^((0|[1-9]\d*)\.?)*$", args.release):
print(f"Skip upload of un-released version '{args.release}'")
logger.info("Skip upload of un-released version '%s'", args.release)
return

if args.delete:
Expand Down
Loading
Loading