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"""
4Classes and utilities for communicating with external organizations (e.g.
5downloading inputs and uploading results, sending alerts, etc.). Pretty much
6everything having to do with data transfer should be inheriting from primitives
7in this package.
8"""
10import re
11import sys
12from llama.filehandler import FileHandler
13from llama.filehandler.mixins import (
14 ObservingVetoMixin,
15 OnlineVetoMixin,
16)
17from llama.flags import FlagDict
20def upload_flag_false(eventdir):
21 """Return whether a trigger directory has its "UPLOAD" flag set to
22 "false"."""
23 return FlagDict(eventdir)['UPLOAD'] == 'false'
26# pylint: disable=too-many-ancestors, abstract-method
27class UploadReceipt(FileHandler, OnlineVetoMixin, ObservingVetoMixin):
28 """
29 A log file created as a side effect of uploading some other original
30 file to some remote server (like gw-astronomy.org or gracedb). The
31 generate() function should contain instructions on how to upload the
32 original file and should log helpful information to the UploadReceipt
33 file. The presence of an UploadReceipt file should be taken as evidence
34 that an upload has succeeded.
36 Define the file that is to be uploaded as ``UPLOAD`` when subclassing
37 ``UploadReceipt``.
38 """
40 class_vetoes = (
41 (upload_flag_false, None),
42 )
44 _REQUIRED = ("UPLOAD", "FILENAME_FMT", "CLASSNAME_FMT")
46 UPLOAD = None
47 FILENAME_FMT = None
48 CLASSNAME_FMT = None
50 @classmethod
51 def set_class_attributes(cls, subclass):
52 """See ``FileHandler.set_class_attributes``; this method additionally
53 sets the ``FILENAME`` and ``DEPENDENCIES`` attributes based on
54 ``subclass.UPLOAD``."""
55 fmt = subclass.FILENAME_FMT
56 subclass.FILENAME = fmt.format(subclass.UPLOAD.FILENAME)
57 subclass.DEPENDENCIES = (subclass.UPLOAD,)
58 super().set_class_attributes(subclass)
59 return subclass
61 @classmethod
62 def upload_this(cls, *args, namespace=None, **kwargs):
63 """Return a decorator that automatically uploads ``FileHandler``
64 classes by implementing this class with that ``FileHandler`` class as
65 the ``UPLOAD`` attribute. Additional attributes are set by
66 ``cls.decorator_dict`` using the ``FileHandler`` class decorated by
67 the returned ``decorator``, ``*args``, and ``**kwargs``.
69 Parameters
70 ----------
71 *args
72 Positional arguments to pass to ``cls.decorator_dict`` (after the
73 ``FileHandler`` class decorated by the returned ``decorator``,
74 which takes the first positional argument position).
75 namespace : dict, optional
76 The namespace to which the new ``UploadReceipt`` created by
77 ``decorator`` will be added. The default behavior is to define
78 ``UploadReceipt`` in the global namespace of the calling frame
79 (so that using the decorator is equivalent to manually defining the
80 ``UploadReceipt`` class with the decorated ``FileHandler`` as its
81 upload immediately after defining the ``FileHandler`` to be
82 decorated).
83 **kwargs
84 Keyword arguments to pass to ``cls.decorator_dict``.
86 Returns
87 -------
88 decorator : func
89 A decorator that takes a ``FileHandler`` and returns an
90 implementation of this class with the decorated ``FileHandler`` as
91 its ``UPLOAD`` file (along with any other implementation attributes
92 calculated by this class's ``decorator_dict`` function using the new
93 ``FileHandler`` class, ``*args``, and ``**kwargs`` as input). The
94 generated classname will be named
95 ``cls.CLASSNAME_FMT.format(upload)`` (where ``upload`` is the
96 decorated ``FileHandler``) and will be added as a variable with
97 that name to ``namespace``.
99 Raises
100 ------
101 TypeError
102 If there is only one positional argument provided which is itself a
103 ``type`` (since this usually indicates that the developer forgot to
104 **call** this method to get the *actual* decorator and instead
105 applied this function as if it were a decorator to the
106 ``FileHandler`` class they intend to decorate).
107 Exception
108 ``decorator_dict`` can, in principle, throw other exceptions as well.
109 See the specific implementation for details.
110 """
111 if namespace is None and len(args) == 1 and isinstance(args[0], type):
112 raise TypeError("Didn't expect a ``type`` as the only positional "
113 "argument (with no keyword arguments provided): "
114 f"{args[0]} did you forget to pass ``globals()`` "
115 "as the first argument to ``upload_this`` to get "
116 f"the actual decorator for {args[0]}?")
117 if namespace is None:
118 namespace = sys._getframe(1).f_globals
119 module = namespace['__name__']
121 def decorator(upload):
122 """Create a new ``UploadReceipt`` subclass implementation to upload
123 ``upload`` (subclass of ``FileHandler``). See
124 ``UploadReceipt.upload_this`` for information on which
125 namespace/module the new ``UploadReceipt`` is saved in.
127 Raises
128 ------
129 TypeError
130 If ``cls.decorator_dict`` failed to return a dictionary (or
131 other mutable dict-like object); if ``*args`` and ``**kwargs``
132 don't conform to the actual signature of this class's
133 ``decorator_dict`` implementation; or if ``upload`` is not a
134 ``FileHandler`` subclass.
135 AssertionError
136 If the class name generated from CLASSNAME_FMT is not a valid
137 variable name or if this ``UploadReceipt.decorator_dict``
138 implementation does not accept a positional variable called
139 ``'upload'`` as its first positional argument.
140 """
141 if not issubclass(upload, FileHandler):
142 raise TypeError("Expected ``FileHandler`` subclass, got: "
143 f"{upload}")
144 classname = cls.CLASSNAME_FMT.format(upload.__name__)
145 assert re.findall('^[a-zA-Z_][a-zA-Z0-9_]*$', classname)
146 vnames = cls.decorator_dict.__code__.co_varnames
147 if len(vnames) < 2 or vnames[:2] != ('cls', 'upload'):
148 raise AssertionError(
149 f"This ``UploadReceipt`` subclass ({cls}) has defined"
150 "``decorator_dict`` with the wrong function signature: "
151 f"{cls.__name__}.decorator_dict({', '.join(vnames)}) "
152 "You MUST have (cls, upload) as the names of the first "
153 "positional parameters since ``decorator_dict`` is expected "
154 "to be a classmethod taking the decorated ``FileHandler`` "
155 "class as its first argument (before receiving the *args "
156 "and **kwargs passed from ``upload_this``. "
157 "This code style decision is enforced to avoid subtle "
158 r"implementation bugs; sorry ¯\_(ツ)_/¯"
159 )
160 newclassdict = cls.decorator_dict(upload, *args, **kwargs)
161 newclassdict['UPLOAD'] = upload
162 newclassdict['__module__'] = module
163 newclass = type(classname, (cls,), newclassdict)
164 cls.set_class_attributes(newclass)
165 namespace[classname] = newclass
166 return upload
168 return decorator
170 @classmethod
171 def decorator_dict(cls, upload): # pylint: disable=unused-argument
172 """See ``UploadReceipt.upload_this``. Takes the decorated
173 ``FileHandler`` class as its first argument. By default takes no
174 additional arguments.
176 Parameters
177 ----------
178 upload
179 The decorated ``FileHandler`` class that is being registered for
180 upload.
182 Returns
183 -------
184 newclassdict : dict
185 A dictionary that can be passed to ``type`` to specify the
186 attributes of a new class.
187 """
188 return dict()