panda-server-source 0.8.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.
Files changed (375) hide show
  1. panda_server_source-0.8.0.dist-info/METADATA +55 -0
  2. panda_server_source-0.8.0.dist-info/RECORD +375 -0
  3. panda_server_source-0.8.0.dist-info/WHEEL +4 -0
  4. panda_server_source-0.8.0.dist-info/licenses/LICENSE.txt +202 -0
  5. pandajedi/__init__.py +0 -0
  6. pandajedi/jedibrokerage/AtlasAnalJobBroker.py +2343 -0
  7. pandajedi/jedibrokerage/AtlasBrokerUtils.py +1292 -0
  8. pandajedi/jedibrokerage/AtlasProdJobBroker.py +2064 -0
  9. pandajedi/jedibrokerage/AtlasProdTaskBroker.py +629 -0
  10. pandajedi/jedibrokerage/GenJobBroker.py +381 -0
  11. pandajedi/jedibrokerage/JobBrokerBase.py +133 -0
  12. pandajedi/jedibrokerage/TaskBrokerBase.py +15 -0
  13. pandajedi/jedibrokerage/__init__.py +0 -0
  14. pandajedi/jediconfig/__init__.py +0 -0
  15. pandajedi/jediconfig/jedi_config.py +40 -0
  16. pandajedi/jedicore/FactoryBase.py +182 -0
  17. pandajedi/jedicore/Interaction.py +433 -0
  18. pandajedi/jedicore/JediDBProxy.py +56 -0
  19. pandajedi/jedicore/JediDBProxyPool.py +37 -0
  20. pandajedi/jedicore/JediDBSchemaInfo.py +17 -0
  21. pandajedi/jedicore/JediException.py +13 -0
  22. pandajedi/jedicore/JediTaskBuffer.py +984 -0
  23. pandajedi/jedicore/JediTaskBufferInterface.py +50 -0
  24. pandajedi/jedicore/MsgWrapper.py +124 -0
  25. pandajedi/jedicore/ProcessUtils.py +34 -0
  26. pandajedi/jedicore/SiteCandidate.py +90 -0
  27. pandajedi/jedicore/ThreadUtils.py +205 -0
  28. pandajedi/jedicore/__init__.py +0 -0
  29. pandajedi/jediddm/AtlasDDMClient.py +1708 -0
  30. pandajedi/jediddm/DDMClientBase.py +16 -0
  31. pandajedi/jediddm/DDMInterface.py +83 -0
  32. pandajedi/jediddm/GenDDMClient.py +17 -0
  33. pandajedi/jediddm/__init__.py +0 -0
  34. pandajedi/jedidog/AtlasAnalWatchDog.py +648 -0
  35. pandajedi/jedidog/AtlasDataCarouselWatchDog.py +189 -0
  36. pandajedi/jedidog/AtlasDataLocalityUpdaterWatchDog.py +215 -0
  37. pandajedi/jedidog/AtlasProdWatchDog.py +443 -0
  38. pandajedi/jedidog/AtlasQueueFillerWatchDog.py +800 -0
  39. pandajedi/jedidog/AtlasTaskWithholderWatchDog.py +245 -0
  40. pandajedi/jedidog/AtlasWorkflowManagerWatchDog.py +63 -0
  41. pandajedi/jedidog/GenWatchDog.py +21 -0
  42. pandajedi/jedidog/JumboWatchDog.py +147 -0
  43. pandajedi/jedidog/TypicalWatchDogBase.py +122 -0
  44. pandajedi/jedidog/WatchDogBase.py +53 -0
  45. pandajedi/jedidog/__init__.py +0 -0
  46. pandajedi/jediexec/reniceJEDI.py +25 -0
  47. pandajedi/jedigen/AtlasTaskGenerator.py +110 -0
  48. pandajedi/jedigen/TaskGeneratorBase.py +15 -0
  49. pandajedi/jedigen/__init__.py +0 -0
  50. pandajedi/jedimsgprocessor/__init__.py +0 -0
  51. pandajedi/jedimsgprocessor/ai_parameter_adjuster_msg_processor +71 -0
  52. pandajedi/jedimsgprocessor/atlas_idds_msg_processor.py +66 -0
  53. pandajedi/jedimsgprocessor/base_msg_processor.py +33 -0
  54. pandajedi/jedimsgprocessor/forwarding_msg_processor.py +29 -0
  55. pandajedi/jedimsgprocessor/hpo_msg_processor.py +93 -0
  56. pandajedi/jedimsgprocessor/jedi_contents_feeder_msg_processor.py +63 -0
  57. pandajedi/jedimsgprocessor/jedi_job_generator_msg_processor.py +185 -0
  58. pandajedi/jedimsgprocessor/jedi_post_processor_msg_processor.py +82 -0
  59. pandajedi/jedimsgprocessor/kafka_msg_processor.py +44 -0
  60. pandajedi/jedimsgprocessor/panda_callback_msg_processor.py +182 -0
  61. pandajedi/jedimsgprocessor/panda_to_jedi_msg_processor.py +74 -0
  62. pandajedi/jedimsgprocessor/processing_msg_processor.py +134 -0
  63. pandajedi/jedimsgprocessor/status_report_msg_processor.py +129 -0
  64. pandajedi/jedimsgprocessor/tape_carousel_msg_processor.py +118 -0
  65. pandajedi/jedimsgprocessor/workflow_manager_msg_processor.py +89 -0
  66. pandajedi/jediorder/ContentsFeeder.py +785 -0
  67. pandajedi/jediorder/JediDaemon.py +27 -0
  68. pandajedi/jediorder/JediKnight.py +55 -0
  69. pandajedi/jediorder/JediMaster.py +238 -0
  70. pandajedi/jediorder/JediMsgProcessor.py +32 -0
  71. pandajedi/jediorder/JobBroker.py +33 -0
  72. pandajedi/jediorder/JobGenerator.py +2763 -0
  73. pandajedi/jediorder/JobSplitter.py +308 -0
  74. pandajedi/jediorder/JobThrottler.py +28 -0
  75. pandajedi/jediorder/PostProcessor.py +166 -0
  76. pandajedi/jediorder/TaskBroker.py +268 -0
  77. pandajedi/jediorder/TaskCommando.py +334 -0
  78. pandajedi/jediorder/TaskGenerator.py +17 -0
  79. pandajedi/jediorder/TaskRefiner.py +611 -0
  80. pandajedi/jediorder/TaskSetupper.py +17 -0
  81. pandajedi/jediorder/WatchDog.py +79 -0
  82. pandajedi/jediorder/__init__.py +0 -0
  83. pandajedi/jedipprocess/AtlasAnalPostProcessor.py +404 -0
  84. pandajedi/jedipprocess/AtlasPostProcessorUtils.py +50 -0
  85. pandajedi/jedipprocess/AtlasProdPostProcessor.py +178 -0
  86. pandajedi/jedipprocess/GenPostProcessor.py +35 -0
  87. pandajedi/jedipprocess/MailTemplates.py +185 -0
  88. pandajedi/jedipprocess/PostProcessorBase.py +269 -0
  89. pandajedi/jedipprocess/__init__.py +0 -0
  90. pandajedi/jedirefine/AtlasAnalTaskRefiner.py +259 -0
  91. pandajedi/jedirefine/AtlasProdTaskRefiner.py +162 -0
  92. pandajedi/jedirefine/GenTaskRefiner.py +61 -0
  93. pandajedi/jedirefine/RefinerUtils.py +154 -0
  94. pandajedi/jedirefine/TaskRefinerBase.py +996 -0
  95. pandajedi/jedirefine/__init__.py +0 -0
  96. pandajedi/jedisetup/AtlasTaskSetupper.py +309 -0
  97. pandajedi/jedisetup/GenTaskSetupper.py +16 -0
  98. pandajedi/jedisetup/SimpleTaskSetupper.py +152 -0
  99. pandajedi/jedisetup/TaskSetupperBase.py +15 -0
  100. pandajedi/jedisetup/__init__.py +0 -0
  101. pandajedi/jeditest/SchemaChecker.py +41 -0
  102. pandajedi/jeditest/addContWoSoTask.py +54 -0
  103. pandajedi/jeditest/addDataCarouselRequest.py +47 -0
  104. pandajedi/jeditest/addEventServiceTask.py +101 -0
  105. pandajedi/jeditest/addHpoTask.py +81 -0
  106. pandajedi/jeditest/addIDDSStageInRequest.py +56 -0
  107. pandajedi/jeditest/addNonAtlasTask.py +71 -0
  108. pandajedi/jeditest/addNonAtlasTaskEnc.py +64 -0
  109. pandajedi/jeditest/addTestTask.py +124 -0
  110. pandajedi/jeditest/addTestTaskParamToDEFT.py +70 -0
  111. pandajedi/jeditest/addTestTaskWithMerger.py +97 -0
  112. pandajedi/jeditest/brokerTest.py +78 -0
  113. pandajedi/jeditest/cancelDataCarouselRequest.py +35 -0
  114. pandajedi/jeditest/contentsFeederTest.py +28 -0
  115. pandajedi/jeditest/contentsFeederTestWithTask.py +18 -0
  116. pandajedi/jeditest/ddmTest.py +4 -0
  117. pandajedi/jeditest/jobGenForOneTask.py +38 -0
  118. pandajedi/jeditest/jobGeneratorTest.py +43 -0
  119. pandajedi/jeditest/jobSplitterTest.py +104 -0
  120. pandajedi/jeditest/jobThrottlerTest.py +22 -0
  121. pandajedi/jeditest/msgProcessorTest.py +9 -0
  122. pandajedi/jeditest/postProcessorTest.py +18 -0
  123. pandajedi/jeditest/resubmitDataCarouselRequest.py +35 -0
  124. pandajedi/jeditest/resubmitTaskParam.py +20 -0
  125. pandajedi/jeditest/taskBrokerTest.py +22 -0
  126. pandajedi/jeditest/taskBrokerTestWithTask.py +38 -0
  127. pandajedi/jeditest/taskBufferTest.py +5 -0
  128. pandajedi/jeditest/taskCommandoTest.py +28 -0
  129. pandajedi/jeditest/taskRefinerTest.py +30 -0
  130. pandajedi/jeditest/testOneTaskToRefine.py +49 -0
  131. pandajedi/jeditest/testOneTaskToTaskBroker.py +42 -0
  132. pandajedi/jeditest/testPandaDDMMap.py +62 -0
  133. pandajedi/jeditest/updateDataCarouselRequestParams.py +50 -0
  134. pandajedi/jeditest/watchDogTest.py +25 -0
  135. pandajedi/jeditest/workQueueTest.py +22 -0
  136. pandajedi/jedithrottle/AtlasAnalJobThrottler.py +19 -0
  137. pandajedi/jedithrottle/AtlasProdJobThrottler.py +18 -0
  138. pandajedi/jedithrottle/GenJobThrottler.py +26 -0
  139. pandajedi/jedithrottle/JobThrottlerBase.py +496 -0
  140. pandajedi/jedithrottle/__init__.py +0 -0
  141. pandaserver/__init__.py +0 -0
  142. pandaserver/api/__init__.py +0 -0
  143. pandaserver/api/v1/__init__.py +0 -0
  144. pandaserver/api/v1/common.py +339 -0
  145. pandaserver/api/v1/credential_management_api.py +372 -0
  146. pandaserver/api/v1/data_carousel_api.py +264 -0
  147. pandaserver/api/v1/event_api.py +269 -0
  148. pandaserver/api/v1/file_server_api.py +942 -0
  149. pandaserver/api/v1/harvester_api.py +445 -0
  150. pandaserver/api/v1/http_client.py +166 -0
  151. pandaserver/api/v1/idds_api.py +180 -0
  152. pandaserver/api/v1/job_api.py +463 -0
  153. pandaserver/api/v1/metaconfig_api.py +122 -0
  154. pandaserver/api/v1/pilot_api.py +895 -0
  155. pandaserver/api/v1/statistics_api.py +207 -0
  156. pandaserver/api/v1/system_api.py +186 -0
  157. pandaserver/api/v1/task_api.py +1520 -0
  158. pandaserver/api/v1/tests/__init__.py +0 -0
  159. pandaserver/api/v1/tests/credential_management_api_tests.py +116 -0
  160. pandaserver/api/v1/tests/data_carousel_api_tests.py +82 -0
  161. pandaserver/api/v1/tests/harvester_api_tests.py +188 -0
  162. pandaserver/api/v1/tests/idds_api_tests.py +45 -0
  163. pandaserver/api/v1/tests/job_api_tests.py +112 -0
  164. pandaserver/api/v1/tests/pilot_api_tests.py +154 -0
  165. pandaserver/api/v1/tests/statistics_api_tests.py +104 -0
  166. pandaserver/api/v1/tests/system_api_tests.py +58 -0
  167. pandaserver/api/v1/tests/task_api_tests.py +590 -0
  168. pandaserver/api/v1/version.py +2 -0
  169. pandaserver/api/v1/workflow_api.py +136 -0
  170. pandaserver/brokerage/SiteMapper.py +406 -0
  171. pandaserver/brokerage/__init__.py +0 -0
  172. pandaserver/brokerage/broker.py +272 -0
  173. pandaserver/config/__init__.py +0 -0
  174. pandaserver/config/config_utils.py +15 -0
  175. pandaserver/config/daemon_config.py +29 -0
  176. pandaserver/config/mcp_config.py +38 -0
  177. pandaserver/config/panda_config.py +233 -0
  178. pandaserver/configurator/Carbon.py +65 -0
  179. pandaserver/configurator/Configurator.py +908 -0
  180. pandaserver/configurator/__init__.py +0 -0
  181. pandaserver/configurator/aux.py +73 -0
  182. pandaserver/daemons/__init__.py +0 -0
  183. pandaserver/daemons/master.py +110 -0
  184. pandaserver/daemons/master_systemd.py +79 -0
  185. pandaserver/daemons/scripts/__init__.py +0 -0
  186. pandaserver/daemons/scripts/add_main.py +264 -0
  187. pandaserver/daemons/scripts/add_sub.py +221 -0
  188. pandaserver/daemons/scripts/cache_pilots.py +47 -0
  189. pandaserver/daemons/scripts/cache_schedconfig.py +286 -0
  190. pandaserver/daemons/scripts/carbon.py +50 -0
  191. pandaserver/daemons/scripts/configurator.py +94 -0
  192. pandaserver/daemons/scripts/copyArchive.py +1387 -0
  193. pandaserver/daemons/scripts/datasetManager.py +1154 -0
  194. pandaserver/daemons/scripts/dummy_test.py +25 -0
  195. pandaserver/daemons/scripts/evpPD2P.py +144 -0
  196. pandaserver/daemons/scripts/hs_scrapers.py +264 -0
  197. pandaserver/daemons/scripts/metric_collector.py +927 -0
  198. pandaserver/daemons/scripts/panda_activeusers_query.py +80 -0
  199. pandaserver/daemons/scripts/pilotStreaming.py +109 -0
  200. pandaserver/daemons/scripts/process_workflow_files_daemon.py +166 -0
  201. pandaserver/daemons/scripts/recover_lost_files_daemon.py +177 -0
  202. pandaserver/daemons/scripts/task_evaluator.py +405 -0
  203. pandaserver/daemons/scripts/tmpwatch.py +70 -0
  204. pandaserver/daemons/scripts/worker_synchronization.py +97 -0
  205. pandaserver/daemons/utils.py +753 -0
  206. pandaserver/dataservice/DataServiceUtils.py +323 -0
  207. pandaserver/dataservice/ErrorCode.py +25 -0
  208. pandaserver/dataservice/RecoverLostFilesCore.py +322 -0
  209. pandaserver/dataservice/__init__.py +0 -0
  210. pandaserver/dataservice/activator.py +79 -0
  211. pandaserver/dataservice/adder_atlas_plugin.py +1106 -0
  212. pandaserver/dataservice/adder_dummy_plugin.py +31 -0
  213. pandaserver/dataservice/adder_gen.py +954 -0
  214. pandaserver/dataservice/adder_plugin_base.py +27 -0
  215. pandaserver/dataservice/adder_result.py +75 -0
  216. pandaserver/dataservice/adder_simple_plugin.py +160 -0
  217. pandaserver/dataservice/closer.py +247 -0
  218. pandaserver/dataservice/closer_atlas_plugin.py +90 -0
  219. pandaserver/dataservice/ddm.py +1381 -0
  220. pandaserver/dataservice/ddm_handler.py +95 -0
  221. pandaserver/dataservice/dyn_data_distributer.py +824 -0
  222. pandaserver/dataservice/event_lookup_client_ei.py +77 -0
  223. pandaserver/dataservice/event_picker.py +268 -0
  224. pandaserver/dataservice/finisher.py +194 -0
  225. pandaserver/dataservice/setupper.py +225 -0
  226. pandaserver/dataservice/setupper_atlas_plugin.py +1851 -0
  227. pandaserver/dataservice/setupper_dummy_plugin.py +47 -0
  228. pandaserver/dataservice/setupper_plugin_base.py +67 -0
  229. pandaserver/jobdispatcher/DispatcherUtils.py +35 -0
  230. pandaserver/jobdispatcher/ErrorCode.py +19 -0
  231. pandaserver/jobdispatcher/JobDispatcher.py +1625 -0
  232. pandaserver/jobdispatcher/Protocol.py +443 -0
  233. pandaserver/jobdispatcher/Watcher.py +186 -0
  234. pandaserver/jobdispatcher/__init__.py +0 -0
  235. pandaserver/pandamcp/__init__.py +0 -0
  236. pandaserver/pandamcp/mcp_main.py +56 -0
  237. pandaserver/pandamcp/mcp_test_client.py +95 -0
  238. pandaserver/pandamcp/mcp_utils.py +74 -0
  239. pandaserver/proxycache/__init__.py +0 -0
  240. pandaserver/proxycache/panda_proxy_cache.py +240 -0
  241. pandaserver/proxycache/token_cache.py +141 -0
  242. pandaserver/server/.gacl +6 -0
  243. pandaserver/server/panda.py +539 -0
  244. pandaserver/srvcore/CoreUtils.py +572 -0
  245. pandaserver/srvcore/MailUtils.py +106 -0
  246. pandaserver/srvcore/__init__.py +0 -0
  247. pandaserver/srvcore/allowed_methods.py +100 -0
  248. pandaserver/srvcore/oidc_utils.py +152 -0
  249. pandaserver/srvcore/panda_request.py +196 -0
  250. pandaserver/srvcore/srv_msg_utils.py +42 -0
  251. pandaserver/taskbuffer/ConBridge.py +513 -0
  252. pandaserver/taskbuffer/DBProxyPool.py +95 -0
  253. pandaserver/taskbuffer/DataCarousel.py +3053 -0
  254. pandaserver/taskbuffer/DatasetSpec.py +137 -0
  255. pandaserver/taskbuffer/DdmSpec.py +201 -0
  256. pandaserver/taskbuffer/ErrorCode.py +87 -0
  257. pandaserver/taskbuffer/EventServiceUtils.py +396 -0
  258. pandaserver/taskbuffer/FileSpec.py +290 -0
  259. pandaserver/taskbuffer/GlobalShares.py +244 -0
  260. pandaserver/taskbuffer/HarvesterMetricsSpec.py +92 -0
  261. pandaserver/taskbuffer/Initializer.py +82 -0
  262. pandaserver/taskbuffer/InputChunk.py +1080 -0
  263. pandaserver/taskbuffer/JediCacheSpec.py +57 -0
  264. pandaserver/taskbuffer/JediDatasetSpec.py +588 -0
  265. pandaserver/taskbuffer/JediFileSpec.py +242 -0
  266. pandaserver/taskbuffer/JediTaskSpec.py +1687 -0
  267. pandaserver/taskbuffer/JobSpec.py +1006 -0
  268. pandaserver/taskbuffer/JobUtils.py +232 -0
  269. pandaserver/taskbuffer/NucleusSpec.py +116 -0
  270. pandaserver/taskbuffer/OraDBProxy.py +174 -0
  271. pandaserver/taskbuffer/PanDAMsgProcessor.py +8 -0
  272. pandaserver/taskbuffer/PandaDBSchemaInfo.py +18 -0
  273. pandaserver/taskbuffer/ParseJobXML.py +350 -0
  274. pandaserver/taskbuffer/PickleFileSpec.py +18 -0
  275. pandaserver/taskbuffer/PickleJobSpec.py +25 -0
  276. pandaserver/taskbuffer/PrioUtil.py +40 -0
  277. pandaserver/taskbuffer/ProcessGroups.py +111 -0
  278. pandaserver/taskbuffer/ResourceSpec.py +175 -0
  279. pandaserver/taskbuffer/SQLDumper.py +37 -0
  280. pandaserver/taskbuffer/SQLManager.py +60 -0
  281. pandaserver/taskbuffer/SiteSpec.py +304 -0
  282. pandaserver/taskbuffer/SupErrors.py +10 -0
  283. pandaserver/taskbuffer/TaskBuffer.py +2882 -0
  284. pandaserver/taskbuffer/TaskBufferInterface.py +163 -0
  285. pandaserver/taskbuffer/Utils.py +848 -0
  286. pandaserver/taskbuffer/WorkQueue.py +227 -0
  287. pandaserver/taskbuffer/WorkQueueMapper.py +203 -0
  288. pandaserver/taskbuffer/WorkerSpec.py +143 -0
  289. pandaserver/taskbuffer/WrappedCursor.py +487 -0
  290. pandaserver/taskbuffer/WrappedPickle.py +88 -0
  291. pandaserver/taskbuffer/WrappedPostgresConn.py +24 -0
  292. pandaserver/taskbuffer/__init__.py +0 -0
  293. pandaserver/taskbuffer/db_proxy_mods/OraDBProxy_structure.png +0 -0
  294. pandaserver/taskbuffer/db_proxy_mods/__init__.py +7 -0
  295. pandaserver/taskbuffer/db_proxy_mods/base_module.py +841 -0
  296. pandaserver/taskbuffer/db_proxy_mods/data_carousel_module.py +772 -0
  297. pandaserver/taskbuffer/db_proxy_mods/entity_module.py +3804 -0
  298. pandaserver/taskbuffer/db_proxy_mods/job_complex_module.py +5661 -0
  299. pandaserver/taskbuffer/db_proxy_mods/job_standalone_module.py +2640 -0
  300. pandaserver/taskbuffer/db_proxy_mods/metrics_module.py +874 -0
  301. pandaserver/taskbuffer/db_proxy_mods/misc_standalone_module.py +4272 -0
  302. pandaserver/taskbuffer/db_proxy_mods/task_complex_module.py +6172 -0
  303. pandaserver/taskbuffer/db_proxy_mods/task_event_module.py +4953 -0
  304. pandaserver/taskbuffer/db_proxy_mods/task_standalone_module.py +4999 -0
  305. pandaserver/taskbuffer/db_proxy_mods/task_utils_module.py +2063 -0
  306. pandaserver/taskbuffer/db_proxy_mods/worker_module.py +1944 -0
  307. pandaserver/taskbuffer/db_proxy_mods/workflow_module.py +832 -0
  308. pandaserver/taskbuffer/retryModule.py +552 -0
  309. pandaserver/taskbuffer/task_split_rules.py +308 -0
  310. pandaserver/taskbuffer/workflow_processor.py +249 -0
  311. pandaserver/taskbuffer/wrapped_oracle_conn.py +18 -0
  312. pandaserver/test/SchemaChecker.py +40 -0
  313. pandaserver/test/__init__.py +0 -0
  314. pandaserver/test/banUser.py +43 -0
  315. pandaserver/test/boostPrio.py +19 -0
  316. pandaserver/test/boostUser.py +35 -0
  317. pandaserver/test/callbackDDM.py +11 -0
  318. pandaserver/test/daod_on_demand.py +95 -0
  319. pandaserver/test/finishJob.py +184 -0
  320. pandaserver/test/finishTaskJEDI.py +11 -0
  321. pandaserver/test/getJobs.py +53 -0
  322. pandaserver/test/killJob.py +79 -0
  323. pandaserver/test/killJobLowPrio.py +99 -0
  324. pandaserver/test/killJobsInTask.py +56 -0
  325. pandaserver/test/killTask.py +80 -0
  326. pandaserver/test/killTaskJEDI.py +11 -0
  327. pandaserver/test/killUser.py +83 -0
  328. pandaserver/test/lsst/README.txt +7 -0
  329. pandaserver/test/lsst/lsstSubmit.py +139 -0
  330. pandaserver/test/lsst/lsstSubmitMERGEtest.py +141 -0
  331. pandaserver/test/lsst/lsstSubmitPhosim332.py +53 -0
  332. pandaserver/test/lsst/prepare-client-tarball-from-bigpanda-server.sh +41 -0
  333. pandaserver/test/reassignJobs.py +17 -0
  334. pandaserver/test/reassignSite.py +82 -0
  335. pandaserver/test/reassignTask.py +117 -0
  336. pandaserver/test/reassignWaiting.py +39 -0
  337. pandaserver/test/recoverLostFiles.py +7 -0
  338. pandaserver/test/reloadInputDS.py +51 -0
  339. pandaserver/test/sendCommandToJob.py +26 -0
  340. pandaserver/test/setDebugMode.py +38 -0
  341. pandaserver/test/setPriority.py +28 -0
  342. pandaserver/test/testEvgen.py +60 -0
  343. pandaserver/test/testEvgen17.py +58 -0
  344. pandaserver/test/testG4sim.py +81 -0
  345. pandaserver/test/testG4sim17.py +90 -0
  346. pandaserver/test/testGlobalShares.py +131 -0
  347. pandaserver/test/testReco.py +103 -0
  348. pandaserver/test/testSimulReco14.py +98 -0
  349. pandaserver/test/testSiteMap.py +26 -0
  350. pandaserver/test/testUpdateWorkerPilotStatus.py +9 -0
  351. pandaserver/userinterface/Client.py +1686 -0
  352. pandaserver/userinterface/UserIF.py +1799 -0
  353. pandaserver/userinterface/__init__.py +0 -0
  354. pandaserver/workflow/__init__.py +0 -0
  355. pandaserver/workflow/data_handler_plugins/base_data_handler.py +48 -0
  356. pandaserver/workflow/data_handler_plugins/ddm_collection_data_handler.py +100 -0
  357. pandaserver/workflow/data_handler_plugins/panda_task_data_handler.py +140 -0
  358. pandaserver/workflow/pcwl_test.py +122 -0
  359. pandaserver/workflow/pcwl_utils.py +384 -0
  360. pandaserver/workflow/psnakemake_container.json +68 -0
  361. pandaserver/workflow/psnakemake_task.json +62 -0
  362. pandaserver/workflow/psnakemake_test.py +60 -0
  363. pandaserver/workflow/snakeparser/__init__.py +3 -0
  364. pandaserver/workflow/snakeparser/extensions.py +74 -0
  365. pandaserver/workflow/snakeparser/log.py +53 -0
  366. pandaserver/workflow/snakeparser/names.py +3 -0
  367. pandaserver/workflow/snakeparser/parser.py +332 -0
  368. pandaserver/workflow/snakeparser/utils.py +29 -0
  369. pandaserver/workflow/step_handler_plugins/base_step_handler.py +85 -0
  370. pandaserver/workflow/step_handler_plugins/panda_task_step_handler.py +280 -0
  371. pandaserver/workflow/workflow_base.py +531 -0
  372. pandaserver/workflow/workflow_core.py +1974 -0
  373. pandaserver/workflow/workflow_core_smoketest.py +388 -0
  374. pandaserver/workflow/workflow_parser.py +222 -0
  375. pandaserver/workflow/workflow_utils.py +1047 -0
@@ -0,0 +1,1047 @@
1
+ import copy
2
+ import json
3
+ import re
4
+ import shlex
5
+ import tempfile
6
+
7
+ from idds.atlas.workflowv2.atlaslocalpandawork import ATLASLocalPandaWork
8
+ from idds.atlas.workflowv2.atlaspandawork import ATLASPandaWork
9
+ from idds.workflowv2.workflow import AndCondition, Condition, OrCondition, Workflow
10
+ from pandaclient import PhpoScript, PrunScript
11
+
12
+
13
+ # extract argument value from execution string
14
+ def get_arg_value(arg, exec_str):
15
+ args = shlex.split(exec_str)
16
+ if arg in args:
17
+ return args[args.index(arg) + 1]
18
+ for item in args:
19
+ if item.startswith(arg):
20
+ return item.split("=")[-1]
21
+ return None
22
+
23
+
24
+ # merge job parameters
25
+ def merge_job_params(base_params, io_params):
26
+ new_params = []
27
+ # remove exec stuff from base_params
28
+ exec_start = False
29
+ end_exec = False
30
+ for tmp_item in base_params:
31
+ if tmp_item["type"] == "constant" and tmp_item["value"].startswith("-p "):
32
+ exec_start = True
33
+ continue
34
+ if exec_start:
35
+ if end_exec:
36
+ pass
37
+ elif tmp_item["type"] == "constant" and "padding" not in tmp_item:
38
+ end_exec = True
39
+ continue
40
+ if exec_start and not end_exec:
41
+ continue
42
+ new_params.append(tmp_item)
43
+ # take exec and IO stuff from io_params
44
+ exec_start = False
45
+ for tmp_item in io_params:
46
+ if tmp_item["type"] == "constant" and tmp_item["value"] == "__delimiter__":
47
+ exec_start = True
48
+ continue
49
+ # ignore archive option
50
+ if tmp_item["type"] == "constant" and tmp_item["value"].startswith("-a "):
51
+ continue
52
+ if not exec_start:
53
+ continue
54
+ new_params.append(tmp_item)
55
+ return new_params
56
+
57
+
58
+ # DAG vertex
59
+ class Node(object):
60
+ def __init__(self, id, node_type, data, is_leaf, name):
61
+ self.id = id
62
+ self.type = node_type
63
+ self.data = data
64
+ self.is_leaf = is_leaf
65
+ self.is_tail = False
66
+ self.is_head = False
67
+ self.inputs = {}
68
+ self.outputs = {}
69
+ self.output_types = []
70
+ self.scatter = None
71
+ self.parents = set()
72
+ self.name = name
73
+ self.sub_nodes = set()
74
+ self.root_inputs = None
75
+ self.task_params = None
76
+ self.condition = None
77
+ self.is_workflow_output = False
78
+ self.loop = False
79
+ self.in_loop = False
80
+ self.upper_root_inputs = None
81
+
82
+ def add_parent(self, id):
83
+ self.parents.add(id)
84
+
85
+ # set real input values
86
+ def set_input_value(self, key, src_key, src_value):
87
+ # replace the value with a list of parameter names and indexes if value is a list,
88
+ # and src and dst are looping params
89
+ if isinstance(src_value, list):
90
+ src_loop_param_name = self.get_loop_param_name(src_key)
91
+ loop_params = self.get_loop_param_name(key.split("/")[-1]) is not None and src_loop_param_name is not None
92
+ if loop_params:
93
+ src_value = [{"src": src_loop_param_name, "idx": i} for i in range(len(src_value))]
94
+ # resolve values
95
+ if isinstance(self.inputs[key]["source"], list):
96
+ self.inputs[key].setdefault("value", copy.copy(self.inputs[key]["source"]))
97
+ tmp_list = []
98
+ for k in self.inputs[key]["value"]:
99
+ if k == src_key:
100
+ tmp_list.append(src_value)
101
+ else:
102
+ tmp_list.append(k)
103
+ self.inputs[key]["value"] = tmp_list
104
+ else:
105
+ self.inputs[key]["value"] = src_value
106
+
107
+ # convert inputs to dict inputs
108
+ def convert_dict_inputs(self, skip_suppressed=False):
109
+ data = {}
110
+ for k, v in self.inputs.items():
111
+ if skip_suppressed and "suppressed" in v and v["suppressed"]:
112
+ continue
113
+ y_name = k.split("/")[-1]
114
+ if "value" in v:
115
+ data[y_name] = v["value"]
116
+ elif "default" in v:
117
+ data[y_name] = v["default"]
118
+ else:
119
+ raise ReferenceError(f"{k} is not resolved")
120
+ return data
121
+
122
+ # convert outputs to set
123
+ def convert_set_outputs(self):
124
+ data = set()
125
+ for k, v in self.outputs.items():
126
+ if "value" in v:
127
+ data.add(v["value"])
128
+ return data
129
+
130
+ # verify
131
+ def verify(self):
132
+ if self.is_leaf:
133
+ dict_inputs = self.convert_dict_inputs(True)
134
+ # check input
135
+ for k, v in dict_inputs.items():
136
+ if v is None:
137
+ return False, f"{k} is unresolved"
138
+ # check args
139
+ for k in ["opt_exec", "opt_args"]:
140
+ test_str = dict_inputs.get(k)
141
+ if test_str:
142
+ m = re.search(r"%{[A-Z]*DS(\d+|\*)}", test_str)
143
+ if m:
144
+ return False, f"{m.group(0)} is unresolved in {k}"
145
+ if self.type == "prun":
146
+ for k in dict_inputs:
147
+ if k not in [
148
+ "opt_inDS",
149
+ "opt_inDsType",
150
+ "opt_secondaryDSs",
151
+ "opt_secondaryDsTypes",
152
+ "opt_args",
153
+ "opt_exec",
154
+ "opt_useAthenaPackages",
155
+ "opt_containerImage",
156
+ ]:
157
+ return False, f"unknown input parameter {k} for {self.type}"
158
+ elif self.type in ["junction", "reana"]:
159
+ for k in dict_inputs:
160
+ if k not in [
161
+ "opt_inDS",
162
+ "opt_inDsType",
163
+ "opt_args",
164
+ "opt_exec",
165
+ "opt_containerImage",
166
+ ]:
167
+ return False, f"unknown input parameter {k} for {self.type}"
168
+ elif self.type == "phpo":
169
+ for k in dict_inputs:
170
+ if k not in ["opt_trainingDS", "opt_trainingDsType", "opt_args"]:
171
+ return False, f"unknown input parameter {k} for {self.type}"
172
+ elif self.type == "gitlab":
173
+ for k in dict_inputs:
174
+ if k not in [
175
+ "opt_inDS",
176
+ "opt_args",
177
+ "opt_api",
178
+ "opt_projectID",
179
+ "opt_ref",
180
+ "opt_triggerToken",
181
+ "opt_accessToken",
182
+ "opt_site",
183
+ "opt_input_location",
184
+ ]:
185
+ return False, f"unknown input parameter {k} for {self.type}"
186
+ elif self.type == "workflow":
187
+ reserved_params = ["i"]
188
+ loop_global, workflow_global = self.get_global_parameters()
189
+ if loop_global:
190
+ for k in reserved_params:
191
+ if k in loop_global:
192
+ return (
193
+ False,
194
+ f"parameter {k} cannot be used since it is reserved by the system",
195
+ )
196
+ return True, ""
197
+
198
+ # string representation
199
+ def __str__(self):
200
+ outstr = f"ID:{self.id} Name:{self.name} Type:{self.type}\n"
201
+ outstr += f" Parent:{','.join([str(p) for p in self.parents])}\n"
202
+ outstr += " Input:\n"
203
+ for k, v in self.convert_dict_inputs().items():
204
+ outstr += f" {k}: {v}\n"
205
+ outstr += " Output:\n"
206
+ for k, v in self.outputs.items():
207
+ if "value" in v:
208
+ v = v["value"]
209
+ else:
210
+ v = "NA"
211
+ outstr += f" {v}\n"
212
+ return outstr
213
+
214
+ # short description
215
+ def short_desc(self):
216
+ return f"ID:{self.id} Name:{self.name} Type:{self.type}"
217
+
218
+ # resolve workload-specific parameters
219
+ def resolve_params(self, task_template=None, id_map=None, workflow=None):
220
+ if self.type in ["prun", "junction", "reana"]:
221
+ dict_inputs = self.convert_dict_inputs()
222
+ if "opt_secondaryDSs" in dict_inputs:
223
+ # look for secondaryDsTypes if missing
224
+ if "opt_secondaryDsTypes" not in dict_inputs:
225
+ dict_inputs["opt_secondaryDsTypes"] = []
226
+ for ds_name in dict_inputs["opt_secondaryDSs"]:
227
+ added = False
228
+ for pid in self.parents:
229
+ parent_node = id_map[pid]
230
+ if ds_name in parent_node.convert_set_outputs():
231
+ dict_inputs["opt_secondaryDsTypes"].append(parent_node.output_types[0])
232
+ added = True
233
+ break
234
+ if not added:
235
+ # use None if not found
236
+ dict_inputs["opt_secondaryDsTypes"].append(None)
237
+ # resolve secondary dataset names
238
+ idx = 1
239
+ list_sec_ds = []
240
+ for ds_name, ds_type in zip(dict_inputs["opt_secondaryDSs"], dict_inputs["opt_secondaryDsTypes"]):
241
+ if ds_type and "*" in ds_type:
242
+ ds_type = ds_type.replace("*", "XYZ")
243
+ ds_type += ".tgz"
244
+ src = f"%{{SECDS{idx}}}"
245
+ if ds_type:
246
+ dst = f"{ds_name}_{ds_type}/"
247
+ else:
248
+ dst = f"{ds_name}/"
249
+ dict_inputs["opt_exec"] = re.sub(src, dst, dict_inputs["opt_exec"])
250
+ dict_inputs["opt_args"] = re.sub(src, dst, dict_inputs["opt_args"])
251
+ idx += 1
252
+ list_sec_ds.append(src)
253
+ if list_sec_ds:
254
+ src = r"%{SECDS\*}"
255
+ if "opt_exec" in dict_inputs:
256
+ dict_inputs["opt_exec"] = re.sub(src, ",".join(list_sec_ds), dict_inputs["opt_exec"])
257
+ if "opt_args" in dict_inputs:
258
+ dict_inputs["opt_args"] = re.sub(src, ",".join(list_sec_ds), dict_inputs["opt_args"])
259
+ for k, v in self.inputs.items():
260
+ if k.endswith("opt_exec"):
261
+ v["value"] = dict_inputs["opt_exec"]
262
+ elif k.endswith("opt_args"):
263
+ v["value"] = dict_inputs["opt_args"]
264
+ if self.is_leaf and task_template:
265
+ self.task_params = self.make_task_params(task_template, id_map, workflow)
266
+ [n.resolve_params(task_template, id_map, self) for n in self.sub_nodes]
267
+
268
+ # create task params
269
+ def make_task_params(self, task_template, id_map, workflow_node):
270
+ # task name
271
+ for k, v in self.outputs.items():
272
+ task_name = v["value"]
273
+ break
274
+ if self.type in ["prun", "junction", "reana"]:
275
+ dict_inputs = self.convert_dict_inputs(skip_suppressed=True)
276
+ # check type
277
+ use_athena = False
278
+ if "opt_useAthenaPackages" in dict_inputs and dict_inputs["opt_useAthenaPackages"] and self.type != "reana":
279
+ use_athena = True
280
+ container_image = None
281
+ if "opt_containerImage" in dict_inputs and dict_inputs["opt_containerImage"]:
282
+ container_image = dict_inputs["opt_containerImage"]
283
+ if use_athena:
284
+ task_params = copy.deepcopy(task_template["athena"])
285
+ else:
286
+ task_params = copy.deepcopy(task_template["container"])
287
+ task_params["taskName"] = task_name
288
+ # cli params
289
+ com = ["prun"]
290
+ if self.type == "junction":
291
+ # add default output for junction
292
+ if "opt_args" not in dict_inputs:
293
+ dict_inputs["opt_args"] = ""
294
+ results_json = "results.json"
295
+ if "--outputs" not in dict_inputs["opt_args"]:
296
+ dict_inputs["opt_args"] += f" --outputs {results_json}"
297
+ else:
298
+ m = re.search("(--outputs)( +|=)([^ ]+)", dict_inputs["opt_args"])
299
+ if results_json not in m.group(3):
300
+ tmp_dst = m.group(1) + "=" + m.group(3) + "," + results_json
301
+ dict_inputs["opt_args"] = re.sub(m.group(0), tmp_dst, dict_inputs["opt_args"])
302
+ com += shlex.split(dict_inputs["opt_args"])
303
+ if "opt_inDS" in dict_inputs and dict_inputs["opt_inDS"]:
304
+ list_in_ds = self.get_input_ds_list(dict_inputs, id_map)
305
+ if self.type not in ["reana"]:
306
+ in_ds_str = ",".join(list_in_ds)
307
+ com += ["--inDS", in_ds_str, "--notExpandInDS", "--notExpandSecDSs"]
308
+ if self.type in ["junction"]:
309
+ com += ["--forceStaged", "--forceStagedSecondary"]
310
+ if self.type in ["prun", "junction", "reana"]:
311
+ # replace placeholders in opt_exec and opt_args
312
+ for idx, dst in enumerate(list_in_ds):
313
+ src = f"%{{DS{idx + 1}}}"
314
+ if "opt_exec" in dict_inputs:
315
+ dict_inputs["opt_exec"] = re.sub(src, dst, dict_inputs["opt_exec"])
316
+ if "opt_args" in dict_inputs:
317
+ dict_inputs["opt_args"] = re.sub(src, dst, dict_inputs["opt_args"])
318
+ if list_in_ds:
319
+ src = r"%{DS\*}"
320
+ if "opt_exec" in dict_inputs:
321
+ dict_inputs["opt_exec"] = re.sub(src, ",".join(list_in_ds), dict_inputs["opt_exec"])
322
+ if "opt_args" in dict_inputs:
323
+ dict_inputs["opt_args"] = re.sub(src, ",".join(list_in_ds), dict_inputs["opt_args"])
324
+ for k, v in self.inputs.items():
325
+ if k.endswith("opt_exec"):
326
+ v["value"] = dict_inputs["opt_exec"]
327
+ elif k.endswith("opt_args"):
328
+ v["value"] = dict_inputs["opt_args"]
329
+ # global parameters
330
+ if workflow_node:
331
+ tmp_global, tmp_workflow_global = workflow_node.get_global_parameters()
332
+ src_dst_list = []
333
+ # looping globals
334
+ if tmp_global:
335
+ for k in tmp_global:
336
+ tmp_src = f"%{{{k}}}"
337
+ tmp_dst = f"___idds___user_{k}___"
338
+ src_dst_list.append((tmp_src, tmp_dst))
339
+ # workflow globls
340
+ if tmp_workflow_global:
341
+ for k, v in tmp_workflow_global.items():
342
+ tmp_src = f"%{{{k}}}"
343
+ tmp_dst = f"{v}"
344
+ src_dst_list.append((tmp_src, tmp_dst))
345
+ # iteration count
346
+ tmp_src = "%{i}"
347
+ tmp_dst = "___idds___num_run___"
348
+ src_dst_list.append((tmp_src, tmp_dst))
349
+ # replace
350
+ for tmp_src, tmp_dst in src_dst_list:
351
+ if "opt_exec" in dict_inputs:
352
+ dict_inputs["opt_exec"] = re.sub(tmp_src, tmp_dst, dict_inputs["opt_exec"])
353
+ if "opt_args" in dict_inputs:
354
+ dict_inputs["opt_args"] = re.sub(tmp_src, tmp_dst, dict_inputs["opt_args"])
355
+ com += ["--exec", dict_inputs["opt_exec"]]
356
+ com += ["--outDS", task_name]
357
+ if container_image:
358
+ com += ["--containerImage", container_image]
359
+ parse_com = copy.copy(com[1:])
360
+ else:
361
+ # add dummy container to keep build step consistent
362
+ parse_com = copy.copy(com[1:])
363
+ parse_com += ["--containerImage", None]
364
+ # force a writable temp base for dry parsing regardless of process cwd
365
+ parse_com += ["--tmpDir", tempfile.gettempdir()]
366
+ athena_tag = False
367
+ if use_athena:
368
+ com += ["--useAthenaPackages"]
369
+ athena_tag = "--athenaTag" in com
370
+ # add cmtConfig
371
+ if athena_tag and "--cmtConfig" not in parse_com:
372
+ parse_com += [
373
+ "--cmtConfig",
374
+ task_params["architecture"].split("@")[0],
375
+ ]
376
+ # parse args without setting --useAthenaPackages since it requires real Athena runtime
377
+ parsed_params = PrunScript.main(True, parse_com, dry_mode=True)
378
+ task_params["cliParams"] = " ".join(shlex.quote(x) for x in com)
379
+ # set parsed parameters
380
+ for p_key, p_value in parsed_params.items():
381
+ if p_key in ["buildSpec"]:
382
+ continue
383
+ if p_key not in task_params or p_key in [
384
+ "log",
385
+ "container_name",
386
+ "multiStepExec",
387
+ "site",
388
+ "excludedSite",
389
+ "includedSite",
390
+ ]:
391
+ task_params[p_key] = p_value
392
+ elif p_key == "architecture":
393
+ task_params[p_key] = p_value
394
+ if not container_image:
395
+ if task_params[p_key] is None:
396
+ task_params[p_key] = ""
397
+ if "@" not in task_params[p_key] and "basePlatform" in task_params:
398
+ task_params[p_key] = f"{task_params[p_key]}@{task_params['basePlatform']}"
399
+ elif athena_tag:
400
+ if p_key in ["transUses", "transHome"]:
401
+ task_params[p_key] = p_value
402
+ # merge job params
403
+ task_params["jobParameters"] = merge_job_params(task_params["jobParameters"], parsed_params["jobParameters"])
404
+ # outputs
405
+ for tmp_item in task_params["jobParameters"]:
406
+ if tmp_item["type"] == "template" and tmp_item["param_type"] == "output":
407
+ if tmp_item["value"].startswith("regex|"):
408
+ self.output_types.append(re.search(r"_([^_]+)/$", tmp_item["dataset"]).group(1))
409
+ else:
410
+ self.output_types.append(re.search(r"}\.(.+)$", tmp_item["value"]).group(1))
411
+ # add a dummy output if empty. this is to allow association to downstream steps which is described through outputs
412
+ if not self.output_types:
413
+ self.output_types.append("dummy")
414
+ # container
415
+ if not container_image:
416
+ if "container_name" in task_params:
417
+ del task_params["container_name"]
418
+ if "multiStepExec" in task_params:
419
+ del task_params["multiStepExec"]
420
+ if "basePlatform" in task_params:
421
+ del task_params["basePlatform"]
422
+ # no build
423
+ if use_athena and "--noBuild" in parse_com:
424
+ for tmp_item in task_params["jobParameters"]:
425
+ if tmp_item["type"] == "constant" and tmp_item["value"] == "-l ${LIB}":
426
+ tmp_item["value"] = f"-a {task_params['buildSpec']['archiveName']}"
427
+ del task_params["buildSpec"]
428
+ # parent
429
+ # if self.parents and len(self.parents) == 1:
430
+ # task_params["noWaitParent"] = True
431
+ # task_params["parentTaskName"] = id_map[list(self.parents)[0]].task_params["taskName"]
432
+ # notification
433
+ if not self.is_workflow_output:
434
+ task_params["noEmail"] = True
435
+ # use instant PQs
436
+ if self.type in ["junction", "reana"]:
437
+ task_params["runOnInstant"] = True
438
+ # return
439
+ return task_params
440
+ elif self.type == "phpo":
441
+ dict_inputs = self.convert_dict_inputs(skip_suppressed=True)
442
+ # extract source and base URL
443
+ source_url = task_template["container"]["sourceURL"]
444
+ for tmp_item in task_template["container"]["jobParameters"]:
445
+ if tmp_item["type"] == "constant" and tmp_item["value"].startswith("-a "):
446
+ source_name = tmp_item["value"].split()[-1]
447
+ # cli params
448
+ com = shlex.split(dict_inputs["opt_args"])
449
+ if "opt_trainingDS" in dict_inputs and dict_inputs["opt_trainingDS"]:
450
+ if "opt_trainingDsType" not in dict_inputs or not dict_inputs["opt_trainingDsType"]:
451
+ in_ds_suffix = None
452
+ for parent_id in self.parents:
453
+ parent_node = id_map[parent_id]
454
+ if dict_inputs["opt_trainingDS"] in parent_node.convert_set_outputs():
455
+ in_ds_suffix = parent_node.output_types[0]
456
+ break
457
+ else:
458
+ in_ds_suffix = dict_inputs["opt_inDsType"]
459
+ in_ds_str = f"{dict_inputs['opt_trainingDS']}_{in_ds_suffix}/"
460
+ com += ["--trainingDS", in_ds_str]
461
+ com += ["--outDS", task_name]
462
+ # get task params
463
+ task_params = PhpoScript.main(True, com, dry_mode=True)
464
+ # change sandbox
465
+ new_job_params = []
466
+ for tmp_item in task_params["jobParameters"]:
467
+ if tmp_item["type"] == "constant" and tmp_item["value"].startswith("-a "):
468
+ tmp_item["value"] = f"-a {source_name} --sourceURL {source_url}"
469
+ new_job_params.append(tmp_item)
470
+ task_params["jobParameters"] = new_job_params
471
+ # return
472
+ return task_params
473
+ elif self.type == "gitlab":
474
+ dict_inputs = self.convert_dict_inputs(skip_suppressed=True)
475
+ list_in_ds = self.get_input_ds_list(dict_inputs, id_map)
476
+ task_params = copy.copy(task_template["container"])
477
+ task_params["taskName"] = task_name
478
+ task_params["noInput"] = True
479
+ task_params["nEventsPerJob"] = 1
480
+ task_params["nEvents"] = 1
481
+ task_params["processingType"] = re.sub(r"-[^-]+$", "-gitlab", task_params["processingType"])
482
+ task_params["useSecrets"] = True
483
+ task_params["site"] = dict_inputs["opt_site"]
484
+ task_params["cliParams"] = ""
485
+ task_params["log"]["container"] = task_params["log"]["dataset"] = f"{task_name}.log/"
486
+ # set gitlab parameters
487
+ task_params["jobParameters"] = [
488
+ {
489
+ "type": "constant",
490
+ "value": json.dumps(
491
+ {
492
+ "project_api": dict_inputs["opt_api"],
493
+ "project_id": int(dict_inputs["opt_projectID"]),
494
+ "ref": dict_inputs["opt_ref"],
495
+ "trigger_token": dict_inputs["opt_triggerToken"],
496
+ "access_token": dict_inputs["opt_accessToken"],
497
+ "input_datasets": ",".join(list_in_ds),
498
+ "input_location": dict_inputs.get("opt_input_location"),
499
+ }
500
+ ),
501
+ }
502
+ ]
503
+
504
+ del task_params["container_name"]
505
+ del task_params["multiStepExec"]
506
+ return task_params
507
+ return None
508
+
509
+ # get global parameters in the workflow
510
+ def get_global_parameters(self):
511
+ if self.is_leaf:
512
+ root_inputs = self.upper_root_inputs
513
+ else:
514
+ root_inputs = self.root_inputs
515
+ if root_inputs is None:
516
+ return None, None
517
+ loop_params = {}
518
+ workflow_params = {}
519
+ for k, v in root_inputs.items():
520
+ m = self.get_loop_param_name(k)
521
+ if m:
522
+ loop_params[m] = v
523
+ else:
524
+ param = k.split("#")[-1]
525
+ workflow_params[param] = v
526
+ return loop_params, workflow_params
527
+
528
+ # get all sub node IDs
529
+ def get_all_sub_node_ids(self, all_ids=None):
530
+ if all_ids is None:
531
+ all_ids = set()
532
+ all_ids.add(self.id)
533
+ for sub_node in self.sub_nodes:
534
+ all_ids.add(sub_node.id)
535
+ if not sub_node.is_leaf:
536
+ sub_node.get_all_sub_node_ids(all_ids)
537
+ return all_ids
538
+
539
+ # get loop param name
540
+ def get_loop_param_name(self, k):
541
+ param = k.split("#")[-1]
542
+ m = re.search(r"^param_(.+)", param)
543
+ if m:
544
+ return m.group(1)
545
+ return None
546
+
547
+ # def get input dataset list
548
+ def get_input_ds_list(self, dict_inputs, id_map):
549
+ if "opt_inDS" not in dict_inputs:
550
+ return []
551
+ if isinstance(dict_inputs["opt_inDS"], list):
552
+ is_list_in_ds = True
553
+ else:
554
+ is_list_in_ds = False
555
+ if "opt_inDsType" not in dict_inputs or not dict_inputs["opt_inDsType"]:
556
+ if is_list_in_ds:
557
+ in_ds_suffix = []
558
+ in_ds_list = dict_inputs["opt_inDS"]
559
+ else:
560
+ in_ds_suffix = None
561
+ in_ds_list = [dict_inputs["opt_inDS"]]
562
+ for tmp_in_ds in in_ds_list:
563
+ for parent_id in self.parents:
564
+ parent_node = id_map[parent_id]
565
+ if tmp_in_ds in parent_node.convert_set_outputs():
566
+ if is_list_in_ds:
567
+ in_ds_suffix.append(parent_node.output_types[0])
568
+ else:
569
+ in_ds_suffix = parent_node.output_types[0]
570
+ break
571
+ else:
572
+ in_ds_suffix = dict_inputs["opt_inDsType"]
573
+ if "*" in in_ds_suffix:
574
+ in_ds_suffix = in_ds_suffix.replace("*", "XYZ") + ".tgz"
575
+ if is_list_in_ds:
576
+ list_in_ds = [f"{s1}_{s2}/" if s2 else s1 for s1, s2 in zip(dict_inputs["opt_inDS"], in_ds_suffix)]
577
+ else:
578
+ list_in_ds = [f"{dict_inputs['opt_inDS']}_{in_ds_suffix}/" if in_ds_suffix else dict_inputs["opt_inDS"]]
579
+ return list_in_ds
580
+
581
+
582
+ # dump nodes
583
+ def dump_nodes(node_list, dump_str=None, only_leaves=False):
584
+ if dump_str is None:
585
+ dump_str = "\n"
586
+ for node in node_list:
587
+ if node.is_leaf:
588
+ dump_str += f"{node}"
589
+ if node.task_params is not None:
590
+ dump_str += json.dumps(node.task_params, indent=4, sort_keys=True)
591
+ dump_str += "\n\n"
592
+ else:
593
+ if not only_leaves:
594
+ dump_str += f"{node}\n"
595
+ dump_str = dump_nodes(node.sub_nodes, dump_str, only_leaves)
596
+ return dump_str
597
+
598
+
599
+ # get id map
600
+ def get_node_id_map(node_list, id_map=None):
601
+ if id_map is None:
602
+ id_map = {}
603
+ for node in node_list:
604
+ id_map[node.id] = node
605
+ if node.sub_nodes:
606
+ id_map = get_node_id_map(node.sub_nodes, id_map)
607
+ return id_map
608
+
609
+
610
+ # get all parents
611
+ def get_all_parents(node_list, all_parents=None):
612
+ if all_parents is None:
613
+ all_parents = set()
614
+ for node in node_list:
615
+ all_parents |= node.parents
616
+ if node.sub_nodes:
617
+ all_parents = get_all_parents(node.sub_nodes, all_parents)
618
+ return all_parents
619
+
620
+
621
+ # set workflow outputs
622
+ def set_workflow_outputs(node_list, all_parents=None):
623
+ if all_parents is None:
624
+ all_parents = get_all_parents(node_list)
625
+ for node in node_list:
626
+ if node.is_leaf and node.id not in all_parents:
627
+ node.is_workflow_output = True
628
+ if node.sub_nodes:
629
+ set_workflow_outputs(node.sub_nodes, all_parents)
630
+
631
+
632
+ # convert parameter names to parent IDs
633
+ def convert_params_in_condition_to_parent_ids(condition_item, input_data, id_map):
634
+ for item in ["left", "right"]:
635
+ param = getattr(condition_item, item)
636
+ if isinstance(param, str):
637
+ m = re.search(r"^[^\[]+\[(\d+)\]", param)
638
+ if m:
639
+ param = param.split("[")[0]
640
+ idx = int(m.group(1))
641
+ else:
642
+ idx = None
643
+ isOK = False
644
+ for tmp_name, tmp_data in input_data.items():
645
+ if param == tmp_name.split("/")[-1]:
646
+ isOK = True
647
+ if isinstance(tmp_data["parent_id"], list):
648
+ if idx is not None:
649
+ if idx < 0 or idx >= len(tmp_data["parent_id"]):
650
+ raise IndexError(f"index {idx} is out of bounds for parameter {param} with {len(tmp_data['parent_id'])} parents")
651
+ parent_id = tmp_data["parent_id"][idx]
652
+ if parent_id not in id_map:
653
+ raise ReferenceError(f"unresolved parent_id {parent_id} for parameter {param}[{idx}]")
654
+ setattr(condition_item, item, id_map[parent_id])
655
+ else:
656
+ resolved_parent_ids = set()
657
+ for parent_id in tmp_data["parent_id"]:
658
+ if parent_id not in id_map:
659
+ raise ReferenceError(f"unresolved parent_id {parent_id} for parameter {param}")
660
+ resolved_parent_ids |= id_map[parent_id]
661
+ setattr(condition_item, item, list(resolved_parent_ids))
662
+ else:
663
+ if tmp_data["parent_id"] not in id_map:
664
+ raise ReferenceError(f"unresolved parent_id {tmp_data['parent_id']} for parameter {param}")
665
+ setattr(condition_item, item, id_map[tmp_data["parent_id"]])
666
+ break
667
+ if not isOK:
668
+ raise ReferenceError(f"unresolved parameter {param} in the condition string")
669
+ elif isinstance(param, ConditionItem):
670
+ convert_params_in_condition_to_parent_ids(param, input_data, id_map)
671
+
672
+
673
+ # resolve nodes
674
+ def resolve_nodes(node_list, root_inputs, data, serial_id, parent_ids, out_ds_name, log_stream):
675
+ for k in root_inputs:
676
+ kk = k.split("#")[-1]
677
+ if kk in data:
678
+ root_inputs[k] = data[kk]
679
+ tmp_to_real_id_map = {}
680
+ resolved_map = {}
681
+ # map of object identity to original temporary node ID used in resolved_map keys
682
+ node_key_map = {}
683
+ all_nodes = []
684
+ for node in node_list:
685
+ # resolve input
686
+ for tmp_name, tmp_data in node.inputs.items():
687
+ if not tmp_data["source"]:
688
+ continue
689
+ if isinstance(tmp_data["source"], list):
690
+ tmp_sources = tmp_data["source"]
691
+ if "parent_id" in tmp_data:
692
+ # Make a copy to avoid mutating the original list stored in node.inputs
693
+ tmp_parent_ids = list(tmp_data["parent_id"])
694
+ tmp_parent_ids += [None] * (len(tmp_sources) - len(tmp_parent_ids))
695
+ else:
696
+ tmp_parent_ids = [None] * len(tmp_sources)
697
+ else:
698
+ tmp_sources = [tmp_data["source"]]
699
+ if "parent_id" in tmp_data:
700
+ tmp_parent_ids = [tmp_data["parent_id"]]
701
+ else:
702
+ tmp_parent_ids = [None] * len(tmp_sources)
703
+ for tmp_source, tmp_parent_id in zip(tmp_sources, tmp_parent_ids):
704
+ isOK = False
705
+ # check root input
706
+ if tmp_source in root_inputs:
707
+ node.is_head = True
708
+ node.set_input_value(tmp_name, tmp_source, root_inputs[tmp_source])
709
+ continue
710
+ # check parent output
711
+ for i in node.parents:
712
+ for r_node in resolved_map[i]:
713
+ if tmp_source in r_node.outputs:
714
+ node.set_input_value(
715
+ tmp_name,
716
+ tmp_source,
717
+ r_node.outputs[tmp_source]["value"],
718
+ )
719
+ isOK = True
720
+ break
721
+ if isOK:
722
+ break
723
+ if isOK:
724
+ continue
725
+ # check resolved parent outputs
726
+ if tmp_parent_id is not None:
727
+ values = [list(r_node.outputs.values())[0]["value"] for r_node in resolved_map[tmp_parent_id]]
728
+ if len(values) == 1:
729
+ values = values[0]
730
+ node.set_input_value(tmp_name, tmp_source, values)
731
+ continue
732
+ # scatter
733
+ if node.scatter:
734
+ # resolve scattered parameters
735
+ scatters = None
736
+ sc_nodes = []
737
+ for item in node.scatter:
738
+ if scatters is None:
739
+ scatters = [{item: v} for v in node.inputs[item]["value"]]
740
+ else:
741
+ [i.update({item: v}) for i, v in zip(scatters, node.inputs[item]["value"])]
742
+ for idx, item in enumerate(scatters):
743
+ sc_node = copy.deepcopy(node)
744
+ for k, v in item.items():
745
+ sc_node.inputs[k]["value"] = v
746
+ for tmp_node in sc_node.sub_nodes:
747
+ tmp_node.scatter_index = idx
748
+ tmp_node.upper_root_inputs = sc_node.root_inputs
749
+ sc_nodes.append(sc_node)
750
+ else:
751
+ sc_nodes = [node]
752
+ # loop over scattered nodes
753
+ for sc_node in sc_nodes:
754
+ original_node_id = sc_node.id
755
+ all_nodes.append(sc_node)
756
+ node_key_map[id(sc_node)] = original_node_id
757
+ # set real node ID
758
+ resolved_map.setdefault(original_node_id, [])
759
+ tmp_to_real_id_map.setdefault(original_node_id, set())
760
+ # resolve parents
761
+ real_parens = set()
762
+ for i in sc_node.parents:
763
+ real_parens |= tmp_to_real_id_map[i]
764
+ sc_node.parents = real_parens
765
+ if sc_node.is_head:
766
+ sc_node.parents |= parent_ids
767
+ if sc_node.is_leaf:
768
+ resolved_map[original_node_id].append(sc_node)
769
+ tmp_to_real_id_map[original_node_id].add(serial_id)
770
+ sc_node.id = serial_id
771
+ serial_id += 1
772
+ else:
773
+ serial_id, sub_tail_nodes, sc_node.sub_nodes = resolve_nodes(
774
+ sc_node.sub_nodes,
775
+ sc_node.root_inputs,
776
+ sc_node.convert_dict_inputs(),
777
+ serial_id,
778
+ sc_node.parents,
779
+ out_ds_name,
780
+ log_stream,
781
+ )
782
+ resolved_map[original_node_id] += sub_tail_nodes
783
+ tmp_to_real_id_map[original_node_id] |= set([n.id for n in sub_tail_nodes])
784
+ sc_node.id = serial_id
785
+ serial_id += 1
786
+ # convert parameters to parent IDs in conditions
787
+ if sc_node.condition:
788
+ convert_params_in_condition_to_parent_ids(sc_node.condition, sc_node.inputs, tmp_to_real_id_map)
789
+ # resolve outputs
790
+ if sc_node.is_leaf:
791
+ for tmp_name, tmp_data in sc_node.outputs.items():
792
+ tmp_data["value"] = f"{out_ds_name}_{sc_node.id:03d}_{sc_node.name}"
793
+ # add loop count for nodes in a loop
794
+ if sc_node.in_loop:
795
+ tmp_data["value"] += ".___idds___num_run___"
796
+ # return tails
797
+ tail_nodes = []
798
+ for node in all_nodes:
799
+ original_node_id = node_key_map.get(id(node), node.id)
800
+ if node.is_tail:
801
+ tail_nodes.append(node)
802
+ else:
803
+ tail_nodes += resolved_map[original_node_id]
804
+ return serial_id, tail_nodes, all_nodes
805
+
806
+
807
+ # condition item
808
+ class ConditionItem(object):
809
+ def __init__(self, left, right=None, operator=None):
810
+ if operator not in ["and", "or", "not", None]:
811
+ raise TypeError(f"unknown operator '{operator}'")
812
+ if operator in ["not", None] and right:
813
+ raise TypeError(f"right param is given for operator '{operator}'")
814
+ self.left = left
815
+ self.right = right
816
+ self.operator = operator
817
+
818
+ def get_dict_form(self, serial_id=None, dict_form=None):
819
+ if dict_form is None:
820
+ dict_form = {}
821
+ is_entry = True
822
+ else:
823
+ is_entry = False
824
+ if serial_id is None:
825
+ serial_id = 0
826
+ if isinstance(self.left, ConditionItem):
827
+ serial_id, dict_form = self.left.get_dict_form(serial_id, dict_form)
828
+ left_id = serial_id
829
+ serial_id += 1
830
+ else:
831
+ left_id = self.left
832
+ if isinstance(self.right, ConditionItem):
833
+ serial_id, dict_form = self.right.get_dict_form(serial_id, dict_form)
834
+ right_id = serial_id
835
+ serial_id += 1
836
+ else:
837
+ right_id = self.right
838
+ dict_form[serial_id] = {
839
+ "left": left_id,
840
+ "right": right_id,
841
+ "operator": self.operator,
842
+ }
843
+ if is_entry:
844
+ # sort
845
+ keys = sorted(dict_form.keys())
846
+ return [(k, dict_form[k]) for k in keys]
847
+ else:
848
+ return serial_id, dict_form
849
+
850
+
851
+ # convert nodes to workflow
852
+ def convert_nodes_to_workflow(nodes, workflow_node=None, workflow=None, workflow_name=None):
853
+ if workflow is None:
854
+ is_top = True
855
+ workflow = Workflow()
856
+ workflow.name = workflow_name
857
+ else:
858
+ is_top = False
859
+ id_work_map = {}
860
+ all_sub_id_work_map = {}
861
+ sub_to_id_map = {}
862
+ cond_dump_str = " Conditions\n"
863
+ class_dump_str = f"===== Workflow ID:{workflow_node.id if workflow_node else workflow_name} ====\n"
864
+ class_dump_str += " Works\n"
865
+ dump_str_list = []
866
+ # create works or workflows
867
+ for node in nodes:
868
+ if node.is_leaf:
869
+ # work
870
+ if node.type == "junction":
871
+ work = ATLASLocalPandaWork(task_parameters=node.task_params)
872
+ work.add_custom_condition("to_exit", True)
873
+ else:
874
+ work = ATLASPandaWork(task_parameters=node.task_params)
875
+ workflow.add_work(work)
876
+ id_work_map[node.id] = work
877
+ class_dump_str += f" {node.short_desc()} Class:{work.__class__.__name__}\n"
878
+ else:
879
+ # sub workflow
880
+ sub_workflow = Workflow()
881
+ id_work_map[node.id] = sub_workflow
882
+ class_dump_str += f" {node.short_desc()} Class:{sub_workflow.__class__.__name__}\n"
883
+ sub_id_work_map, tmp_dump_str_list = convert_nodes_to_workflow(node.sub_nodes, node, sub_workflow)
884
+ dump_str_list += tmp_dump_str_list
885
+ for sub_id in node.get_all_sub_node_ids():
886
+ all_sub_id_work_map[sub_id] = sub_workflow
887
+ sub_to_id_map[sub_id] = node.id
888
+ # add loop condition
889
+ if node.loop:
890
+ for sub_node in node.sub_nodes:
891
+ if sub_node.type == "junction":
892
+ # use to_continue for loop termination
893
+ j_work = sub_id_work_map[sub_node.id]
894
+ j_work.add_custom_condition(key="to_continue", value=True)
895
+ cond = Condition(cond=j_work.get_custom_condition_status)
896
+ sub_workflow.add_loop_condition(cond)
897
+ cond_dump_str += f" Loop in ID:{node.id} with terminator ID:{sub_node.id}\n"
898
+ break
899
+ workflow.add_work(sub_workflow)
900
+ # add conditions
901
+ for node in nodes:
902
+ if node.parents:
903
+ c_work = id_work_map[node.id]
904
+ if not node.condition:
905
+ # default conditions if unspecified
906
+ cond_func_list = []
907
+ for p_id in node.parents:
908
+ if p_id in id_work_map:
909
+ p_work = id_work_map[p_id]
910
+ str_p_id = p_id
911
+ elif p_id in all_sub_id_work_map:
912
+ p_work = all_sub_id_work_map[p_id]
913
+ str_p_id = sub_to_id_map[p_id]
914
+ else:
915
+ # head node
916
+ continue
917
+ if len(node.parents) > 1 or isinstance(p_work, Workflow) or node.type in ["junction", "reana", "gitlab"]:
918
+ cond_function = p_work.is_processed
919
+ else:
920
+ cond_function = p_work.is_started
921
+ if cond_function not in cond_func_list:
922
+ cond_func_list.append(cond_function)
923
+ cond_dump_str += f" Default Link ID:{str_p_id} {cond_function.__name__} -> ID:{node.id}\n"
924
+ cond = AndCondition(true_works=[c_work], conditions=cond_func_list)
925
+ workflow.add_condition(cond)
926
+ else:
927
+ # convert conditions
928
+ cond_list = node.condition.get_dict_form()
929
+ base_cond_map = {}
930
+ str_cond_map = {}
931
+ root_condition = None
932
+ for tmp_idx, base_cond in cond_list:
933
+ # leaf condition
934
+ if base_cond["right"] is None:
935
+ # condition based on works
936
+ cond_func_list = []
937
+ str_func_list = []
938
+ for p_id in base_cond["left"]:
939
+ if p_id in id_work_map:
940
+ p_work = id_work_map[p_id]
941
+ str_p_id = p_id
942
+ else:
943
+ p_work = all_sub_id_work_map[p_id]
944
+ str_p_id = sub_to_id_map[p_id]
945
+ # finished or failed
946
+ if base_cond["operator"] is None:
947
+ cond_function = p_work.is_processed
948
+ else:
949
+ cond_function = p_work.is_failed
950
+ cond_func_list.append(cond_function)
951
+ str_func_list.append(f"ID:{str_p_id} {cond_function.__name__}")
952
+ cond = AndCondition(conditions=cond_func_list)
953
+ base_cond_map[tmp_idx] = cond
954
+ str_func = "AND ".join(str_func_list)
955
+ str_cond_map[tmp_idx] = str_func
956
+ cond_dump_str += f" Unary Ops {cond.__class__.__name__}({str_func}) -> ID:{node.id}\n"
957
+ root_condition = cond
958
+ else:
959
+ # composite condition
960
+ l_str_func_list = []
961
+ r_str_func_list = []
962
+ if isinstance(base_cond["left"], set):
963
+ cond_func_list = []
964
+ for p_id in base_cond["left"]:
965
+ if p_id in id_work_map:
966
+ p_work = id_work_map[p_id]
967
+ str_p_id = p_id
968
+ else:
969
+ p_work = all_sub_id_work_map[p_id]
970
+ str_p_id = sub_to_id_map[p_id]
971
+ cond_function = p_work.is_processed
972
+ cond_func_list.append(cond_function)
973
+ l_str_func_list.append(f"ID:{str_p_id} {cond_function.__name__}")
974
+ l_cond = AndCondition(conditions=cond_func_list)
975
+ l_str_func = "AND ".join(l_str_func_list)
976
+ str_cond_map[base_cond["left"]] = l_str_func
977
+ else:
978
+ l_cond = base_cond_map[base_cond["left"]]
979
+ l_str_func = str_cond_map[base_cond["left"]]
980
+ if isinstance(base_cond["right"], set):
981
+ cond_func_list = []
982
+ for p_id in base_cond["right"]:
983
+ if p_id in id_work_map:
984
+ p_work = id_work_map[p_id]
985
+ str_p_id = p_id
986
+ else:
987
+ p_work = all_sub_id_work_map[p_id]
988
+ str_p_id = sub_to_id_map[p_id]
989
+ cond_function = p_work.is_processed
990
+ cond_func_list.append(cond_function)
991
+ r_str_func_list.append(f"ID:{str_p_id} {cond_function.__name__}")
992
+ r_cond = AndCondition(conditions=cond_func_list)
993
+ r_str_func = "AND ".join(r_str_func_list)
994
+ str_cond_map[base_cond["right"]] = r_str_func
995
+ else:
996
+ r_cond = base_cond_map[base_cond["right"]]
997
+ r_str_func = str_cond_map[base_cond["right"]]
998
+ if base_cond["operator"] == "and":
999
+ cond = AndCondition(
1000
+ conditions=[
1001
+ l_cond.is_condition_true,
1002
+ r_cond.is_condition_true,
1003
+ ]
1004
+ )
1005
+ else:
1006
+ cond = OrCondition(
1007
+ conditions=[
1008
+ l_cond.is_condition_true,
1009
+ r_cond.is_condition_true,
1010
+ ]
1011
+ )
1012
+ base_cond_map[tmp_idx] = cond
1013
+ cond_dump_str += f" Binary Ops {cond.__class__.__name__}({l_str_func}, {r_str_func}) for ID:{node.id}\n"
1014
+ root_condition = cond
1015
+ # set root condition
1016
+ if root_condition:
1017
+ root_condition.true_works = [c_work]
1018
+ workflow.add_condition(root_condition)
1019
+ # global parameters
1020
+ if workflow_node:
1021
+ tmp_global, tmp_workflow_global = workflow_node.get_global_parameters()
1022
+ if tmp_global:
1023
+ loop_locals = {}
1024
+ loop_slices = []
1025
+ for k, v in tmp_global.items():
1026
+ if not isinstance(v, dict):
1027
+ # normal looping locals
1028
+ loop_locals["user_" + k] = tmp_global[k]
1029
+ else:
1030
+ # sliced locals
1031
+ v["src"] = "user_" + v["src"]
1032
+ loop_slices.append([k, v])
1033
+ if loop_locals:
1034
+ workflow.set_global_parameters(loop_locals)
1035
+ for k, v in loop_slices:
1036
+ workflow.set_sliced_global_parameters(source=v["src"], index=v["idx"], name="user_" + k)
1037
+ cond_dump_str += "\n Looping local variables\n"
1038
+ cond_dump_str += f" {tmp_global}\n"
1039
+ if tmp_workflow_global:
1040
+ cond_dump_str += "\n Workflow local variable\n"
1041
+ cond_dump_str += f" {tmp_workflow_global}\n"
1042
+ # dump strings
1043
+ dump_str_list.insert(0, class_dump_str + "\n" + cond_dump_str + "\n\n")
1044
+ # return
1045
+ if not is_top:
1046
+ return id_work_map, dump_str_list
1047
+ return workflow, dump_str_list