pdmv-http-client 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,431 @@
1
+ from enum import Enum
2
+ from typing import Union
3
+
4
+ from rest import McM
5
+ from rest.applications.mcm.invalidate_request import InvalidateDeleteRequests
6
+ from rest.utils.miscellaneous import pformat
7
+
8
+
9
+ class RootRequestReset(Enum):
10
+ """
11
+ Describes how a root request was reset so that
12
+ it is reusable in later steps to recreate its
13
+ chained requests.
14
+ """
15
+
16
+ # Root request was soft reset.
17
+ SOFT_RESET = 1
18
+
19
+ # Root request was fully reset.
20
+ # This request was validated before, so
21
+ # instead of repeating the validation,
22
+ # old results are reincluded again.
23
+ FULL_RESET = 2
24
+
25
+ # Root request keeps output
26
+ # (it created a big "RAW" dataset being used in other steps, chains, ...)
27
+ # Handle it so that it is not required to recreate it
28
+ # again.
29
+ KEEPS_OUTPUT = 3
30
+
31
+
32
+ class ChainRequestResubmitter:
33
+ """
34
+ Rewinds a chained request to its root
35
+ so that you can recreate the chain and attempt to
36
+ inject it again.
37
+
38
+ The scenario to use this is the following: Some configuration
39
+ applied in a campaign or chained campaign was wrong so that, after
40
+ applying a patch on these elements (that work as a template) you
41
+ need to recreate all the requests using this new change and reinject them.
42
+
43
+ Attributes:
44
+ mcm: McM client instance
45
+ """
46
+
47
+ def __init__(self, mcm: McM) -> None:
48
+ self._mcm = mcm
49
+ self._invalidator = InvalidateDeleteRequests(mcm=self._mcm)
50
+ self.logger = self._mcm.logger
51
+
52
+ def _root_request_to_approve(self, root_prepid: str) -> RootRequestReset:
53
+ """
54
+ This is a customization for resetting a root request so
55
+ that it ends up in an 'approve/approved' status and could
56
+ be easily reused to recreate a chained request and inject it
57
+ again.
58
+
59
+ Args:
60
+ root_prepid: Root request's prepid
61
+
62
+ Returns:
63
+ An attribute describing how the root request was processed.
64
+ """
65
+ data: Union[dict, None] = self._mcm.get(
66
+ object_type="requests", object_id=root_prepid
67
+ )
68
+ if not data:
69
+ raise ValueError(f"Root request {root_prepid} not found")
70
+
71
+ approval = data.get("approval")
72
+ status = data.get("status")
73
+ if approval == "submit" and status == "done":
74
+ keep_output: Union[list[bool], None] = data.get("keep_output")
75
+ if not keep_output:
76
+ raise ValueError("Keep output is not defined")
77
+
78
+ if all(keep_output):
79
+ # Avoid resetting this kind of root request
80
+ # Just reuse it as it is
81
+ return RootRequestReset.KEEPS_OUTPUT
82
+ else:
83
+ self.logger.warning(
84
+ (
85
+ "Root request (%s) is already done, resetting it, "
86
+ "announcing the invalidation, and setting it to approve/approved"
87
+ ),
88
+ root_prepid,
89
+ )
90
+ validation: Union[dict, None] = data.get("validation")
91
+ self.logger.debug("Validation results: %s", validation)
92
+ if not validation:
93
+ raise ValueError("Validation results not available")
94
+
95
+ # Reset the request.
96
+ reset_result = self._mcm.reset(prepid=root_prepid)
97
+ if not reset_result:
98
+ msg = f"Unable to reset the root request: {root_prepid}"
99
+ self.logger.error(msg)
100
+ raise RuntimeError(msg)
101
+
102
+ # Announce the invalidation.
103
+ self._invalidator._process_invalidation(request_prepids=[root_prepid])
104
+
105
+ # Force its status to approve/approved, re-including the validation.
106
+ data: dict = self._mcm.get(
107
+ object_type="requests", object_id=root_prepid
108
+ )
109
+ assert isinstance(data, dict)
110
+
111
+ data["validation"] = validation
112
+ data["approval"] = "approve"
113
+ data["status"] = "approved"
114
+
115
+ updated_result = self._mcm.update(
116
+ object_type="requests", object_data=data
117
+ )
118
+ if not updated_result or not updated_result.get("results"):
119
+ msg = f"Error forcing the root request status: {root_prepid}"
120
+ self.logger.error(msg)
121
+ raise RuntimeError(msg)
122
+
123
+ return RootRequestReset.FULL_RESET
124
+ else:
125
+ # Soft reset request
126
+ soft_reset_result = self._mcm.soft_reset(prepid=root_prepid)
127
+ self.logger.info("Soft reset result: %s", soft_reset_result)
128
+ if not soft_reset_result:
129
+ msg = (
130
+ f"Unable to soft reset the request ({root_prepid}) "
131
+ "and set its status to approve/approved"
132
+ )
133
+ self.logger.error(msg)
134
+ raise RuntimeError(msg)
135
+
136
+ return RootRequestReset.SOFT_RESET
137
+
138
+ def _include_tag(self, request_prepid: str, tag: str) -> None:
139
+ """
140
+ Includes a new tag for the `request`
141
+
142
+ Args:
143
+ request_prepid (str): Request identifier
144
+ tag (str): New tag to include
145
+ """
146
+ request_data: dict = self._mcm.get("requests", object_id=request_prepid)
147
+ assert isinstance(request_data, dict)
148
+
149
+ request_data["tags"] += [tag]
150
+ tag_result = self._mcm.update("requests", request_data)
151
+ self.logger.debug("Include `tag` result: %s", tag_result)
152
+ if not tag_result or not tag_result.get("results"):
153
+ msg = f"Unable to include a tracking tag for the root request: {request_prepid}"
154
+ self.logger.error(msg)
155
+ raise RuntimeError(msg)
156
+
157
+ def _pick_target_campaign(
158
+ self, chained_campaign_prepid: str, datatier: str, soft_datatier: bool
159
+ ) -> Union[str, None]:
160
+ """
161
+ Retrieves the campaign related to the desired data tier for a given chained campaign
162
+
163
+ Args:
164
+ chained_campaign_prepid: Chained campaign identifier.
165
+ datatier: Data tier related to the campaign to retrieve.
166
+ soft_datatier: Force using the latest campaign found in the chained request
167
+ if there is none related to the requested data tier.
168
+
169
+ Returns:
170
+ Campaign related to the data tier, None if not found
171
+ """
172
+ chained_campaign: Union[dict, None] = self._mcm.get(
173
+ object_type="chained_campaigns", object_id=chained_campaign_prepid
174
+ )
175
+ if not chained_campaign:
176
+ raise ValueError(f"Chained campaign not found: {chained_campaign_prepid}")
177
+
178
+ campaigns: list[list[str]] = chained_campaign.get("campaigns", [])
179
+ for campaign_range in campaigns:
180
+ for c in campaign_range:
181
+ c_str = c or ""
182
+ if c_str.startswith("Run") and datatier.lower() in c_str.lower():
183
+ return c_str
184
+
185
+ self.logger.debug(
186
+ "There's no campaign related to the data tier %s for the chained campaign %s: %s",
187
+ datatier,
188
+ chained_campaign_prepid,
189
+ campaigns,
190
+ )
191
+ if soft_datatier:
192
+ latest_campaign_range = campaigns[-1]
193
+ for c in latest_campaign_range:
194
+ c_str = c or ""
195
+ if c_str.startswith("Run"):
196
+ self.logger.debug("Using the latest available campaign: %s", c_str)
197
+ return c_str
198
+
199
+ return None
200
+
201
+ def _reserve_chain_request(
202
+ self, chain_request_prepid: str, datatier: str, soft_datatier: bool
203
+ ) -> None:
204
+ """
205
+ Reserve a chained request up to a desired data tier. This creates the
206
+ remaining requests in the chained request based on a chained campaign
207
+ definition (template).
208
+
209
+ Args:
210
+ chain_request_prepid: Chained request identifier.
211
+ datatier: Limits the requests created up to this data tier.
212
+ soft_datatier: Force using the latest campaign found in the chained request
213
+ if there is none related to the requested data tier.
214
+ """
215
+ chained_request: Union[dict, None] = self._mcm.get(
216
+ object_type="chained_requests", object_id=chain_request_prepid
217
+ )
218
+ if not chained_request:
219
+ raise ValueError(f"Chain request not found: {chain_request_prepid}")
220
+
221
+ # Pick the `member_of_campaign` attribute and look
222
+ # for the chained campaign. This has the info for the correct
223
+ # campaign modifications (campaigns and flows, customizations)
224
+ member_of_campaign = chained_request.get("member_of_campaign")
225
+ if not member_of_campaign:
226
+ raise ValueError(
227
+ f"Member of campaign is not set for {chain_request_prepid}"
228
+ )
229
+
230
+ target_campaign = self._pick_target_campaign(
231
+ chained_campaign_prepid=member_of_campaign,
232
+ datatier=datatier,
233
+ soft_datatier=soft_datatier,
234
+ )
235
+ if not target_campaign:
236
+ msg = (
237
+ f"There's no campaign related to the desired data tier ({datatier}) in "
238
+ f"the linked chained campaign ({member_of_campaign}) that could "
239
+ "be used to reserve the chained request"
240
+ )
241
+ raise ValueError(msg)
242
+
243
+ # Reserve the chain
244
+ reserve_endpoint = f"restapi/chained_requests/flow/{chain_request_prepid}/reserve/{target_campaign}"
245
+ reserve_result = self._mcm._get(url=reserve_endpoint)
246
+ if not reserve_result or not reserve_result.get("results", False):
247
+ raise RuntimeError(
248
+ f"Unable to reserve the chained request ({chain_request_prepid}). Details: {reserve_result}"
249
+ )
250
+
251
+ def _approve_request_until(
252
+ self, request_prepid: str, approval: str, status: str
253
+ ) -> None:
254
+ """
255
+ Approves one request until a desired state.
256
+
257
+ Args:
258
+ request_prepid: Request identifier.
259
+ approval: Desired approval for the request.
260
+ status: Desired status for the request.
261
+ """
262
+ request: Union[dict, None] = self._mcm.get(
263
+ object_type="requests", object_id=request_prepid
264
+ )
265
+ if not request:
266
+ raise ValueError(f"Request not found: {request_prepid}")
267
+
268
+ is_root: bool = self._mcm.is_root_request(request=request)
269
+ attempts = 10
270
+ for _ in range(attempts):
271
+ request = self._mcm.get(object_type="requests", object_id=request_prepid)
272
+ req_approval = request.get("approval")
273
+ req_status = request.get("status")
274
+
275
+ if approval == req_approval and status == req_status:
276
+ return
277
+
278
+ approve_result = self._mcm.approve(
279
+ object_type="requests", object_id=request_prepid
280
+ )
281
+ if not approve_result or not approve_result.get("results"):
282
+ if approve_result and is_root:
283
+ message = approve_result.get("message", "")
284
+ if "Illegal Approval Step: 5" in message:
285
+ return
286
+
287
+ msg = (
288
+ "Unable to approve the request to the next status - "
289
+ f"Request PrepID: {request_prepid} - "
290
+ f"Current approval/status: {req_approval}/{req_status} - "
291
+ f"Details: {pformat(approve_result)}"
292
+ )
293
+ self.logger.error(msg)
294
+ raise RuntimeError(msg)
295
+
296
+ raise RuntimeError("Unable to get the desired approval status")
297
+
298
+ def _perform_chain_request_injection(
299
+ self, chain_request_prepid: str, root_request_processing: RootRequestReset
300
+ ) -> None:
301
+ """
302
+ Injects a chained request based on how its root request was reset.
303
+ This MUST BE executed only after rewinding a chained request to root and reserving it again.
304
+
305
+ Args:
306
+ chain_request_prepid: Chained request identifier.
307
+ root_request_processing: Describes how the root request was reset.
308
+ """
309
+ chained_request: Union[dict, None] = self._mcm.get(
310
+ object_type="chained_requests", object_id=chain_request_prepid
311
+ )
312
+ if not chained_request:
313
+ raise ValueError(f"Chain request not found: {chain_request_prepid}")
314
+
315
+ if root_request_processing == RootRequestReset.KEEPS_OUTPUT:
316
+ # Just flow the chained request
317
+ flow_result = self._mcm.flow(chained_request_prepid=chain_request_prepid)
318
+ if not flow_result:
319
+ msg = f"Unable to flow the following chained request: {chain_request_prepid}"
320
+ self.logger.error(msg)
321
+ raise RuntimeError(msg)
322
+
323
+ elif root_request_processing in (
324
+ RootRequestReset.SOFT_RESET,
325
+ RootRequestReset.FULL_RESET,
326
+ ):
327
+ # Retrieve the newly created requests and
328
+ # make sure their approval/state is approve/approved
329
+ requests_in_chain = chained_request.get("chain", [])
330
+ to_approve = requests_in_chain[1:]
331
+ root_request_in_chain = requests_in_chain[0]
332
+ self.logger.info("Making sure non-root requests are approve/approved")
333
+ for req_prepid in to_approve:
334
+ self._approve_request_until(
335
+ request_prepid=req_prepid,
336
+ approval="approve",
337
+ status="approved",
338
+ )
339
+
340
+ # Operate the root request in the chain and
341
+ # make sure its state is submit/submitted.
342
+ # INFO: This takes time ~3 to 5 min.
343
+ self.logger.info("Injecting chained request")
344
+ self._approve_request_until(
345
+ request_prepid=root_request_in_chain,
346
+ approval="submit",
347
+ status="submitted",
348
+ )
349
+
350
+ else:
351
+ raise NotImplementedError(
352
+ "There's no way to inject a chained request based on: %s",
353
+ root_request_processing,
354
+ )
355
+
356
+ def resubmit_chain_request(
357
+ self,
358
+ root_request_prepid: str,
359
+ datatier: str = "nanoaod",
360
+ soft_datatier: bool = False,
361
+ tracking_tag: Union[str, None] = None,
362
+ ) -> None:
363
+ """
364
+ Resubmit all the chained requests that reference a particular root request.
365
+ To achieve this, this procedure rewinds all the chained requests to the root step,
366
+ deleting and invalidating all the found requests except for the root one.
367
+ Then, it recreates them by reserving the chain to the desired data tier and finally reinjects them.
368
+
369
+ If you require only to resubmit a very specific part (some particular step like MiniAOD or NanoAOD)
370
+ of a chained request, avoid using this. Rewind the chain to the previous step manually and resubmit it.
371
+
372
+ Args:
373
+ root_request_prepid: Root request identifier.
374
+ datatier: Limit data tier for reserving all the chained requests.
375
+ soft_datatier: Force using the latest campaign found in the chained request
376
+ if there is none related to the requested data tier.
377
+ tracking_tag: Includes a tracking tag for the root request
378
+ to monitor its progress after patching this.
379
+ """
380
+ if not self._mcm.is_root_request(request=root_request_prepid):
381
+ raise ValueError(
382
+ f"Provided request ({root_request_prepid}) is not a root request"
383
+ )
384
+
385
+ chained_requests: list[dict] = self._mcm.get(
386
+ object_type="chained_requests", query=f"contains={root_request_prepid}"
387
+ )
388
+ chained_requests_prepid: list[str] = [
389
+ ch["prepid"] for ch in chained_requests if ch.get("prepid")
390
+ ]
391
+ self.logger.info(
392
+ "Resubmitting the following chained request related to %s: %s",
393
+ root_request_prepid,
394
+ pformat(chained_requests_prepid),
395
+ )
396
+
397
+ # Operate the chained requests so that only the root request remains
398
+ self.logger.info(
399
+ "Rewinding chained requests to root and deleting intermediate requests"
400
+ )
401
+ self._invalidator.invalidate_delete_cascade_requests(
402
+ requests_prepid=[root_request_prepid]
403
+ )
404
+
405
+ # Reset the root request and include the tag
406
+ root_processing = self._root_request_to_approve(root_prepid=root_request_prepid)
407
+ if tracking_tag:
408
+ self.logger.info(
409
+ "Including tracking tag (%s) for the root request: %s",
410
+ tracking_tag,
411
+ root_request_prepid,
412
+ )
413
+ self._include_tag(request_prepid=root_request_prepid, tag=tracking_tag)
414
+
415
+ # Reserve the chained requests
416
+ for ch_r in chained_requests_prepid:
417
+ self.logger.info(
418
+ "Reserving chained requests (%s) up to data tier: %s", ch_r, datatier
419
+ )
420
+ self._reserve_chain_request(
421
+ chain_request_prepid=ch_r,
422
+ datatier=datatier,
423
+ soft_datatier=soft_datatier,
424
+ )
425
+
426
+ # Reinject the chained requests
427
+ for ch_r in chained_requests_prepid:
428
+ self.logger.info("Reinjecting chained requests: %s", ch_r)
429
+ self._perform_chain_request_injection(
430
+ chain_request_prepid=ch_r, root_request_processing=root_processing
431
+ )
@@ -0,0 +1,78 @@
1
+ """
2
+ REST client for the ReReco application
3
+ """
4
+
5
+ from typing import Union
6
+
7
+ from rest.applications.base import BaseClient
8
+
9
+
10
+ class ReReco(BaseClient):
11
+ """
12
+ Initializes an HTTP client for querying ReReco.
13
+ """
14
+
15
+ def __init__(
16
+ self,
17
+ id: str = BaseClient.SSO,
18
+ debug: bool = False,
19
+ cookie: Union[str, None] = None,
20
+ dev: bool = True,
21
+ client_id: str = "",
22
+ client_secret: str = "",
23
+ ):
24
+
25
+ # Set the HTTP session
26
+ super().__init__(
27
+ app="rereco",
28
+ id=id,
29
+ debug=debug,
30
+ cookie=cookie,
31
+ dev=dev,
32
+ client_id=client_id,
33
+ client_secret=client_secret,
34
+ )
35
+
36
+ def get(self, object_type, object_id=None, query=None, method="get", page=-1):
37
+ object_type = object_type.strip()
38
+ if object_id:
39
+ object_id = object_id.strip()
40
+ url = "api/%s/%s/%s" % (object_type, method, object_id)
41
+ result = self._get(url).get("response")
42
+ if not result:
43
+ return None
44
+ return result
45
+ elif query:
46
+ if page != -1:
47
+ url = "api/search/?db_name=%s&page=%d&%s" % (
48
+ object_type,
49
+ page,
50
+ query,
51
+ )
52
+ results = self._get(url).get("response", {}).get("results", [])
53
+ return results
54
+ else:
55
+ page_results = [{}]
56
+ results = []
57
+ page = 0
58
+ while page_results:
59
+ page_results = self.get(
60
+ object_type=object_type, query=query, method=method, page=page
61
+ )
62
+ results += page_results
63
+ page += 1
64
+
65
+ return results
66
+ else:
67
+ # nothing to do
68
+ return None
69
+
70
+ def put(self, object_type, object_data, method="create"):
71
+ url = "api/%s/%s" % (object_type, method)
72
+ res = self._put(url, object_data)
73
+ return res
74
+
75
+ def post(self, object_type, object_data, method="update"):
76
+ url = "api/%s/%s" % (object_type, method)
77
+ res = self._post(url, object_data)
78
+ return res
@@ -0,0 +1,95 @@
1
+ """
2
+ REST client for the Stats2 application.
3
+ """
4
+
5
+ from typing import Union
6
+
7
+ from rest.applications.base import BaseClient
8
+
9
+
10
+ class Stats2(BaseClient):
11
+ """
12
+ Initializes an HTTP client for querying Stats2.
13
+ """
14
+
15
+ def __init__(
16
+ self,
17
+ id: str = BaseClient.SSO,
18
+ debug: bool = False,
19
+ cookie: Union[str, None] = None,
20
+ dev: bool = True,
21
+ client_id: str = "",
22
+ client_secret: str = "",
23
+ ):
24
+ # Set the HTTP session
25
+ super().__init__(
26
+ app="stats",
27
+ id=id,
28
+ debug=debug,
29
+ cookie=cookie,
30
+ dev=dev,
31
+ client_id=client_id,
32
+ client_secret=client_secret,
33
+ )
34
+
35
+ def get_workflow(self, workflow_name: str) -> dict:
36
+ """
37
+ Retrieves the Stats2 document related to the given workflow.
38
+
39
+ Args:
40
+ workflow_name: ReqMgr2 Request ID (a.k.a workflow in PdmV applications).
41
+ """
42
+ url = f"api/get_json/{workflow_name}"
43
+ return self._get(url=url)
44
+
45
+ def update_workflow(self, workflow_name: str) -> dict:
46
+ """
47
+ Updates the content of a document, from dbs and reqmgr
48
+
49
+ Args:
50
+ workflow_name: ReqMgr2 Request ID (a.k.a workflow in PdmV applications).
51
+ """
52
+ url = f"api/update?workflow_name={workflow_name}"
53
+ return self._get(url=url)
54
+
55
+ def get_prepid(self, prepid: str) -> list[dict]:
56
+ """
57
+ Retrieves a list of Stats2 documents related to the given prepid.
58
+
59
+ Args:
60
+ prepid: The main identifier for requests in PdmV applications.
61
+ """
62
+ url = f"api/fetch?prepid={prepid}"
63
+ return self._get(url=url)
64
+
65
+ def get_input_dataset(self, input_dataset: str) -> list[dict]:
66
+ """
67
+ Retrieves a list of Stats2 documents related to the given input dataset.
68
+
69
+ Args:
70
+ input_dataset: Input dataset name. It follows DBS and DAS
71
+ schema.
72
+ """
73
+ url = f"api/fetch?input_dataset={input_dataset}"
74
+ return self._get(url=url)
75
+
76
+ def get_output_dataset(self, output_dataset: str) -> list[dict]:
77
+ """
78
+ Retrieves the Stats2 document related to the given output dataset.
79
+
80
+ Args:
81
+ output_dataset: Output dataset name. It follows DBS and DAS
82
+ schema.
83
+ """
84
+ url = f"api/fetch?output_dataset={output_dataset}"
85
+ return self._get(url=url)
86
+
87
+ def get_request(self, prepid: str):
88
+ """
89
+ Retrieves the Stats2 document related to the request.
90
+
91
+ Args:
92
+ prepid: Request's prepid. This is only useful for McM requests.
93
+ """
94
+ url = f"api/fetch?request={prepid}"
95
+ return self._get(url=url)
File without changes
File without changes