Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# (c) Stefan Countryman, 2019
3"""
4``FileHandler`` classes that log file uploads to Slack.
6Slack API documentation:
7https://api.slack.com/methods/chat.postMessage
8https://api.slack.com/messaging/composing/formatting#
10(Link to permissions in top right of the above pages.)
11"""
13import json
14import socket
15import logging
16from llama.flags import FlagDict
17from llama.filehandler import (
18 GenerationError,
19 JSONFile,
20)
21from llama.com.utils import UploadReceipt
22from llama import detectors
23from llama.files.skymap_info import (
24 SkymapInfo,
25 gracedb_url,
26)
27from llama.serve.gui import gui_url
28# have to import some things that SkymapInfo depends on so that we can make
29# uploads for them here.
30from llama.files.lvc_gcn_xml import LvcGcnXml, LvcRetractionXml
31from llama.files.lvalert_json import LVAlertJSON
32from llama.com.slack import (
33 SLACK_CHANNELS,
34 SLACK_TOKENS,
35 client,
36)
38LOGGER = logging.getLogger(__name__)
39# initialize an empty list of org-specific vetoes and populate it below
40VETOES = {obs: list() for obs in SLACK_CHANNELS}
41FMT = """VERSIONED_FILENAME: *{filename_for_download}*
42FILENAME: {filename}
43GRACEID: `{graceid}`
44LLAMAID: `{eventid}`
45GRACEDB_LINK: {link}
46LLAMA_LINK: {llama_url}
47ANALYSIS_SERVER: {host}
48EVENT_DIRECTORY: {eventdir}
49UPLOAD_FILEHANDLER: `{uploadhandler}`
50UPLOADER: `{selftype}`
51WALL_TIME: {walltime} sec
52EVENT_SNAPSHOT: {githash}
53TAGS: #{graceid} #{eventid} #{filename}"""
56def icecube_upload_flag_false(eventdir):
57 """Return whether a trigger directory has its "ICECUBE_UPLOAD" flag set to
58 "false"."""
59 return FlagDict(eventdir)['ICECUBE_UPLOAD'] == 'false'
62VETOES["IceCube"].append(icecube_upload_flag_false)
65# pylint: disable=too-many-ancestors,duplicate-bases
66class SlackReceipt(UploadReceipt, JSONFile):
67 """
68 A log file created after an upload attempt to Slack. Use these
69 filehandlers to upload files to slack. Additional vetoes that turn a
70 specific upload receipt off are specified in ``VETOES``.
71 """
73 ORGANIZATION = None
75 _REQUIRED = ('ORGANIZATION',)
77 @staticmethod
78 def _classname_format(org):
79 """Get the ``CLASSNAME_FMT`` attribute for organization ``org``."""
80 assert org in SLACK_CHANNELS
81 return f"RctSlk{getattr(detectors, org).abbrev.capitalize()}{{}}"
83 @classmethod
84 def set_class_attributes(cls, subclass):
85 """See ``UploadReceipt.set_class_attributes``; this method first sets
86 the ``FILENAME_FMT`` and ``CLASSNAME_FMT`` attributes based on
87 ``subclass.ORGANIZATION``. Also adds ``SkymapInfo`` to
88 ``subclass.DEPENDENCIES`` if it is not already a member and sets
89 ``class_vetoes`` to the ones defined for this organization.
90 """
91 abbrev = getattr(detectors, subclass.ORGANIZATION).abbrev.lower()
92 fmt = f"rct_slk_{abbrev}_{{}}.json"
93 subclass.FILENAME_FMT = fmt
94 setattr(subclass, 'class_vetoes',
95 tuple((v, None) for v in VETOES[subclass.ORGANIZATION]))
96 super().set_class_attributes(subclass)
97 for attr in ['DEPENDENCIES', 'UR_DEPENDENCIES']:
98 if SkymapInfo not in getattr(subclass, attr):
99 setattr(subclass, attr, tuple(list(getattr(subclass, attr)) +
100 [SkymapInfo]))
101 return subclass
103 @property
104 def upload_title(self):
105 """Print a title for this upload."""
106 return "{} from {} (LLAMA internal ID: {})".format(
107 self.UPLOAD.FILENAME,
108 SkymapInfo(self).graceid,
109 self.eventid,
110 )
112 @property
113 def comment(self):
114 """An explanation of this upload ``FileHandler`` to be printed to team
115 slack."""
116 skyinfo = SkymapInfo(self)
117 link = gracedb_url(skyinfo.graceid)
118 calling_fh = self if (self.parent is None) else self.parent
119 upload = self.UPLOAD(calling_fh) # pylint: disable=not-callable
120 return FMT.format(
121 filename_for_download=upload.filename_for_download,
122 filename=upload.FILENAME,
123 llama_url=gui_url(upload),
124 uploadhandler=type(upload).__name__,
125 selftype=type(self).__name__,
126 graceid=skyinfo.graceid,
127 link=link,
128 eventid=self.eventid,
129 eventdir=calling_fh.eventdir,
130 walltime=upload.meta.wall_time,
131 githash=calling_fh.git.current_hash,
132 host=socket.gethostname(),
133 )
135 def _generate(self):
136 from slack.errors import SlackApiError
137 calling_fh = self if (self.parent is None) else self.parent
138 upload = self.UPLOAD(calling_fh) # pylint: disable=not-callable
139 res = dict()
140 for chan in SLACK_CHANNELS[self.ORGANIZATION]:
141 with upload.open('rb') as upfile:
142 try:
143 clnt = client(SLACK_TOKENS[self.ORGANIZATION])
144 res[chan] = clnt.files_upload(
145 channels=chan, # only upload to one channel at a time
146 filename=upload.filename_for_download,
147 file=upfile,
148 title=self.upload_title,
149 initial_comment=self.comment,
150 )
151 if not (res[chan].get('ok') and
152 res[chan].status_code == 200):
153 msg = (f"Error while uploading {upload}. status: "
154 f"{res[chan].status_code}, response: "
155 f"{res[chan].data}")
156 LOGGER.error(msg)
157 raise SlackApiError(msg, res[chan])
158 except SlackApiError as err:
159 LOGGER.error("Error while uploading: %s", err)
160 raise GenerationError(str(err))
161 self._write_json({k: v.data for k, v in res.items()})
164class SlackReceiptLlama(SlackReceipt):
165 """A ``SlackReceipt`` for organization ``LLAMA``."""
167 ORGANIZATION = "LLAMA"
168 CLASSNAME_FMT = SlackReceipt._classname_format(ORGANIZATION)
171class SlackReceiptIcecube(SlackReceipt):
172 """A ``SlackReceipt`` for organization ``IceCube``."""
174 ORGANIZATION = "IceCube"
175 CLASSNAME_FMT = SlackReceipt._classname_format(ORGANIZATION)
178# register SkymapInfo as a slack upload
179SlackReceiptLlama.upload_this()(SkymapInfo)
180SlackReceiptLlama.upload_this()(LvcGcnXml)
181SlackReceiptLlama.upload_this()(LvcRetractionXml)
182SlackReceiptLlama.upload_this()(LVAlertJSON)