Coverage for /opt/conda/envs/apienv/lib/python3.10/site-packages/daiquiri/core/saving/base.py: 85%
163 statements
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-14 02:13 +0000
« prev ^ index » next coverage.py v7.6.4, created at 2024-11-14 02:13 +0000
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3import json
4import os
5import logging
6import re
7import string
8from abc import abstractmethod, abstractproperty
9import time
10from flask import g
11from marshmallow import fields
13from daiquiri.core import CoreBase, CoreResource, marshal
14from daiquiri.core.utils import loader
15from daiquiri.core.schema import ErrorSchema, MessageSchema
16from daiquiri.core.schema.saving import SavingSchema
18logger = logging.getLogger(__name__)
21class Saving:
22 def __init__(self, config, *args, **kwargs):
23 kwargs["config"] = config
25 try:
26 self._saving = loader(
27 "daiquiri.core.saving",
28 "SavingHandler",
29 config["saving_type"],
30 *args,
31 **kwargs,
32 )
33 except KeyError:
34 logger.warning(
35 "Configuration section for saving missing, saving component not configured"
36 )
37 self._saving = None
39 def init(self):
40 return self._saving
43class SavingResource(CoreResource):
44 @marshal(
45 inp={
46 "sessionid": fields.Int(),
47 "sampleid": fields.Int(),
48 "subsampleid": fields.Int(),
49 },
50 out=[[200, SavingSchema(), "The current saving setup"]],
51 )
52 def get(self, **kwargs):
53 """Get the current saving configuration"""
54 kwargs["sessionid"] = g.blsession.get("sessionid")
55 return {"arguments": self._parent.get_schema_fields(**kwargs)}, 200
57 @marshal(
58 inp=SavingSchema,
59 out=[
60 [200, MessageSchema(), "Updated saving arguments"],
61 [400, ErrorSchema(), "Could not update saving arguments"],
62 ],
63 )
64 def patch(self, **kwargs):
65 """Update the saving arguments"""
66 if False:
67 return {"message": "saving arguments updated"}
68 else:
69 return {"error": "Could not update saving arguments"}, 400
72class FormatterObject:
73 """Used in interpolating Saving arguments"""
75 def __init__(self, resource, srepr, info):
76 self._resource = resource
77 self._srepr = srepr
78 self._info = info
80 def __str__(self):
81 return self._srepr
83 def _make_safe(self, value):
84 """Make a value safe for the file system
86 Replaces spaces and special characters with underscore
87 """
88 if value is None:
89 return ""
91 if isinstance(value, dict):
92 return json.dumps(value, indent=2)
93 else:
94 return re.sub(r"[^\w\.-]", "_", value)
96 def __getattr__(self, attr):
97 try:
98 return self._make_safe(self._info[attr])
99 except KeyError:
100 raise AttributeError(
101 f"{self._resource}.{attr} (available: {list(self._info.keys())}"
102 ) from None
105class SavingFormatter(string.Formatter):
106 """Used for interpolating Saving arguments"""
108 def __init__(self, handler, actor_args, partial_evaulation=False):
109 """
110 :param SavingHandler handler:
111 """
112 super().__init__()
113 self._handler = handler
114 self._actor_args = actor_args
115 self._partial_evaluation = partial_evaulation
117 def get_value(self, key, args, kwds):
118 """Order of finding the key:
120 - format named arguments
121 - handler config
122 - actor arguments
123 """
124 if isinstance(key, str):
125 if key == "epoch":
126 return str(int(time.time()))
127 try:
128 return kwds[key]
129 except KeyError:
130 pass
131 try:
132 return self._handler._config[key]
133 except KeyError:
134 pass
135 try:
136 return self._get_from_actor_args(key)
137 except KeyError:
138 pass
140 # TODO: Which edge cases does this not work for?
141 if self._partial_evaluation:
142 return f"{{{key}}}"
144 return super().get_value(key, args, kwds)
146 @property
147 def _metadata(self):
148 return self._handler._metadata
150 def _get_from_actor_args(self, key):
151 """Get key from actor arguments and allow special metadata keys
152 to be compounded like `{sessionid.name}`
154 :param str key:
155 :returns FormatterObject:
156 """
157 # TODO: too specific
158 if key == "componentid":
159 value = self._actor_args["sampleid"]
160 else:
161 value = self._actor_args[key]
163 info = None
164 if key == "sessionid":
165 info = self._metadata.get_sessions(sessionid=value, no_context=True)
166 elif key == "sampleid":
167 info = self._metadata.get_samples(sampleid=value, no_context=True)
168 elif key == "componentid":
169 info = self._metadata.get_components(sampleid=value, no_context=True)
170 if info["rows"]:
171 info = info["rows"][0]
172 elif key == "subsampleid":
173 info = self._metadata.get_subsamples(subsampleid=value, no_context=True)
174 elif key == "datacollectionid":
175 info = self._metadata.get_datacollections(
176 datacollectionid=value, no_context=True
177 )
178 if info is None:
179 info = {}
180 return FormatterObject(
181 key,
182 json.dumps(value, indent=2) if isinstance(value, dict) else str(value),
183 info,
184 )
187class SavingHandler(CoreBase):
188 _require_session = True
189 _require_blsession = True
190 _base_url = "saving"
192 _callbacks = []
193 _formatter = string.Formatter()
195 def setup(self):
196 self.saving_arguments = self._config.get("saving_arguments", {})
197 self.register_route(SavingResource, "")
199 def eval_saving_arguments(
200 self,
201 eval_args,
202 extra_saving_args=None,
203 raise_on_missing=True,
204 warn_on_missing=False,
205 partial_evaulation=False,
206 saving_arguments=None,
207 ):
208 logger.debug(f"saving arguments {eval_args}")
209 formatter = SavingFormatter(self, eval_args, partial_evaulation)
211 if saving_arguments:
212 all_saving_args = {**saving_arguments}
213 else:
214 all_saving_args = {**self.saving_arguments}
215 if extra_saving_args:
216 all_saving_args.update(extra_saving_args)
218 kwargs = {}
219 for name, template in all_saving_args.items():
220 try:
221 kwargs[name] = formatter.format(template)
222 except (KeyError, AttributeError):
223 if raise_on_missing:
224 raise
225 # With warnings enabled the unreplaced key will not be available
226 elif warn_on_missing:
227 logger.warning(
228 f"Could not evaluate template variable {name}: {template}"
229 )
230 else:
231 kwargs[name] = template
233 return kwargs
235 def get_schema_fields(self, **eval_args):
236 """Return the saving arguments"""
237 saving_arguments = self.eval_saving_arguments(
238 eval_args, raise_on_missing=False, partial_evaulation=True
239 )
240 saving_arguments["proposal_root_path"] = self.proposal_root_path
241 return saving_arguments
243 def set_filename(self, extra_saving_args=None, set_metadata=True, **eval_args):
244 """Define the file name"""
245 self._set_filename(
246 **self.eval_saving_arguments(eval_args, extra_saving_args=extra_saving_args)
247 )
248 if set_metadata and self._config.get("saving_metadata"):
249 self.set_metadata(self._config["saving_metadata"], **eval_args)
250 return self.filename
252 @abstractmethod
253 def _set_filename(self, **eval_args):
254 """Child class implementation of applying the saving arguments"""
255 pass
257 @abstractproperty
258 def filename(self):
259 """Child class implementation of returning the current filename"""
260 pass
262 def set_metadata(self, saving_arguments, warn_on_missing=True, **eval_args):
263 """Evaluate metadata required for metadata catalogue"""
264 evaluated = self.eval_saving_arguments(
265 eval_args,
266 saving_arguments=saving_arguments,
267 raise_on_missing=not warn_on_missing,
268 warn_on_missing=warn_on_missing,
269 )
270 self._set_metadata(**evaluated)
271 return evaluated
273 def _set_metadata(self, **metadata):
274 """Apply the evaluated metadata to whatever service is required
275 Should be overridden in the child class
276 """
277 pass
279 @abstractproperty
280 def create_root_path(self):
281 pass
283 @abstractmethod
284 def create_path(self, path):
285 pass
287 @property
288 def dirname(self):
289 return os.path.dirname(self.filename)
291 @property
292 def basename(self):
293 return os.path.basename(self.filename)
295 @property
296 def proposal_root_path(self):
297 pass