Skip to content

Commit 749c4bd

Browse files
authored
Logbook wrapper (#412)
* Logbook Entry Creator * RBAC
1 parent 2bb6b5b commit 749c4bd

File tree

10 files changed

+951
-4
lines changed

10 files changed

+951
-4
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
# OMC3 Changelog
22

3-
#### 2023-04-20 - v0.8.0 - _jdilly_
3+
#### 2023-04-27 - v0.9.0 - _jdilly_
44

5+
- Added:
6+
- RBAC token provider in omc3.utils.rbac
7+
- pylogbook wrapper in omc3.scripts.create_logbook_entry
58

9+
#### 2023-04-20 - v0.8.0 - _jdilly_
610

711
- Fix:
812
- Changed all `pandas`/`tfs-pandas` `append()` and `join()` to `concat()`

omc3/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
__title__ = "omc3"
1212
__description__ = "An accelerator physics tools package for the OMC team at CERN."
1313
__url__ = "https://github.com/pylhc/omc3"
14-
__version__ = "0.8.0"
14+
__version__ = "0.9.0"
1515
__author__ = "pylhc"
1616
__author_email__ = "[email protected]"
1717
__license__ = "MIT"
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
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+

omc3/utils/iotools.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,15 @@ def __new__(cls, value):
9393
return value
9494

9595

96+
class OptionalStr(metaclass=get_instance_faker_meta(str, type(None))):
97+
"""A class that allows `str` or `None`.
98+
Can be used in string-lists when individual entries can be `None`."""
99+
def __new__(cls, value):
100+
if isinstance(value, str):
101+
value = value.strip("\'\"") # behavior like dict-parser, IMPORTANT FOR EVERY STRING-FAKER
102+
return value
103+
104+
96105
def convert_paths_in_dict_to_strings(dict_: dict) -> dict:
97106
"""Converts all Paths in the dict to strings, including those in iterables."""
98107
dict_ = dict_.copy()

omc3/utils/rbac-krb5.conf

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
include /etc/krb5.conf
2+
3+
[libdefaults]
4+
dns_canonicalize_hostname = False

0 commit comments

Comments
 (0)