|
| 1 | +""" |
| 2 | +Create Logbook Entry |
| 3 | +-------------------- |
| 4 | +
|
| 5 | +Simple wrapper around pylogbook to create logbook entries via python |
| 6 | +from commandline. |
| 7 | +
|
| 8 | +**Arguments:** |
| 9 | +
|
| 10 | +*--Optional--* |
| 11 | +
|
| 12 | +- **text** *(str)*: |
| 13 | +
|
| 14 | + Text to be written into the new logbook entry. |
| 15 | +
|
| 16 | + default: ```` |
| 17 | +
|
| 18 | +
|
| 19 | +- **files** *(PathOrStr)*: |
| 20 | +
|
| 21 | + Files to attach to the new logbook entry. |
| 22 | +
|
| 23 | +
|
| 24 | +- **filenames** *(OptionalStr)*: |
| 25 | +
|
| 26 | + Filenames to be used with the given files. If omitted, the original |
| 27 | + filenames will be used. |
| 28 | +
|
| 29 | +
|
| 30 | +- **logbook** *(str)*: |
| 31 | +
|
| 32 | + Name of the logbook to create the entry in. |
| 33 | +
|
| 34 | + default: ``LHC_OMC`` |
| 35 | +
|
| 36 | +
|
| 37 | +- **pdf2png**: |
| 38 | +
|
| 39 | + Convert pdf files to png and also upload these. |
| 40 | +
|
| 41 | + action: ``store_true`` |
| 42 | +
|
| 43 | +
|
| 44 | +- **tags** *(str)*: |
| 45 | +
|
| 46 | + Tags to be added to the event. |
| 47 | +
|
| 48 | +""" |
| 49 | +import mimetypes |
| 50 | +from pathlib import Path |
| 51 | +from typing import Iterable, Union, List |
| 52 | + |
| 53 | +import urllib3 |
| 54 | +from requests.exceptions import HTTPError, ConnectionError, ConnectTimeout |
| 55 | + |
| 56 | +from generic_parser import entrypoint, EntryPointParameters |
| 57 | +from omc3.utils.iotools import PathOrStr, OptionalStr |
| 58 | +from omc3.utils.logging_tools import get_logger |
| 59 | +from omc3.utils.mock import cern_network_import |
| 60 | +from omc3.utils.rbac import RBAC |
| 61 | +pylogbook = cern_network_import("pylogbook") # raises ImportError if used |
| 62 | + |
| 63 | +# for typing: |
| 64 | +try: |
| 65 | + from pylogbook._attachment_builder import AttachmentBuilder, AttachmentBuilderType |
| 66 | + from pylogbook.models import Event |
| 67 | +except ImportError: |
| 68 | + AttachmentBuilderType, Event = type(None), type(None) |
| 69 | + AttachmentBuilder = None |
| 70 | + |
| 71 | +# disables unverified HTTPS warning for cern-host |
| 72 | +urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) |
| 73 | + |
| 74 | +# Possible errors during RBAC connection |
| 75 | +CONNECTION_ERRORS = (HTTPError, ConnectionError, ConnectTimeout, ImportError, RuntimeError) |
| 76 | + |
| 77 | +OMC_LOGBOOK = "LHC_OMC" |
| 78 | +PNG_DPI = 300 # dpi for converted png (from pdf) |
| 79 | + |
| 80 | +LOGGER = get_logger(__name__) |
| 81 | + |
| 82 | +def get_params(): |
| 83 | + return EntryPointParameters( |
| 84 | + logbook=dict( |
| 85 | + type=str, |
| 86 | + help="Name of the logbook to create the entry in.", |
| 87 | + default=OMC_LOGBOOK, |
| 88 | + ), |
| 89 | + text=dict( |
| 90 | + type=str, |
| 91 | + help="Text to be written into the new logbook entry.", |
| 92 | + default="", |
| 93 | + ), |
| 94 | + files=dict( |
| 95 | + type=PathOrStr, |
| 96 | + nargs="+", |
| 97 | + help="Files to attach to the new logbook entry.", |
| 98 | + ), |
| 99 | + filenames=dict( |
| 100 | + type=OptionalStr, |
| 101 | + nargs="+", |
| 102 | + help=( |
| 103 | + "Filenames to be used with the given files. " |
| 104 | + "If omitted, the original filenames will be used." |
| 105 | + ), |
| 106 | + ), |
| 107 | + tags=dict( |
| 108 | + type=str, |
| 109 | + nargs="+", |
| 110 | + help="Tags to be added to the event.", |
| 111 | + ), |
| 112 | + pdf2png=dict( |
| 113 | + action="store_true", |
| 114 | + help="Convert pdf files to png and also upload these." |
| 115 | + ) |
| 116 | + ) |
| 117 | + |
| 118 | + |
| 119 | +@entrypoint(get_params(), strict=True) |
| 120 | +def main(opt) -> Event: |
| 121 | + """ Create a new entry in the logbook and attach the given files. """ |
| 122 | + # do attachments first as it also tests if files are there etc. |
| 123 | + attachments = _get_attachments(opt.files, opt.filenames, opt.pdf2png) |
| 124 | + |
| 125 | + # initialize logbook client |
| 126 | + rbac_token = _get_rbac_token() |
| 127 | + client = pylogbook.Client(rbac_token=rbac_token) |
| 128 | + logbook = pylogbook.ActivitiesClient(opt.logbook, client=client) |
| 129 | + |
| 130 | + # create event and upload attachments |
| 131 | + event = logbook.add_event(opt.text, tags=opt.tags or ()) |
| 132 | + for attachment in attachments: |
| 133 | + event.attach_content( |
| 134 | + contents=attachment.contents, |
| 135 | + mime_type=attachment.mime_type, |
| 136 | + name=attachment.short_name, |
| 137 | + ) |
| 138 | + return event |
| 139 | + |
| 140 | + |
| 141 | +# Private Functions ------------------------------------------------------------ |
| 142 | + |
| 143 | +def _get_rbac_token() -> str: |
| 144 | + """ Get an RBAC token, either by location or by Kerberos. """ |
| 145 | + rbac = RBAC(application=f"omc3.{Path(__file__).stem}") |
| 146 | + try: |
| 147 | + rbac.authenticate_location() |
| 148 | + except CONNECTION_ERRORS as e: |
| 149 | + LOGGER.debug( |
| 150 | + f"Getting RBAC token from location failed. " |
| 151 | + f"{e.__class__.__name__}: {str(e)}" |
| 152 | + ) |
| 153 | + else: |
| 154 | + LOGGER.info(f"Logged in to RBAC via location as user {rbac.user}.") |
| 155 | + return rbac.token |
| 156 | + |
| 157 | + try: |
| 158 | + rbac.authenticate_kerberos() |
| 159 | + except CONNECTION_ERRORS as e: |
| 160 | + LOGGER.debug( |
| 161 | + f"Getting RBAC token via Kerberos failed. " |
| 162 | + f"{e.__class__.__name__}: {str(e)}" |
| 163 | + ) |
| 164 | + else: |
| 165 | + LOGGER.info(f"Logged in to RBAC via Kerberos as user {rbac.user}.") |
| 166 | + return rbac.token |
| 167 | + |
| 168 | + # DEBUG ONLY --- |
| 169 | + # try: |
| 170 | + # rbac.authenticate_explicit(user=input("Username: "), password=input("Password: ")) |
| 171 | + # except CONNTECTION_ERRORS as e: |
| 172 | + # LOGGER.debug( |
| 173 | + # f"Explicit RBAC failed. " |
| 174 | + # f"{e.__class__.__name__}: {str(e)}" |
| 175 | + # ) |
| 176 | + # else: |
| 177 | + # LOGGER.info(f"Logged in to RBAC as user {rbac.user}.") |
| 178 | + # return rbac.token |
| 179 | + |
| 180 | + raise NameError("Could not get RBAC token.") |
| 181 | + |
| 182 | + |
| 183 | +def _get_attachments(files: Iterable[Union[str, Path]], |
| 184 | + filenames: Iterable[str] = None, |
| 185 | + pdf2png: bool = False) -> List[AttachmentBuilderType]: |
| 186 | + """ Read the file-attachments and assign their names. """ |
| 187 | + if files is None: |
| 188 | + return [] |
| 189 | + |
| 190 | + if filenames and len(filenames) != len(files): |
| 191 | + raise ValueError( |
| 192 | + f"List of files (length {len(files)}) and " |
| 193 | + f"list of filenames (length: {filenames}) " |
| 194 | + f"need to be of equal length." |
| 195 | + ) |
| 196 | + |
| 197 | + _add_mimetypes(files) |
| 198 | + if filenames is None: |
| 199 | + filenames = [None] * len(files) |
| 200 | + |
| 201 | + # TODO: Return iterator, reading attachments only when needed? |
| 202 | + attachments = [] |
| 203 | + for filepath, filename in zip(files, filenames): |
| 204 | + filepath = Path(filepath) |
| 205 | + attachment = AttachmentBuilder.from_file(filepath) |
| 206 | + attachments.append(attachment) |
| 207 | + |
| 208 | + # Convert pdf to png if desired |
| 209 | + png_attachment = None |
| 210 | + if pdf2png and filepath.suffix.lower() == ".pdf": |
| 211 | + png_attachment = _convert_pdf_to_png(filepath) |
| 212 | + |
| 213 | + if png_attachment: |
| 214 | + attachments.append(png_attachment) |
| 215 | + |
| 216 | + # Assign new filenames |
| 217 | + if filename and filename.lower() != "none": |
| 218 | + attachment.short_name = filename |
| 219 | + if png_attachment: |
| 220 | + png_attachment.short_name = filename.replace(".pdf", ".png").replace(".PDF", ".png") |
| 221 | + |
| 222 | + return attachments |
| 223 | + |
| 224 | + |
| 225 | +def _add_mimetypes(files: Iterable[Union[str, Path]]): |
| 226 | + """ Adds all unknown suffixes as plain/text, which should suffice for our |
| 227 | + purposes. |
| 228 | + TODO: if it's a binary sdds file, it should be 'application/octet-stream' |
| 229 | + see https://stackoverflow.com/a/6783972/5609590 |
| 230 | +
|
| 231 | + This is done, because the attachment builder uses the mimetypes package |
| 232 | + to guess the mimetype and if it doesn't find it (e.g. `.tfs` or `.dat`) |
| 233 | + raises an error. |
| 234 | + """ |
| 235 | + if files is None: |
| 236 | + return |
| 237 | + |
| 238 | + for f in files: |
| 239 | + f_path = Path(f) |
| 240 | + mime, _ = mimetypes.guess_type(f_path.name) |
| 241 | + if mime is None: |
| 242 | + mimetypes.add_type("text/plain", f.suffix, strict=True) |
| 243 | + |
| 244 | + |
| 245 | +def _convert_pdf_to_png(filepath: Path): |
| 246 | + """ Convert the first page of a pdf file into a png image. """ |
| 247 | + try: |
| 248 | + import fitz # PyMuPDF, imported as fitz for backward compatibility reasons |
| 249 | + except ImportError: |
| 250 | + LOGGER.warning("Missing `pymupdf` package. PDF conversion not possible.") |
| 251 | + return None |
| 252 | + |
| 253 | + doc = fitz.open(filepath) # open document |
| 254 | + |
| 255 | + if len(doc) > 1: |
| 256 | + LOGGER.warning(f"Big PDF-File with {len(doc)} pages found. " |
| 257 | + "Conversion only implemented for single-page files. " |
| 258 | + "Skipping conversion.") |
| 259 | + return None |
| 260 | + |
| 261 | + pixmap = doc[0].get_pixmap(dpi=PNG_DPI) # only first page |
| 262 | + attachment = AttachmentBuilder.from_bytes( |
| 263 | + contents=pixmap.tobytes("png"), |
| 264 | + mime_type="image/png", |
| 265 | + name=filepath.with_suffix(".png").name |
| 266 | + ) |
| 267 | + return attachment |
| 268 | + |
| 269 | + |
| 270 | +# Script Mode ------------------------------------------------------------------ |
| 271 | + |
| 272 | +if __name__ == '__main__': |
| 273 | + main() |
| 274 | + |
0 commit comments