wmglobalqueue 2.4.5.1__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 (347) hide show
  1. Utils/CPMetrics.py +270 -0
  2. Utils/CertTools.py +100 -0
  3. Utils/EmailAlert.py +50 -0
  4. Utils/ExtendedUnitTestCase.py +62 -0
  5. Utils/FileTools.py +182 -0
  6. Utils/IteratorTools.py +80 -0
  7. Utils/MathUtils.py +31 -0
  8. Utils/MemoryCache.py +119 -0
  9. Utils/Patterns.py +24 -0
  10. Utils/Pipeline.py +137 -0
  11. Utils/PortForward.py +97 -0
  12. Utils/ProcFS.py +112 -0
  13. Utils/ProcessStats.py +194 -0
  14. Utils/PythonVersion.py +17 -0
  15. Utils/Signals.py +36 -0
  16. Utils/TemporaryEnvironment.py +27 -0
  17. Utils/Throttled.py +227 -0
  18. Utils/Timers.py +130 -0
  19. Utils/Timestamps.py +86 -0
  20. Utils/TokenManager.py +143 -0
  21. Utils/Tracing.py +60 -0
  22. Utils/TwPrint.py +98 -0
  23. Utils/Utilities.py +318 -0
  24. Utils/__init__.py +11 -0
  25. Utils/wmcoreDTools.py +707 -0
  26. WMCore/ACDC/Collection.py +57 -0
  27. WMCore/ACDC/CollectionTypes.py +12 -0
  28. WMCore/ACDC/CouchCollection.py +67 -0
  29. WMCore/ACDC/CouchFileset.py +238 -0
  30. WMCore/ACDC/CouchService.py +73 -0
  31. WMCore/ACDC/DataCollectionService.py +485 -0
  32. WMCore/ACDC/Fileset.py +94 -0
  33. WMCore/ACDC/__init__.py +11 -0
  34. WMCore/Algorithms/Alarm.py +39 -0
  35. WMCore/Algorithms/MathAlgos.py +274 -0
  36. WMCore/Algorithms/MiscAlgos.py +67 -0
  37. WMCore/Algorithms/ParseXMLFile.py +115 -0
  38. WMCore/Algorithms/Permissions.py +27 -0
  39. WMCore/Algorithms/Singleton.py +58 -0
  40. WMCore/Algorithms/SubprocessAlgos.py +129 -0
  41. WMCore/Algorithms/__init__.py +7 -0
  42. WMCore/Cache/GenericDataCache.py +98 -0
  43. WMCore/Cache/WMConfigCache.py +572 -0
  44. WMCore/Cache/__init__.py +0 -0
  45. WMCore/Configuration.py +659 -0
  46. WMCore/DAOFactory.py +47 -0
  47. WMCore/DataStructs/File.py +177 -0
  48. WMCore/DataStructs/Fileset.py +140 -0
  49. WMCore/DataStructs/Job.py +182 -0
  50. WMCore/DataStructs/JobGroup.py +142 -0
  51. WMCore/DataStructs/JobPackage.py +49 -0
  52. WMCore/DataStructs/LumiList.py +734 -0
  53. WMCore/DataStructs/Mask.py +219 -0
  54. WMCore/DataStructs/MathStructs/ContinuousSummaryHistogram.py +197 -0
  55. WMCore/DataStructs/MathStructs/DiscreteSummaryHistogram.py +92 -0
  56. WMCore/DataStructs/MathStructs/SummaryHistogram.py +117 -0
  57. WMCore/DataStructs/MathStructs/__init__.py +0 -0
  58. WMCore/DataStructs/Pickleable.py +24 -0
  59. WMCore/DataStructs/Run.py +256 -0
  60. WMCore/DataStructs/Subscription.py +175 -0
  61. WMCore/DataStructs/WMObject.py +47 -0
  62. WMCore/DataStructs/WorkUnit.py +112 -0
  63. WMCore/DataStructs/Workflow.py +60 -0
  64. WMCore/DataStructs/__init__.py +8 -0
  65. WMCore/Database/CMSCouch.py +1430 -0
  66. WMCore/Database/ConfigDBMap.py +29 -0
  67. WMCore/Database/CouchMonitoring.py +450 -0
  68. WMCore/Database/CouchUtils.py +118 -0
  69. WMCore/Database/DBCore.py +198 -0
  70. WMCore/Database/DBCreator.py +113 -0
  71. WMCore/Database/DBExceptionHandler.py +59 -0
  72. WMCore/Database/DBFactory.py +117 -0
  73. WMCore/Database/DBFormatter.py +177 -0
  74. WMCore/Database/Dialects.py +13 -0
  75. WMCore/Database/ExecuteDAO.py +327 -0
  76. WMCore/Database/MongoDB.py +241 -0
  77. WMCore/Database/MySQL/Destroy.py +42 -0
  78. WMCore/Database/MySQL/ListUserContent.py +20 -0
  79. WMCore/Database/MySQL/__init__.py +9 -0
  80. WMCore/Database/MySQLCore.py +132 -0
  81. WMCore/Database/Oracle/Destroy.py +56 -0
  82. WMCore/Database/Oracle/ListUserContent.py +19 -0
  83. WMCore/Database/Oracle/__init__.py +9 -0
  84. WMCore/Database/ResultSet.py +44 -0
  85. WMCore/Database/Transaction.py +91 -0
  86. WMCore/Database/__init__.py +9 -0
  87. WMCore/Database/ipy_profile_couch.py +438 -0
  88. WMCore/GlobalWorkQueue/CherryPyThreads/CleanUpTask.py +29 -0
  89. WMCore/GlobalWorkQueue/CherryPyThreads/HeartbeatMonitor.py +105 -0
  90. WMCore/GlobalWorkQueue/CherryPyThreads/LocationUpdateTask.py +28 -0
  91. WMCore/GlobalWorkQueue/CherryPyThreads/ReqMgrInteractionTask.py +35 -0
  92. WMCore/GlobalWorkQueue/CherryPyThreads/__init__.py +0 -0
  93. WMCore/GlobalWorkQueue/__init__.py +0 -0
  94. WMCore/GroupUser/CouchObject.py +127 -0
  95. WMCore/GroupUser/Decorators.py +51 -0
  96. WMCore/GroupUser/Group.py +33 -0
  97. WMCore/GroupUser/Interface.py +73 -0
  98. WMCore/GroupUser/User.py +96 -0
  99. WMCore/GroupUser/__init__.py +11 -0
  100. WMCore/Lexicon.py +836 -0
  101. WMCore/REST/Auth.py +202 -0
  102. WMCore/REST/CherryPyPeriodicTask.py +166 -0
  103. WMCore/REST/Error.py +333 -0
  104. WMCore/REST/Format.py +642 -0
  105. WMCore/REST/HeartbeatMonitorBase.py +90 -0
  106. WMCore/REST/Main.py +636 -0
  107. WMCore/REST/Server.py +2435 -0
  108. WMCore/REST/Services.py +24 -0
  109. WMCore/REST/Test.py +120 -0
  110. WMCore/REST/Tools.py +38 -0
  111. WMCore/REST/Validation.py +250 -0
  112. WMCore/REST/__init__.py +1 -0
  113. WMCore/ReqMgr/DataStructs/RequestStatus.py +209 -0
  114. WMCore/ReqMgr/DataStructs/RequestType.py +13 -0
  115. WMCore/ReqMgr/DataStructs/__init__.py +0 -0
  116. WMCore/ReqMgr/__init__.py +1 -0
  117. WMCore/Services/AlertManager/AlertManagerAPI.py +111 -0
  118. WMCore/Services/AlertManager/__init__.py +0 -0
  119. WMCore/Services/CRIC/CRIC.py +238 -0
  120. WMCore/Services/CRIC/__init__.py +0 -0
  121. WMCore/Services/DBS/DBS3Reader.py +1044 -0
  122. WMCore/Services/DBS/DBSConcurrency.py +44 -0
  123. WMCore/Services/DBS/DBSErrors.py +112 -0
  124. WMCore/Services/DBS/DBSReader.py +23 -0
  125. WMCore/Services/DBS/DBSUtils.py +166 -0
  126. WMCore/Services/DBS/DBSWriterObjects.py +381 -0
  127. WMCore/Services/DBS/ProdException.py +133 -0
  128. WMCore/Services/DBS/__init__.py +8 -0
  129. WMCore/Services/FWJRDB/FWJRDBAPI.py +118 -0
  130. WMCore/Services/FWJRDB/__init__.py +0 -0
  131. WMCore/Services/HTTPS/HTTPSAuthHandler.py +66 -0
  132. WMCore/Services/HTTPS/__init__.py +0 -0
  133. WMCore/Services/LogDB/LogDB.py +201 -0
  134. WMCore/Services/LogDB/LogDBBackend.py +191 -0
  135. WMCore/Services/LogDB/LogDBExceptions.py +11 -0
  136. WMCore/Services/LogDB/LogDBReport.py +85 -0
  137. WMCore/Services/LogDB/__init__.py +0 -0
  138. WMCore/Services/MSPileup/__init__.py +0 -0
  139. WMCore/Services/MSUtils/MSUtils.py +54 -0
  140. WMCore/Services/MSUtils/__init__.py +0 -0
  141. WMCore/Services/McM/McM.py +173 -0
  142. WMCore/Services/McM/__init__.py +8 -0
  143. WMCore/Services/MonIT/Grafana.py +133 -0
  144. WMCore/Services/MonIT/__init__.py +0 -0
  145. WMCore/Services/PyCondor/PyCondorAPI.py +154 -0
  146. WMCore/Services/PyCondor/__init__.py +0 -0
  147. WMCore/Services/ReqMgr/ReqMgr.py +261 -0
  148. WMCore/Services/ReqMgr/__init__.py +0 -0
  149. WMCore/Services/ReqMgrAux/ReqMgrAux.py +419 -0
  150. WMCore/Services/ReqMgrAux/__init__.py +0 -0
  151. WMCore/Services/RequestDB/RequestDBReader.py +267 -0
  152. WMCore/Services/RequestDB/RequestDBWriter.py +39 -0
  153. WMCore/Services/RequestDB/__init__.py +0 -0
  154. WMCore/Services/Requests.py +624 -0
  155. WMCore/Services/Rucio/Rucio.py +1290 -0
  156. WMCore/Services/Rucio/RucioUtils.py +74 -0
  157. WMCore/Services/Rucio/__init__.py +0 -0
  158. WMCore/Services/RucioConMon/RucioConMon.py +121 -0
  159. WMCore/Services/RucioConMon/__init__.py +0 -0
  160. WMCore/Services/Service.py +400 -0
  161. WMCore/Services/StompAMQ/__init__.py +0 -0
  162. WMCore/Services/TagCollector/TagCollector.py +155 -0
  163. WMCore/Services/TagCollector/XMLUtils.py +98 -0
  164. WMCore/Services/TagCollector/__init__.py +0 -0
  165. WMCore/Services/UUIDLib.py +13 -0
  166. WMCore/Services/UserFileCache/UserFileCache.py +160 -0
  167. WMCore/Services/UserFileCache/__init__.py +8 -0
  168. WMCore/Services/WMAgent/WMAgent.py +63 -0
  169. WMCore/Services/WMAgent/__init__.py +0 -0
  170. WMCore/Services/WMArchive/CMSSWMetrics.py +526 -0
  171. WMCore/Services/WMArchive/DataMap.py +463 -0
  172. WMCore/Services/WMArchive/WMArchive.py +33 -0
  173. WMCore/Services/WMArchive/__init__.py +0 -0
  174. WMCore/Services/WMBS/WMBS.py +97 -0
  175. WMCore/Services/WMBS/__init__.py +0 -0
  176. WMCore/Services/WMStats/DataStruct/RequestInfoCollection.py +300 -0
  177. WMCore/Services/WMStats/DataStruct/__init__.py +0 -0
  178. WMCore/Services/WMStats/WMStatsPycurl.py +145 -0
  179. WMCore/Services/WMStats/WMStatsReader.py +445 -0
  180. WMCore/Services/WMStats/WMStatsWriter.py +273 -0
  181. WMCore/Services/WMStats/__init__.py +0 -0
  182. WMCore/Services/WMStatsServer/WMStatsServer.py +134 -0
  183. WMCore/Services/WMStatsServer/__init__.py +0 -0
  184. WMCore/Services/WorkQueue/WorkQueue.py +492 -0
  185. WMCore/Services/WorkQueue/__init__.py +0 -0
  186. WMCore/Services/__init__.py +8 -0
  187. WMCore/Services/pycurl_manager.py +574 -0
  188. WMCore/WMBase.py +50 -0
  189. WMCore/WMConnectionBase.py +164 -0
  190. WMCore/WMException.py +183 -0
  191. WMCore/WMExceptions.py +269 -0
  192. WMCore/WMFactory.py +76 -0
  193. WMCore/WMInit.py +377 -0
  194. WMCore/WMLogging.py +104 -0
  195. WMCore/WMSpec/ConfigSectionTree.py +442 -0
  196. WMCore/WMSpec/Persistency.py +135 -0
  197. WMCore/WMSpec/Steps/BuildMaster.py +87 -0
  198. WMCore/WMSpec/Steps/BuildTools.py +201 -0
  199. WMCore/WMSpec/Steps/Builder.py +97 -0
  200. WMCore/WMSpec/Steps/Diagnostic.py +89 -0
  201. WMCore/WMSpec/Steps/Emulator.py +62 -0
  202. WMCore/WMSpec/Steps/ExecuteMaster.py +208 -0
  203. WMCore/WMSpec/Steps/Executor.py +210 -0
  204. WMCore/WMSpec/Steps/StepFactory.py +213 -0
  205. WMCore/WMSpec/Steps/TaskEmulator.py +75 -0
  206. WMCore/WMSpec/Steps/Template.py +204 -0
  207. WMCore/WMSpec/Steps/Templates/AlcaHarvest.py +76 -0
  208. WMCore/WMSpec/Steps/Templates/CMSSW.py +613 -0
  209. WMCore/WMSpec/Steps/Templates/DQMUpload.py +59 -0
  210. WMCore/WMSpec/Steps/Templates/DeleteFiles.py +70 -0
  211. WMCore/WMSpec/Steps/Templates/LogArchive.py +84 -0
  212. WMCore/WMSpec/Steps/Templates/LogCollect.py +105 -0
  213. WMCore/WMSpec/Steps/Templates/StageOut.py +105 -0
  214. WMCore/WMSpec/Steps/Templates/__init__.py +10 -0
  215. WMCore/WMSpec/Steps/WMExecutionFailure.py +21 -0
  216. WMCore/WMSpec/Steps/__init__.py +8 -0
  217. WMCore/WMSpec/Utilities.py +63 -0
  218. WMCore/WMSpec/WMSpecErrors.py +12 -0
  219. WMCore/WMSpec/WMStep.py +347 -0
  220. WMCore/WMSpec/WMTask.py +1997 -0
  221. WMCore/WMSpec/WMWorkload.py +2288 -0
  222. WMCore/WMSpec/WMWorkloadTools.py +382 -0
  223. WMCore/WMSpec/__init__.py +9 -0
  224. WMCore/WorkQueue/DataLocationMapper.py +273 -0
  225. WMCore/WorkQueue/DataStructs/ACDCBlock.py +47 -0
  226. WMCore/WorkQueue/DataStructs/Block.py +48 -0
  227. WMCore/WorkQueue/DataStructs/CouchWorkQueueElement.py +148 -0
  228. WMCore/WorkQueue/DataStructs/WorkQueueElement.py +274 -0
  229. WMCore/WorkQueue/DataStructs/WorkQueueElementResult.py +152 -0
  230. WMCore/WorkQueue/DataStructs/WorkQueueElementsSummary.py +185 -0
  231. WMCore/WorkQueue/DataStructs/__init__.py +0 -0
  232. WMCore/WorkQueue/Policy/End/EndPolicyInterface.py +44 -0
  233. WMCore/WorkQueue/Policy/End/SingleShot.py +22 -0
  234. WMCore/WorkQueue/Policy/End/__init__.py +32 -0
  235. WMCore/WorkQueue/Policy/PolicyInterface.py +17 -0
  236. WMCore/WorkQueue/Policy/Start/Block.py +258 -0
  237. WMCore/WorkQueue/Policy/Start/Dataset.py +180 -0
  238. WMCore/WorkQueue/Policy/Start/MonteCarlo.py +131 -0
  239. WMCore/WorkQueue/Policy/Start/ResubmitBlock.py +171 -0
  240. WMCore/WorkQueue/Policy/Start/StartPolicyInterface.py +316 -0
  241. WMCore/WorkQueue/Policy/Start/__init__.py +34 -0
  242. WMCore/WorkQueue/Policy/__init__.py +57 -0
  243. WMCore/WorkQueue/WMBSHelper.py +772 -0
  244. WMCore/WorkQueue/WorkQueue.py +1237 -0
  245. WMCore/WorkQueue/WorkQueueBackend.py +750 -0
  246. WMCore/WorkQueue/WorkQueueBase.py +39 -0
  247. WMCore/WorkQueue/WorkQueueExceptions.py +44 -0
  248. WMCore/WorkQueue/WorkQueueReqMgrInterface.py +278 -0
  249. WMCore/WorkQueue/WorkQueueUtils.py +130 -0
  250. WMCore/WorkQueue/__init__.py +13 -0
  251. WMCore/Wrappers/JsonWrapper/JSONThunker.py +342 -0
  252. WMCore/Wrappers/JsonWrapper/__init__.py +7 -0
  253. WMCore/Wrappers/__init__.py +6 -0
  254. WMCore/__init__.py +10 -0
  255. wmglobalqueue-2.4.5.1.data/data/bin/wmc-dist-patch +15 -0
  256. wmglobalqueue-2.4.5.1.data/data/bin/wmc-dist-unpatch +8 -0
  257. wmglobalqueue-2.4.5.1.data/data/bin/wmc-httpd +3 -0
  258. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/.couchapprc +1 -0
  259. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/README.md +40 -0
  260. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/index.html +264 -0
  261. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/js/ElementInfoByWorkflow.js +96 -0
  262. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/js/StuckElementInfo.js +57 -0
  263. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/js/WorkloadInfoTable.js +80 -0
  264. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/js/dataTable.js +70 -0
  265. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/js/namespace.js +23 -0
  266. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/_attachments/style/main.css +75 -0
  267. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/couchapp.json +4 -0
  268. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/filters/childQueueFilter.js +13 -0
  269. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/filters/filterDeletedDocs.js +3 -0
  270. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/filters/queueFilter.js +11 -0
  271. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/language +1 -0
  272. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lib/mustache.js +333 -0
  273. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lib/validate.js +27 -0
  274. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lib/workqueue_utils.js +61 -0
  275. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lists/elementsDetail.js +28 -0
  276. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lists/filter.js +86 -0
  277. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lists/stuckElements.js +38 -0
  278. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lists/workRestrictions.js +153 -0
  279. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/lists/workflowSummary.js +28 -0
  280. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/rewrites.json +73 -0
  281. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/shows/redirect.js +23 -0
  282. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/shows/status.js +40 -0
  283. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/ElementSummaryByWorkflow.html +27 -0
  284. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/StuckElementSummary.html +26 -0
  285. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/TaskStatus.html +23 -0
  286. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/WorkflowSummary.html +27 -0
  287. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/partials/workqueue-common-lib.html +2 -0
  288. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/partials/yui-lib-remote.html +16 -0
  289. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/templates/partials/yui-lib.html +18 -0
  290. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/updates/in-place.js +50 -0
  291. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/validate_doc_update.js +8 -0
  292. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/vendor/couchapp/_attachments/jquery.couch.app.js +235 -0
  293. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/vendor/couchapp/_attachments/jquery.pathbinder.js +173 -0
  294. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/activeData/map.js +8 -0
  295. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/activeData/reduce.js +2 -0
  296. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/activeParentData/map.js +8 -0
  297. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/activeParentData/reduce.js +2 -0
  298. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/activePileupData/map.js +8 -0
  299. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/activePileupData/reduce.js +2 -0
  300. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/analyticsData/map.js +11 -0
  301. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/analyticsData/reduce.js +1 -0
  302. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/availableByPriority/map.js +6 -0
  303. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/conflicts/map.js +5 -0
  304. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elements/map.js +5 -0
  305. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByData/map.js +8 -0
  306. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByParent/map.js +8 -0
  307. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByParentData/map.js +8 -0
  308. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByPileupData/map.js +8 -0
  309. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByStatus/map.js +8 -0
  310. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsBySubscription/map.js +6 -0
  311. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByWorkflow/map.js +8 -0
  312. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsByWorkflow/reduce.js +3 -0
  313. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/elementsDetailByWorkflowAndStatus/map.js +26 -0
  314. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobInjectStatusByRequest/map.js +10 -0
  315. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobInjectStatusByRequest/reduce.js +1 -0
  316. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobStatusByRequest/map.js +6 -0
  317. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobStatusByRequest/reduce.js +1 -0
  318. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByChildQueueAndPriority/map.js +6 -0
  319. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByChildQueueAndPriority/reduce.js +1 -0
  320. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByChildQueueAndStatus/map.js +6 -0
  321. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByChildQueueAndStatus/reduce.js +1 -0
  322. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByRequest/map.js +6 -0
  323. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByRequest/reduce.js +1 -0
  324. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByStatus/map.js +6 -0
  325. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByStatus/reduce.js +1 -0
  326. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByStatusAndPriority/map.js +6 -0
  327. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/jobsByStatusAndPriority/reduce.js +1 -0
  328. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/openRequests/map.js +6 -0
  329. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/recent-items/map.js +5 -0
  330. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/siteWhitelistByRequest/map.js +6 -0
  331. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/siteWhitelistByRequest/reduce.js +1 -0
  332. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/specsByWorkflow/map.js +5 -0
  333. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/stuckElements/map.js +38 -0
  334. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/wmbsInjectStatusByRequest/map.js +12 -0
  335. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/wmbsInjectStatusByRequest/reduce.js +3 -0
  336. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/wmbsUrl/map.js +6 -0
  337. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/wmbsUrl/reduce.js +2 -0
  338. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/wmbsUrlByRequest/map.js +6 -0
  339. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/wmbsUrlByRequest/reduce.js +2 -0
  340. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/workflowSummary/map.js +9 -0
  341. wmglobalqueue-2.4.5.1.data/data/data/couchapps/WorkQueue/views/workflowSummary/reduce.js +10 -0
  342. wmglobalqueue-2.4.5.1.dist-info/METADATA +26 -0
  343. wmglobalqueue-2.4.5.1.dist-info/RECORD +347 -0
  344. wmglobalqueue-2.4.5.1.dist-info/WHEEL +5 -0
  345. wmglobalqueue-2.4.5.1.dist-info/licenses/LICENSE +202 -0
  346. wmglobalqueue-2.4.5.1.dist-info/licenses/NOTICE +16 -0
  347. wmglobalqueue-2.4.5.1.dist-info/top_level.txt +2 -0
WMCore/REST/Server.py ADDED
@@ -0,0 +1,2435 @@
1
+ from builtins import str, zip, range, object
2
+ from future.utils import viewitems, viewvalues, listvalues
3
+
4
+ import cherrypy
5
+ import inspect
6
+ import os
7
+ import re
8
+ import signal
9
+ from string import ascii_letters as letters
10
+ import time
11
+ from collections import namedtuple
12
+ from functools import wraps
13
+ from threading import Thread, Condition
14
+
15
+ from cherrypy import engine, expose, request, response, HTTPError, HTTPRedirect, tools
16
+ from cherrypy.lib import cpstats
17
+
18
+ from WMCore.REST.Error import *
19
+ from WMCore.REST.Format import *
20
+ from WMCore.REST.Validation import validate_no_more_input
21
+ from Utils.CPMetrics import promMetrics
22
+
23
+ from Utils.Utilities import encodeUnicodeToBytes
24
+
25
+ try:
26
+ from cherrypy.lib import httputil
27
+ except:
28
+ from cherrypy.lib import http as httputil
29
+
30
+ #: List of HTTP methods for which it's possible to register a REST handler.
31
+ _METHODS = ('GET', 'HEAD', 'POST', 'PUT', 'DELETE')
32
+
33
+ #: Regexp for censoring passwords out of SQL statements before logging them.
34
+ _RX_CENSOR = re.compile(r"(identified by) \S+", re.I)
35
+
36
+ #: MIME types which are compressible.
37
+ _COMPRESSIBLE = ['text/html', 'text/html; charset=utf-8',
38
+ 'text/plain', 'text/plain; charset=utf-8',
39
+ 'text/css', 'text/css; charset=utf-8',
40
+ 'text/javascript', 'text/javascript; charset=utf-8',
41
+ 'application/json']
42
+
43
+ #: Type alias for arguments passed to REST validation methods, consisting
44
+ #: of `args`, the additional path arguments, and `kwargs`, the query
45
+ #: arguments either from the query string or body (but not both).
46
+ RESTArgs = namedtuple("RESTArgs", ["args", "kwargs"])
47
+
48
+
49
+ ######################################################################
50
+ ######################################################################
51
+ class RESTFrontPage(object):
52
+ """Base class for a trivial front page intended to hand everything
53
+ over to a javascript-based user interface implementation.
54
+
55
+ This front-page simply serves static content like HTML pages, CSS,
56
+ JavaScript and images from number of configurable document roots.
57
+ Text content such as CSS and JavaScript can be scrunched together,
58
+ or combo-loaded, from several files. All the content supports the
59
+ standard ETag and last-modified validation for optimal caching.
60
+
61
+ The base class assumes the actual application consists of a single
62
+ front-page, which loads JavaScript and other content dynamically,
63
+ and uses HTML5 URL history features to figure out what to do. That
64
+ is, given application mount point <https://cmsweb.cern.ch/app>, all
65
+ links such as <https://cmsweb.cern.ch/app/foo/bar?q=xyz> get mapped
66
+ to the same page, which then figures out what to do at /foo/bar
67
+ relative location or with the query string part.
68
+
69
+ There is a special response for ``rest/preamble.js`` static file.
70
+ This will automatically generate a scriptlet of the following form,
71
+ plus any additional content passed in `preamble`::
72
+
73
+ var REST_DEBUG = (debug_mode),
74
+ REST_SERVER_ROOT = "(mount)",
75
+ REST_INSTANCES = [{ "id": "...", "title": "...", "rank": N }...];
76
+
77
+ REST_DEBUG, ``debug_mode``
78
+ Is set to true/false depending on the value of the constructor
79
+ :ref:`debug_mode` parameter, or if the default None, it's set
80
+ to false if running with minimised assets, i.e. frontpage matches
81
+ ``*-min.html``, true otherwise.
82
+
83
+ REST_SERVER_ROOT, ``mount``
84
+ The URL mount point of this object, needed for history init. Taken
85
+ from the constructor argument.
86
+
87
+ REST_INSTANCES
88
+ If the constructor is given `instances`, its return value is turned
89
+ into a sorted JSON list of available instances for JavaScript. Each
90
+ database instance should have the dictionary keys "``.title``" and
91
+ "``.order``" which will be used for human visible instance label and
92
+ order of appearance, respectively. The ``id`` is the label to use
93
+ for REST API URL construction: the instance dictionary key. This
94
+ variable will not be emitted at all if :ref:`instances` is None.
95
+
96
+ .. rubric:: Attributes
97
+
98
+ .. attribute:: _app
99
+
100
+ Reference to the application object given to the constructor.
101
+
102
+ .. attribute:: _mount
103
+
104
+ The mount point given in the constructor.
105
+
106
+ .. attribute:: _static
107
+
108
+ The roots given to the constructor, with ``rest/preamble.js`` added.
109
+
110
+ .. attribute:: _frontpage
111
+
112
+ The name of the front page file.
113
+
114
+ .. attribute:: _substitutions
115
+
116
+ Extra (name, value) substitutions for `_frontpage`. When serving
117
+ the front-page via `_serve()`, each ``@name@`` is replaced by its
118
+ corresponding `value`.
119
+
120
+ .. attribute:: _embeddings
121
+
122
+ Extra (name, value) file replacements for `_frontpage`. Similar to
123
+ `_substitutions` but each value is a list of file names, and the
124
+ replacement value is the concatenation of all the contents of all
125
+ the files, with leading and trailing white space removed.
126
+
127
+ .. attribute:: _preamble
128
+
129
+ The ``rest/preamble.js`` computed as described above.
130
+
131
+ .. attribute:: _time
132
+
133
+ Server start-up time, used as mtime for ``rest/preamble.js``.
134
+ """
135
+
136
+ def __init__(self, app, config, mount, frontpage, roots,
137
+ substitutions=None, embeddings=None,
138
+ instances=None, preamble=None, debug_mode=None):
139
+ """.. rubric:: Constructor
140
+
141
+ :arg app: Reference to the :class:`~.RESTMain` application.
142
+ :arg config: :class:`~.WMCore.ConfigSection` for me.
143
+ :arg str mount: URL tree mount point for this object.
144
+ :arg str frontpage: Name of the front-page file, which must exist in
145
+ one of the `roots`. If `debug_mode` is None and
146
+ the name matches ``*-min.html``, then debug mode
147
+ is set to False, True otherwise.
148
+ :arg dict roots: Dictionary of roots for serving static files.
149
+ Each key defines the label and path root for URLs,
150
+ and the value should have keys "``root``" for the
151
+ path to start looking up files, and "``rx``" for
152
+ the regular expression to define valid file names.
153
+ **All the root paths must end in a trailing slash.**
154
+ :arg dict substitutions: Extra (name, value) substitutions for `frontpage`.
155
+ :arg dict embeddings: Extra (name, value) file replacements for `frontpage`.
156
+ :arg callable instances: Callable which returns database instances, often
157
+ ``lambda: return self._app.views["data"]._db``
158
+ :arg str preamble: Optional string for additional content for the
159
+ pseudo-file ``rest/preamble.js``.
160
+ :arg bool debug_mode: Specifies how to set REST_DEBUG, see above."""
161
+
162
+ # Verify all roots do end in a slash.
163
+ for origin, info in viewitems(roots):
164
+ if not re.match(r"^[-a-z0-9]+$", origin):
165
+ raise ValueError("invalid root label")
166
+ if not info["root"].endswith("/"):
167
+ raise ValueError("%s 'root' must end in a slash" % origin)
168
+
169
+ # Add preamble pseudo-root.
170
+ roots["rest"] = {"root": None, "rx": re.compile(r"^preamble(?:-min)?\.js$")}
171
+
172
+ # Save various things.
173
+ self._start = time.time()
174
+ self._app = app
175
+ self._mount = mount
176
+ self._frontpage = frontpage
177
+ self._static = roots
178
+ self._substitutions = substitutions or {}
179
+ self._embeddings = embeddings or {}
180
+ if debug_mode is None:
181
+ debug_mode = not frontpage.endswith("-min.html")
182
+
183
+ # Delay preamble setup until server start-up so that we don't try to
184
+ # dereference instances() until after it's been finished constructing.
185
+ engine.subscribe("start", lambda: self._init(debug_mode, instances, preamble), 0)
186
+
187
+ def _init(self, debug_mode, instances, preamble):
188
+ """Delayed preamble initialisation after server is fully configured."""
189
+ self._preamble = ("var REST_DEBUG = %s" % ((debug_mode and "true") or "false"))
190
+ self._preamble += (", REST_SERVER_ROOT = '%s'" % self._mount)
191
+
192
+ if instances:
193
+ instances = [dict(id=k, title=v[".title"], order=v[".order"])
194
+ for k, v in viewitems(instances())]
195
+ instances.sort(key=lambda x: x["order"])
196
+ self._preamble += (", REST_INSTANCES = %s" % json.dumps(instances))
197
+
198
+ self._preamble += ";\n%s" % (preamble or "")
199
+
200
+ def _serve(self, items):
201
+ """Serve static assets.
202
+
203
+ Serve one or more files. If there is just one file, it can be text or
204
+ an image. If there are several files, they are smashed together as a
205
+ combo load operation. In that case it's assumed the files are compatible,
206
+ for example all JavaScript or all CSS.
207
+
208
+ All normal response headers are set correctly, including Content-Type,
209
+ Last-Modified, Cache-Control and ETag. Automatically handles caching
210
+ related request headers If-Match, If-None-Match, If-Modified-Since,
211
+ If-Unmodified-Since and responds appropriately. The caller should use
212
+ CherryPy gzip tool to handle compression-related headers appropriately.
213
+
214
+ In general files are passed through unmodified. The only exception is
215
+ that HTML files will have @MOUNT@ string replaced with the mount point,
216
+ the @NAME@ substitutions from the constructor are replaced by value,
217
+ and @NAME@ embeddings are replaced by file contents.
218
+
219
+ :arg list(str) items: One or more file names to serve.
220
+ :returns: File contents combined as a single string."""
221
+ mtime = 0
222
+ result = ""
223
+ ctype = ""
224
+
225
+ if not items:
226
+ raise HTTPError(404, "No such file")
227
+
228
+ for item in items:
229
+ # There must be at least one slash in the file name.
230
+ if item.find("/") < 0:
231
+ cherrypy.log("ERROR: directory required for front-page '%s'" % item)
232
+ raise HTTPError(404, "No such file")
233
+
234
+ # Split the name to the origin part - the name we look up in roots,
235
+ # and the remaining path part for the rest of the name under that
236
+ # root. For example 'yui/yui/yui-min.js' means we'll look up the
237
+ # path 'yui/yui-min.js' under the 'yui' root.
238
+ origin, path = item.split("/", 1)
239
+ if origin not in self._static:
240
+ cherrypy.log("ERROR: front-page '%s' origin '%s' not in any static root"
241
+ % (item, origin))
242
+ raise HTTPError(404, "No such file")
243
+
244
+ # Look up the description and match path name against validation rx.
245
+ desc = self._static[origin]
246
+ if not desc["rx"].match(path):
247
+ cherrypy.log("ERROR: front-page '%s' not matched by rx '%s' for '%s'"
248
+ % (item, desc["rx"].pattern, origin))
249
+ raise HTTPError(404, "No such file")
250
+
251
+ # If this is not the pseudo-preamble, make sure the requested file
252
+ # exists, and if it does, read it and remember its mtime. For the
253
+ # pseudo preamble use the precomputed string and server start time.
254
+ if origin != "rest":
255
+ fpath = desc["root"] + path
256
+ if not os.access(fpath, os.R_OK):
257
+ cherrypy.log("ERROR: front-page '%s' file does not exist" % item)
258
+ raise HTTPError(404, "No such file")
259
+ try:
260
+ mtime = max(mtime, os.stat(fpath).st_mtime)
261
+ data = open(fpath).read()
262
+ except:
263
+ cherrypy.log("ERROR: front-page '%s' failed to retrieve file" % item)
264
+ raise HTTPError(404, "No such file")
265
+ elif self._preamble:
266
+ mtime = max(mtime, self._start)
267
+ data = self._preamble
268
+ else:
269
+ cherrypy.log("ERROR: front-page '%s' no preamble for 'rest'?" % item)
270
+ raise HTTPError(404, "No such file")
271
+
272
+ # Concatenate contents and set content type based on name suffix.
273
+ ctypemap = {"js": "text/javascript; charset=utf-8",
274
+ "css": "text/css; charset=utf-8",
275
+ "html": "text/html; charset=utf-8"}
276
+ suffix = path.rsplit(".", 1)[-1]
277
+ if suffix in ctypemap:
278
+ if not ctype:
279
+ ctype = ctypemap[suffix]
280
+ elif ctype != ctypemap[suffix]:
281
+ ctype = "text/plain"
282
+ if suffix == "html":
283
+ for var, value in viewitems(self._substitutions):
284
+ data = data.replace("@" + var + "@", value)
285
+ for var, files in viewitems(self._embeddings):
286
+ value = ""
287
+ for fpath in files:
288
+ if not os.access(fpath, os.R_OK):
289
+ cherrypy.log("ERROR: embedded '%s' file '%s' does not exist"
290
+ % (var, fpath))
291
+ raise HTTPError(404, "No such file")
292
+ try:
293
+ mtime = max(mtime, os.stat(fpath).st_mtime)
294
+ value += open(fpath).read().strip()
295
+ except:
296
+ cherrypy.log("ERROR: embedded '%s' file '%s' failed to"
297
+ " retrieve file" % (var, fpath))
298
+ raise HTTPError(404, "No such file")
299
+ data = data.replace("@" + var + "@", value)
300
+ data = data.replace("@MOUNT@", self._mount)
301
+ if result:
302
+ result += "\n"
303
+ result += data
304
+ if not result.endswith("\n"):
305
+ result += "\n"
306
+ elif suffix == "gif":
307
+ ctype = "image/gif"
308
+ result = data
309
+ elif suffix == "png":
310
+ ctype = "image/png"
311
+ result = data
312
+ else:
313
+ raise HTTPError(404, "Unexpected file type")
314
+
315
+ # Build final response + headers.
316
+ response.headers['Content-Type'] = ctype
317
+ response.headers['Last-Modified'] = httputil.HTTPDate(mtime)
318
+ response.headers['Cache-Control'] = "public, max-age=%d" % 86400
319
+ response.headers['ETag'] = '"%s"' % hashlib.sha1(encodeUnicodeToBytes(result)).hexdigest()
320
+ cherrypy.lib.cptools.validate_since()
321
+ cherrypy.lib.cptools.validate_etags()
322
+ return result
323
+
324
+ @expose
325
+ @tools.gzip(compress_level=9, mime_types=_COMPRESSIBLE)
326
+ def static(self, *args, **kwargs):
327
+ """Serve static assets.
328
+
329
+ Assumes a query string in the format used by YUI combo loader, with one
330
+ or more file names separated by ampersands (&). Each name must be a plain
331
+ file name, to be found in one of the roots given to the constructor.
332
+
333
+ Path arguments must be empty, or consist of a single 'yui' string, for
334
+ use as the YUI combo loader. In that case all file names are prefixed
335
+ with 'yui/' to make them compatible with the standard combo loading.
336
+
337
+ Serves assets as documented in :meth:`_serve`."""
338
+ # The code was originally designed to server YUI content.
339
+ # Modified to support any content by joining args into single path
340
+ # on web page it can be /path/static/js/file.js or /path/static/css/file.css
341
+ if len(args) > 1 or (args and args[0] != "yui"):
342
+ return self._serve(['/'.join(args)])
343
+ # Path arguments must be empty, or consist of a single 'yui' string,
344
+ paths = request.query_string.split("&")
345
+ if not paths:
346
+ raise HTTPError(404, "No such file")
347
+ if args:
348
+ paths = [args[0] + "/" + p for p in paths]
349
+ return self._serve(paths)
350
+
351
+ @expose
352
+ def feedback(self, *args, **kwargs):
353
+ """Receive browser problem feedback. Doesn't actually do anything, just
354
+ returns an empty string response."""
355
+ return ""
356
+
357
+ @expose
358
+ @tools.gzip(compress_level=9, mime_types=_COMPRESSIBLE)
359
+ def default(self, *args, **kwargs):
360
+ """Generate the front page, as documented in :meth:`_serve`. The
361
+ JavaScript will actually work out what to do with the rest of the
362
+ URL arguments; they are not used here."""
363
+ return self._serve([self._frontpage])
364
+
365
+ @expose
366
+ def stats(self):
367
+ """
368
+ Return CherryPy stats dict about underlying service activities
369
+ """
370
+ return cpstats.StatsPage().data()
371
+
372
+ @expose
373
+ def metrics(self):
374
+ """
375
+ Return CherryPy stats following the prometheus metrics structure
376
+ """
377
+ metrics = promMetrics(cpstats.StatsPage().data(), self.app.appname)
378
+ return encodeUnicodeToBytes(metrics)
379
+
380
+
381
+ ######################################################################
382
+ ######################################################################
383
+ class MiniRESTApi(object):
384
+ """Minimal base class for REST services.
385
+
386
+ .. rubric:: Overview
387
+
388
+ This is the main base class for the CherryPy-based REST API interfaces.
389
+ It provides a minimal interface for registering API methods to be called
390
+ via REST HTTP calls. Normally implementations should derive from the
391
+ :class:`~.RESTApi` higher level abstraction.
392
+
393
+ Instances of this class maintain a table of handlers associated to HTTP
394
+ method (GET, HEAD, POST, PUT, or DELETE) and an API word. The given API
395
+ word may support any number of different HTTP methods by associating
396
+ them to the different, or even same, callables. Each such API may be
397
+ associated with arbitrary additional arguments, typically options to
398
+ adapt behaviour for the API as described below.
399
+
400
+ Each API handler must be associated to a validation method which will
401
+ be given the remaining URL path and query arguments. The validator
402
+ must *verify* the incoming arguments and *move* safe input to output
403
+ arguments, typically using :mod:`~.RESTValidation` utilities. If any
404
+ arguments remain in input after validation, the call is refused. This
405
+ implies the only way to get *any input at all* to the REST API is to
406
+ introduce a validator code, which must actively process every piece
407
+ of input to be used later in the actual method.
408
+
409
+ When invoked by CherryPy, the class looks up the applicable handler by
410
+ HTTP request method and the first URL path argument, runs validation,
411
+ invokes the method, and formats the output, which should be some python
412
+ data structure which can be rendered into JSON or XML, or some custom
413
+ format (e.g. raw bytes for an image). For sequences and sequence-like
414
+ results, the output is streamed out, object by object, as soon as the
415
+ handler returns; the entire response is never built up in memory except
416
+ as described below for compression and entity tag generation. As the
417
+ assumption is that method will generate some pythonic data structure,
418
+ the caller is required to specify via "Accept" header the desired
419
+ output format.
420
+
421
+ .. rubric:: Validation
422
+
423
+ There is some general validation before invoking the API method. The
424
+ HTTP method is first checked against allowed methods listed above, and
425
+ those for which there is a registered API handler. The API is checked
426
+ to be known, and that a handler has been registered for the combination
427
+ of the two, and that method can produce output in the format requested
428
+ by in the "Accept" header. The derived class can run :meth:`_precall`
429
+ validation hook before the API name is popped off URL path arguments,
430
+ in case it wants to pull other arguments such as database instance
431
+ label from the URL first.
432
+
433
+ The API-specific validation function is called with argument list
434
+ *(apiobj, method, apiname, args, safeargs).* The *apiobj* is the metadata
435
+ object for the API entry created by :meth:`_addAPI`, *method* the HTTP
436
+ method, *apiname* the API name. The remaining two arguments are incoming
437
+ and validated safe arguments, as described above for validation; both
438
+ are instances of :class:`RESTArgs`.
439
+
440
+ .. rubric:: Method call
441
+
442
+ The API handler is invoked with arguments and keyword arguments saved
443
+ into *safeargs* by the validation method. The method returns a value,
444
+ which must be either a python string, a sequence or a generator.
445
+
446
+ .. rubric:: Output formatting
447
+
448
+ Once the REST API has returned a value, the class formats the output.
449
+ For GET and HEAD requests, expire headers are set as specified by the
450
+ API object. After that the format handler is invoked, whose output is
451
+ compressed if allowed by the API and the HTTP request, and entity tag
452
+ headers are processed and generated. Although the API may return data
453
+ in any form for which there is a formatter (cf. :class:`~.JSONFormat`,
454
+ :class:`~.XMLFormat` and :class:`~.RawFormat`) that can be matched to
455
+ the request "Accept" header, the general assumption is APIs return
456
+ "rows" of "objects" which are output with "Transfer-Encoding: chunked"
457
+ in streaming format.
458
+
459
+ It is supported and recommended that APIs verify the operation will
460
+ very likely succeed, then return a python *generator* for the result,
461
+ for example a database cursor for query results. This allows large
462
+ responses to be streamed out as they are produced, without ever
463
+ buffering the entire response as a string in memory.
464
+
465
+ The output from the response formatter is sent to a compressor if the
466
+ request headers include suitable "Accept-Encoding" header and the API
467
+ object hasn't declined compression. Normally all output is compressed
468
+ with ZLIB level 9 provided the client supports ``deflate`` encoding.
469
+ It is recommended to keep compression enabled except if the output is
470
+ known to be incompressible, e.g. images or compressed data files. The
471
+ added CPU use is usually well worth the network communication savings.
472
+
473
+ .. rubric:: Entity tags and response caching
474
+
475
+ Before discussing caching, we note the following makes the implicit
476
+ assumption that GET and HEAD requests are idempotent and repeatable
477
+ withut ill effects. It is expected only PUT, POST and DELETE have
478
+ side effects.
479
+
480
+ All GET and HEAD responses will get an entity tag, or ETag, header. If
481
+ the API does not generate an ETag, a hash digest is computed over the
482
+ serialised response as it's written out, and included in the trailer
483
+ headers of the response.
484
+
485
+ If the API can compute an ETag in a better way than hashing the output,
486
+ or can reply to If-Match / If-None-Match queries without computing the
487
+ result, it should absolutely do so. Since most servers generate dynamic
488
+ content such as database query results, it's hard to generate a useful
489
+ ETag, and the scheme used here, a digest over the content, allows many
490
+ clients like browsers to cache the responses. The server still has to
491
+ re-execute the query and reformat the result to recompute the ETag, but
492
+ avoids having to send matching responses back, and more importantly, the
493
+ client learns its cached copy remains valid and can optimise accordingly.
494
+
495
+ As a usability and performance optimisation, small GET/HEAD responses up
496
+ to :attr:`etag_limit` bytes are buffered entirely, the ETag is computed,
497
+ any If-Match, If-None-Match request headers are processed, the ETag is
498
+ added to the response headers, and the entire body is output as a single
499
+ string without the regular chunked streaming. This effectively allows
500
+ web browser clients to cache small responses even if the API method is
501
+ unable to compute a reliable ETag for it. This covers virtually all
502
+ responses one would access in a browser for most servers in practice.
503
+
504
+ The scheme also reduces network traffic since the body doesn't need to be
505
+ output for successful cache validation queries, and when body is output,
506
+ TCP tends to get fed more efficiently with larger output buffers. On the
507
+ other hand the scheme degrades gracefully for large replies while allowing
508
+ smarter clients, such as those using curl library to talk to the server,
509
+ to get more out of the API, both performance and functionality wise.
510
+
511
+ For large responses the computed digest ETag is still added as a trailer
512
+ header, after all the output has been emitted. These will not be useful
513
+ for almost any client. For one, many clients ignore trailers anyway. For
514
+ another there is no way to perform If-Match / If-None-Match validation.
515
+
516
+ The default digest algorithm is :class:`~.SHA1ETag`, configurable via
517
+ :attr:`etagger`. The tagging and compression work also if the API uses
518
+ CherryPy's ``serve_file()`` to produce the response.
519
+
520
+ For GET responses to be cached by clients the response must include a
521
+ both a "Cache-Control" header and an ETag (or "Last-Modified" time). If
522
+ :attr:`default_expires` and :attr:`default_expires_opts` settings allow
523
+ it, GET responses will include a "Cache-Control: max-age=n" header.
524
+ These can be tuned per API with ``cherrypy.tools.expires(secs=n)``, or
525
+ ``expires`` and ``expires_opts`` :func:`restcall` keyword arguments.
526
+
527
+ .. rubric:: Notes
528
+
529
+ .. note:: Only GET and HEAD requests are allowed to have a query string.
530
+ Other methods (POST, PUT, DELETE) may only have parameters specified
531
+ in the request body. This is protection against XSRF attacks and in
532
+ general attempts to engineer someone to submit a malicious HTML form.
533
+
534
+ .. note:: The ETag generated by the default hash digest is computed from
535
+ the formatted stream, before compression. It is therefore independent
536
+ of "Accept-Encoding" request headers. However the ETag value is stable
537
+ only if the formatter produces stable output given the same input. In
538
+ particular any output which includes python dictionaries may vary over
539
+ calls because of changes in dictionary key iteration order.
540
+
541
+ .. warning:: The response generator returned by the API may fail after it
542
+ has started to generate output. If the response is large enough not
543
+ to fit in the ETag buffer, there is really no convenient way to stop
544
+ producing output and indicate an error to the client since the HTTP
545
+ headers have already been sent out with a good chunk of normal output.
546
+
547
+ If the API-returned generator throws, this implementation closes the
548
+ generated output by adding any required JSON, XML trailers and sets
549
+ X-REST-Status and normal error headers in the trailer headers. The
550
+ clients that understand trailer headers, such as curl-based ones, can
551
+ use the trailer to discover the output is incomplete. Clients which
552
+ ignore trailers, such as web browsers and python's urllib and httplib,
553
+ will not know the output is truncated. Hence derived classes should be
554
+ conservative about using risky constructs in the generator phase.
555
+
556
+ Specifically any method with side effects should return a generator
557
+ response only *after* all side effects have already taken place. For
558
+ example when updating a database, the client should commit first,
559
+ then return a generator. Because of how generators work in python,
560
+ the latter step must be done in a separate function from the one that
561
+ invokes commit!
562
+
563
+ None of this really matters for responses which fit into the ETag
564
+ buffer after compression. That is all but the largest responses in
565
+ most servers, and the above seems a reasonable trade-off between
566
+ not having to buffer huge responses in memory and usability.
567
+
568
+ .. rubric:: Attributes
569
+
570
+ .. attribute:: app
571
+
572
+ Reference to the :class:`~.RESTMain` object from constructor.
573
+
574
+ .. attribute:: config
575
+
576
+ Reference to the :class:`WMCore.Configuraation` section for this API
577
+ mount point as passed in to the constructor by :class:`~.RESTMain`.
578
+
579
+ .. attribute:: methods
580
+
581
+ A dictionary of registered API methods. The keys are HTTP methods,
582
+ the values are another dictionary level with API name as a key, and
583
+ an API object as a value. Do not modify this table directly, use the
584
+ :meth:`_addAPI` method instead.
585
+
586
+ .. attribute:: formats
587
+
588
+ Possible output formats matched against "Accept" headers. This is a
589
+ list of *(mime-type, formatter)* tuples. This is the general list of
590
+ formats supported by this API entry point, with the intention that
591
+ all or at least most API methods support these formats. Individual
592
+ API methods can override the format list using ``formats`` keyword
593
+ argument to :func:`restcall`, for example to specify list of image
594
+ formats for one method which retrieves image files
595
+ (cf. :class:`~.RawFormat`).
596
+
597
+ .. attribute:: etag_limit
598
+
599
+ An integer threshold for number of bytes to buffer internally for
600
+ calculation of ETag header. See the description above for the full
601
+ details on the scheme. The API object can override this limit with
602
+ the ``etag_limit`` keyword argument to :func:`restcall`. The default
603
+ is 8 MB.
604
+
605
+ .. attribute:: compression
606
+
607
+ A list of accepted compression mechanisms to be matched against the
608
+ "Accept-Encoding" HTTP request header. Currently supported values are
609
+ ``deflate`` and ``identity``. Using ``identity`` or emptying the list
610
+ disables compression. The default is ``['deflate']``. Change this only
611
+ for API mount points which are known to generate incompressible output,
612
+ using ``compression`` keyword argument to :func:`restcall`.
613
+
614
+ .. attribute:: compression_level
615
+
616
+ Integer 0-9, the default ZLIB compression level for ``deflate`` encoding.
617
+ The default is the maximum 9; for most servers the increased CPU use is
618
+ usually well worth the reduction in network transmission costs. Setting
619
+ the level to zero disables compression. The API can override this value
620
+ with ``compression_level`` keyword argument to :func:`restcall`.
621
+
622
+ .. attribute:: compression_chunk
623
+
624
+ Integer, the approximate amount of input, in bytes, to consume to form
625
+ compressed output chunks. The preferred value is the ZLIB compression
626
+ horizon, 64kB. Up to this many bytes of output from the stream formatter
627
+ are fed to the compressor, after which the compressor is flushed and the
628
+ compressed output is emitted as HTTP transmission level chunk. Hence the
629
+ receiving side can process chunks much like if the data wasn't compressed:
630
+ the only difference is each HTTP chunk will contain an integral number of
631
+ chunks instead of just one. (Of course in most cases the ETag buffering
632
+ scheme will consume the output and emit it as a single unchunked part.)
633
+ The API can override this value with ``compression_chunk`` keyword
634
+ argument to :func:`restcall`.
635
+
636
+ .. attribute:: default_expires
637
+
638
+ Number, default expire time for GET / HEAD responses in seconds. The
639
+ API object can override this value with ``expires`` keyword argument
640
+ to :func:`restcall`. The default is one hour, or 3600 seconds.
641
+
642
+ .. attribute:: default_expires_opts
643
+
644
+ A sequence of strings, additional default options for "Cache-Control"
645
+ response headers for responses with non-zero expire time limit. The
646
+ strings are joined comma-separated to the "max-age=n" item. The API
647
+ object can override this value with ``expires_opts`` keyword argument
648
+ to :func:`restcall`. The default is an empty list.
649
+
650
+ .. rubric:: Constructor arguments
651
+
652
+ :arg app: The main application :class:`~.RESTMain` object.
653
+ :arg config: The :class:`~.WMCore.ConfigSection` for this object.
654
+ :arg str mount: The CherryPy URL tree mount point for this object.
655
+ """
656
+
657
+ def __init__(self, app, config, mount):
658
+ self.app = app
659
+ self.config = config
660
+ self.etag_limit = 8 * 1024 * 1024
661
+ self.compression_level = 9
662
+ self.compression_chunk = 64 * 1024
663
+ self.compression = ['deflate', 'gzip']
664
+ self.formats = [('application/json', JSONFormat()),
665
+ ('application/xml', XMLFormat(self.app.appname))]
666
+ self.methods = {}
667
+ self.default_expires = 3600
668
+ self.default_expires_opts = []
669
+
670
+ def _addAPI(self, method, api, callable, args, validation, **kwargs):
671
+ """Add an API method.
672
+
673
+ Use this method to register handlers for a method/api combination. This
674
+ creates an internal "API object" which internally represents API target.
675
+ The API object is dictionary and will be passed to validation functions.
676
+
677
+ :arg str method: The HTTP method name: GET, HEAD, PUT, POST, or DELETE.
678
+ :arg str api: The API label, to be matched with first URL path argument.
679
+ The label may not contain slashes.
680
+ :arg callable callable: The handler; see class documentation for signature.
681
+ :arg list args: List of valid positional and keyword argument names.
682
+ These will be copied to the API object which will be passed to the
683
+ validation methods. Normally you'd get these with inspect.getfullargspec().
684
+ :arg callable validation: The validator; see class documentation for the
685
+ signature and behavioural requirements. If `args` is non-empty,
686
+ `validation` is mandatory; if `args` is empty, `callable` does not
687
+ receive any input. The validator must copy validated safe input to
688
+ actual arguments to `callable`.
689
+ :arg dict kwargs: Additional key-value pairs to set in the API object.
690
+
691
+ :returns: Nothing."""
692
+
693
+ if method not in _METHODS:
694
+ raise UnsupportedMethod()
695
+
696
+ if method not in self.methods:
697
+ self.methods[method] = {}
698
+
699
+ if api in self.methods[method]:
700
+ raise ObjectAlreadyExists()
701
+
702
+ if not isinstance(args, list):
703
+ raise TypeError("args is required to be a list")
704
+
705
+ if not isinstance(validation, list):
706
+ raise TypeError("validation is required to be a list")
707
+
708
+ if args and not validation:
709
+ raise ValueError("non-empty validation required for api taking arguments")
710
+
711
+ apiobj = {"args": args, "validation": validation, "call": callable}
712
+ apiobj.update(**kwargs)
713
+ self.methods[method][api] = apiobj
714
+
715
+ @expose
716
+ def stats(self):
717
+ """
718
+ Return CherryPy stats dict about underlying service activities
719
+ """
720
+ return cpstats.StatsPage().data()
721
+
722
+ @expose
723
+ def metrics(self):
724
+ """
725
+ Return CherryPy stats following the prometheus metrics structure
726
+ """
727
+ metrics = promMetrics(cpstats.StatsPage().data(), self.app.appname)
728
+ return encodeUnicodeToBytes(metrics)
729
+
730
+ @expose
731
+ def default(self, *args, **kwargs):
732
+ """The HTTP request handler.
733
+
734
+ This just wraps `args` and `kwargs` into a :class:`RESTArgs` and invokes
735
+ :meth:`_call` enclosed in a try/except which filters all exceptions but
736
+ :class:`HTTPRedirect` via :func:`~.report_rest_error`.
737
+
738
+ In other words the main function of this wrapper is to ensure run-time
739
+ errors are properly logged and translated to meaningful response status
740
+ and headers, including a trace identifier for this particular error. It
741
+ also sets X-REST-Time response header to the total time spent within
742
+ this request.
743
+
744
+ This method is declared "``{response.stream: True}``" to CherryPy.
745
+
746
+ :returns: See :meth:`_call`."""
747
+
748
+ try:
749
+ return self._call(RESTArgs(list(args), kwargs))
750
+ except HTTPRedirect:
751
+ raise
752
+ except cherrypy.HTTPError:
753
+ raise
754
+ except Exception as e:
755
+ report_rest_error(e, format_exc(), True)
756
+ finally:
757
+ if getattr(request, 'start_time', None):
758
+ response.headers["X-REST-Time"] = "%.3f us" % \
759
+ (1e6 * (time.time() - request.start_time))
760
+
761
+ default._cp_config = {'response.stream': True}
762
+
763
+ def _call(self, param):
764
+ """The real HTTP request handler.
765
+
766
+ :arg RESTArgs param: Input path and query arguments.
767
+
768
+ :returns: Normally a generator over the input, but in some cases a
769
+ plain string, as described in the class-level documentation. The
770
+ API handler response, often a generator itself, is usually wrapped
771
+ in couple of layers of additional generators which format and
772
+ compress the output, and handle entity tags generation and matching."""
773
+
774
+ # Make sure the request method is something we actually support.
775
+ if request.method not in self.methods:
776
+ response.headers['Allow'] = " ".join(sorted(self.methods.keys()))
777
+ raise UnsupportedMethod() from None
778
+
779
+ # If this isn't a GET/HEAD request, prevent use of query string to
780
+ # avoid cross-site request attacks and evil silent form submissions.
781
+ # We'd prefer perl cgi behaviour where query string and body args remain
782
+ # separate, but that's not how cherrypy works - it mixes everything
783
+ # into one big happy kwargs.
784
+ if (request.method != 'GET' and request.method != 'HEAD') \
785
+ and request.query_string:
786
+ response.headers['Allow'] = 'GET HEAD'
787
+ raise MethodWithoutQueryString()
788
+
789
+ # Give derived class a chance to look at arguments.
790
+ self._precall(param)
791
+
792
+ # Make sure caller identified the API to call and it is available for
793
+ # the request method.
794
+ if len(param.args) == 0:
795
+ raise APINotSpecified()
796
+ api = param.args.pop(0)
797
+ if api not in self.methods[request.method]:
798
+ methods = " ".join(sorted([m for m, d in viewitems(self.methods) if api in d]))
799
+ response.headers['Allow'] = methods
800
+ if not methods:
801
+ msg = 'Api "%s" not found. This method supports these Apis: %s' % (api, list(self.methods[request.method]))
802
+ raise APINotSupported(msg)
803
+ else:
804
+ msg = 'Api "%s" only supported in method(s): "%s"' % (api, methods)
805
+ raise APIMethodMismatch(msg)
806
+ apiobj = self.methods[request.method][api]
807
+
808
+ # Check what format the caller requested. At least one is required; HTTP
809
+ # spec says no "Accept" header means accept anything, but that is too
810
+ # error prone for a REST data interface as that establishes a default we
811
+ # cannot then change later. So require the client identifies a format.
812
+ # Browsers will accept at least */*; so can clients who don't care.
813
+ # Note that accept() will raise HTTPError(406) if no match is found.
814
+ # Available formats are either specified by REST method, or self.formats.
815
+ try:
816
+ if not request.headers.elements('Accept'):
817
+ raise NotAcceptable('Accept header required')
818
+ formats = apiobj.get('formats', self.formats)
819
+ format = cherrypy.lib.cptools.accept([f[0] for f in formats])
820
+ fmthandler = [f[1] for f in formats if f[0] == format][0]
821
+ except HTTPError:
822
+ format_names = ', '.join(f[0] for f in formats)
823
+ raise NotAcceptable('Available types: %s' % format_names)
824
+
825
+ # Validate arguments. May convert arguments too, e.g. str->int.
826
+ safe = RESTArgs([], {})
827
+ for v in apiobj['validation']:
828
+ v(apiobj, request.method, api, param, safe)
829
+ validate_no_more_input(param)
830
+
831
+ # Invoke the method.
832
+ obj = apiobj['call'](*safe.args, **safe.kwargs)
833
+
834
+ # Add Vary: Accept header.
835
+ vary_by('Accept')
836
+
837
+ # Set expires header if applicable. Note that POST/PUT/DELETE are not
838
+ # cacheable to begin with according to HTTP/1.1 specification. We must
839
+ # do this before actually streaming out the response below in case the
840
+ # ETag matching decides the previous response remains valid.
841
+ if request.method == 'GET' or request.method == 'HEAD':
842
+ expires = self.default_expires
843
+ cpcfg = getattr(apiobj['call'], '_cp_config', None)
844
+ if cpcfg and 'tools.expires.on' in cpcfg:
845
+ expires = cpcfg.get('tools.expires.secs', expires)
846
+ expires = apiobj.get('expires', expires)
847
+ if 'Cache-Control' in response.headers:
848
+ pass
849
+ elif expires < 0:
850
+ response.headers['Pragma'] = 'no-cache'
851
+ response.headers['Expires'] = 'Sun, 19 Nov 1978 05:00:00 GMT'
852
+ response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate,' \
853
+ ' post-check=0, pre-check=0'
854
+ elif expires != None:
855
+ expires_opts = apiobj.get('expires_opts', self.default_expires_opts)
856
+ expires_opts = (expires_opts and ', '.join([''] + expires_opts)) or ''
857
+ response.headers['Cache-Control'] = 'max-age=%d%s' % (expires, expires_opts)
858
+
859
+ # Format the response.
860
+ response.headers['X-REST-Status'] = 100
861
+ response.headers['Content-Type'] = format
862
+ etagger = apiobj.get('etagger', None) or SHA1ETag()
863
+ reply = stream_compress(fmthandler(obj, etagger),
864
+ apiobj.get('compression', self.compression),
865
+ apiobj.get('compression_level', self.compression_level),
866
+ apiobj.get('compression_chunk', self.compression_chunk))
867
+ return stream_maybe_etag(apiobj.get('etag_limit', self.etag_limit), etagger, reply)
868
+
869
+ def _precall(self, param):
870
+ """Point for derived classes to hook into prior to peeking at URL.
871
+
872
+ Derived classes receive the incoming path and keyword arguments in
873
+ `param`, and can peek or modify those as appropriate. In particular
874
+ if they want to pop something off the path part of the URL before
875
+ the base class takes the API name off it, this is the time to do it.
876
+
877
+ The base class implementation does nothing at all.
878
+
879
+ :arg RESTArgs param: Input path and query arguments.
880
+ :returns: Nothing."""
881
+ pass
882
+
883
+
884
+ ######################################################################
885
+ ######################################################################
886
+ class RESTApi(MiniRESTApi):
887
+ """REST service using :class:`~.RESTEntity` for API implementation.
888
+
889
+ This is base class for a REST API implemented in terms of *entities*
890
+ which support GET/PUT/POST/DELETE methods, instead of registering
891
+ individual API methods. The general principle is to perform entity
892
+ modelling, with express relationships, a little like databases are
893
+ designed, then expose those entities as REST API points. This is
894
+ usually very different from exposing just a collection of RPC APIs.
895
+
896
+ Normally each entity represents a collection of objects, and HTTP
897
+ methods translate to customary meanings over the collection:
898
+ GET = query collection, PUT = insert new, POST = update existing,
899
+ DELETE = remove item. Often the entity is written to be natively
900
+ array-oriented so that a single PUT can insert a large number of
901
+ objects, or single DELETE can remove many objects, for example.
902
+ Using optional URL path arguments the entity can support operations
903
+ on sub-items as well as collection-wide ones.
904
+
905
+ Entities should be registered with :meth:`_add`. Where an entity API
906
+ cannot reasonably represent the needs, raw RPC-like methods can still
907
+ be added using :meth:`_addAPI`."""
908
+
909
+ def _addEntities(self, entities, entry, wrapper=None):
910
+ """Private interface for adding APIs for entities.
911
+
912
+ :arg dict entities: See :meth:`_add` documentation.
913
+ :arg callable entry: The pre-entry callback hook. See :meth:`_enter`.
914
+ :arg callable wrapper: Optional wrapper to modify the method handler.
915
+ :returns: Nothing."""
916
+ for label, entity in viewitems(entities):
917
+ for method in _METHODS:
918
+ handler = getattr(entity, method.lower(), None)
919
+ if not handler and method == 'HEAD':
920
+ handler = getattr(entity, 'get', None)
921
+ if handler and getattr(handler, 'rest.exposed', False):
922
+ rest_args = getattr(handler, 'rest.args')
923
+ rest_params = getattr(handler, 'rest.params')
924
+ if wrapper: handler = wrapper(handler)
925
+ self._addAPI(method, label, handler, rest_args,
926
+ [entity.validate, entry],
927
+ entity=entity, **rest_params)
928
+
929
+ def _add(self, entities):
930
+ """Add entities.
931
+
932
+ Adds a collection of entities and their labels. Each entity should be
933
+ derived from :class:`~.RESTEntity` and can define a method per HTTP
934
+ method it supports: :meth:`get` to support GET, :meth:`post` for POST,
935
+ and so on. In addition it must provide a :meth:`validate` method
936
+ compatible with the :class:`~.MiniRESTApi` validator convention; it
937
+ will be called for all HTTP methods. If the entity defines :meth:`get`
938
+ but no :meth:`head` method, :meth:`get` is automatically registered as
939
+ the HEAD request handler as well.
940
+
941
+ The signature of the HTTP method handlers depends on arguments copied
942
+ to `safe` by the validation function. If :meth:`validate` copies one
943
+ safe argument `foo` to `safe`, then :meth:`get` will be invoked with
944
+ exactly one argument, `foo`. There is no point in specifying default
945
+ values for HTTP method handlers as any defaults must be processed in
946
+ :meth:`validate`. In any case it's better to keep all argument defaults
947
+ in one place, in the :meth:`validate` method.
948
+
949
+ :arg dict entities: A dictionary of labels with :class:`~.RESTEntity`
950
+ value. The label is the API name and should be a bare word, with
951
+ no slashes. It's recommended to use dash (-) as a word separator.
952
+ The value is the entity to add, as described above.
953
+ :returns: Nothing."""
954
+ self._addEntities(entities, self._enter)
955
+
956
+ def _enter(self, apiobj, method, api, param, safe):
957
+ """Callback to be invoked after validation completes but before
958
+ the actual API method is invoked.
959
+
960
+ This sets the ``rest_generate_data`` and ``rest_generate_preamble``
961
+ attributes on ``cherrypy.request``, for use later in output formatting.
962
+
963
+ If the :func:`restcall` for the API method was called with ``generate``
964
+ property, it's set as ``request.rest_generate_data`` for use in output
965
+ formatting to label the name of the result object from this call. The
966
+ default value is ``"result"``.
967
+
968
+ The ``request.rest_generate_preamble`` is used to add a description to
969
+ the response preamble. By default there is no description, but if the
970
+ :func:`restcall` call for the API method used ``columns`` property, its
971
+ value is set as ``columns`` key to the preamble. This would be used if
972
+ the output is inherently row oriented, and the preamble gives the column
973
+ titles and subsequent output the column values. This is the recommended
974
+ REST API output format, especially for large output, as it is easy to
975
+ read in clients but much more compact than emitting column labels for
976
+ every row, e.g. by using dictionaries for each row.
977
+
978
+ The API may modify ``request.rest_generate_preamble`` if it wishes to
979
+ insert something to the description part.
980
+
981
+ :arg dict apiobj: API object from :class:`~.MiniRESTApi`.
982
+ :arg str method: HTTP method.
983
+ :arg str api: Name of the API being called.
984
+ :arg RESTArgs param: Incoming arguments.
985
+ :arg RESTArgs safe: Output safe arguments.
986
+ :returns: Nothing."""
987
+ request.rest_generate_data = apiobj.get("generate", None)
988
+ request.rest_generate_preamble = {}
989
+ cols = apiobj.get("columns", None)
990
+ if cols:
991
+ request.rest_generate_preamble["columns"] = cols
992
+
993
+
994
+ ######################################################################
995
+ ######################################################################
996
+ class DBConnectionPool(Thread):
997
+ """Asynchronous and robust database connection pool.
998
+
999
+ .. rubric:: The pool
1000
+
1001
+ This class provides a database connection pool that is thread safe
1002
+ and recovers as gracefully as possible from server side and network
1003
+ outages. Thread safety means that multiple threads may check out and
1004
+ return connections from or to the pool conncurrently. All connection
1005
+ management operations are internally transferred to a separate thread
1006
+ in order to guarantee client web server threads never block even if
1007
+ the database layer blocks or hangs in connection-related calls.
1008
+
1009
+ In other words the effective purpose of this class is to guarantee
1010
+ web servers autonomously and gracefully enter a degraded state when
1011
+ the underlying database or network goes out, responding with "503
1012
+ database unavailable" but continuing to serve requests otherwise.
1013
+ Once database returns to operation the servers normally recover on
1014
+ their own without having to be manually restarted. While the main
1015
+ intent is to avoid requiring intervention on a production service,
1016
+ as a side effect this class makes database-based web services more
1017
+ usable on mobile devices with unstable network connections.
1018
+
1019
+ The primary pooling purpose is to cache connections for databases
1020
+ for which connections are expensive, in particular Oracle. Instead
1021
+ of creating a new connection for each use, much of the time the pool
1022
+ returns a compatible idle connection which the application was done
1023
+ using. Connections unused for more than a pool-configured timeout are
1024
+ automatically closed.
1025
+
1026
+ Between uses the connections are reset by rolling back any pending
1027
+ operations, and tested for validity by probing the server for
1028
+ "liveness" response. Connections failing tests are automatically
1029
+ disposed; when returning conections to the pool clients may indicate
1030
+ they've had trouble with it, and the pool will reclaim the connection
1031
+ instead of queuing it for reuse. In general it is hard to tell which
1032
+ errors involved connection-related issues, so it is safer to flag all
1033
+ errors on returning the connection. It is however better to filter
1034
+ out frequent benign errors such as integrity violations to avoid
1035
+ excessive connectin churn.
1036
+
1037
+ Other database connection pool implementations exist, including for
1038
+ instance a session pool in the Oracle API (cx_Oracle's SessionPool).
1039
+ The reason this class implements pooling and not just validation is
1040
+ to side-step blocking or thread unsafe behaviour in the others. All
1041
+ connection management operations are passed to a worker thread. If
1042
+ the database connection layer stalls, HTTP server threads requesting
1043
+ connections will back off gracefully, reporting to their own clients
1044
+ that the database is currently unavailable. On the other hand no
1045
+ thread is able to create a connection to database if one connection
1046
+ stalls, but usually that would happen anyway with at least Oracle.
1047
+
1048
+ The hand-over to the worker thread of course adds overhead, but the
1049
+ increased robustness and gracefulness in face of problems in practice
1050
+ outweighs the cost by far, and is in any case cheaper than creating
1051
+ a new connection each time. The level of overhead can be tuned by
1052
+ adjusting condition variable contention (cf. `num_signals`).
1053
+
1054
+ The connections returned to clients are neither garbage collected
1055
+ nor is there a ceiling on a maximum number of connections returned.
1056
+ The client needs to be careful to `put()` as many connections as it
1057
+ received from `get()` to avoid leaking connections.
1058
+
1059
+ .. rubric:: Pool specifications
1060
+
1061
+ The database specification handed to the constructor should be a
1062
+ dictionary with the members:
1063
+
1064
+ ``type``
1065
+ Reference to the DB API module, aka connection type.
1066
+
1067
+ ``schema``
1068
+ String, name of the database schema the connection references.
1069
+ Sets connection ``current_schema`` attribute.
1070
+
1071
+ ``clientid``
1072
+ String, identifies the client to session monitor, normally this
1073
+ should be `service-label@fully.qualified.domain`. Sets connection
1074
+ ``client_identifier`` attribute.
1075
+
1076
+ ``liveness``
1077
+ String, SQL to execute to verify the connection remain usable,
1078
+ normally "``select sysdate from dual``" or alike. The statement
1079
+ should require a fresh response from the server on each execution
1080
+ so avoid "``select 1 from dual``" style cacheable queries.
1081
+
1082
+ ``user``
1083
+ String, login user name.
1084
+
1085
+ ``password``
1086
+ String, login password for ``user``.
1087
+
1088
+ ``dsn``
1089
+ String, login data source name, usually the TNS entry name.
1090
+
1091
+ ``timeout``
1092
+ Integer or float, number of seconds to retain unused idle
1093
+ connections before closing them. Note that this only applies to
1094
+ connections handed back to `put()`; connections given by `get()`
1095
+ but never returned to the pool are not considered idle, not even
1096
+ if the client loses the handle reference.
1097
+
1098
+ ``stmtcache``
1099
+ Optional integer, overrides the default statement cache size 50.
1100
+
1101
+ ``trace``
1102
+ Optional boolean flag, if set enables tracing of database activity
1103
+ for this pool, including connection ops, SQL executed, commits and
1104
+ rollbacks, and pool activity on the handles. If True, connections
1105
+ are assigned random unique labels which are used in activity log
1106
+ messages. Recycled idle connections also get a new label, but the
1107
+ connection log message will include the old label which allows
1108
+ previous logs on that connection to be found. Not to be confused
1109
+ with database server side logging; see ``session-sql`` for that.
1110
+
1111
+ ``auth-role``
1112
+ Optional, (NAME, PASSWORD) string sequence. If set, connections
1113
+ acquire the database role *NAME* before use by executing the SQL
1114
+ "``set role none``", "``set role NAME identified by PASSWORD``"
1115
+ on each `get()` of the connection. In other words, if the role is
1116
+ removed or the password is changed, the client will automatically
1117
+ shed the role and fail with an error, closing the connection in
1118
+ the process, despite connection caching.
1119
+
1120
+ ``session-sql``
1121
+ Optional sequence of SQL statement strings. These are executed on
1122
+ each connection `get()`. Use with session trace statements such as
1123
+ "``alter session set sql_trace = true``",
1124
+ "``alter session set events '10046 trace name context forever,
1125
+ level 12'``". It's not recommended to make any database changes.
1126
+
1127
+ .. rubric:: Connection handles
1128
+
1129
+ The `get()` method returns a database connection handle, a dict with
1130
+ the following members. The exact same dict needs to be returned to
1131
+ `put()` -- not a copy of it.
1132
+
1133
+ ``type``
1134
+ Reference to DB API module.
1135
+
1136
+ ``pool``
1137
+ Reference to this pool object.
1138
+
1139
+ ``connection``
1140
+ Reference to the actual DB API connection object.
1141
+
1142
+ ``trace``
1143
+ Always present, but may be either boolean False, or a non-empty
1144
+ string with the trace message prefix to use for all operations
1145
+ concerning this connection.
1146
+
1147
+ .. rubric:: Attributes
1148
+
1149
+ .. attribute:: connection_wait_time
1150
+
1151
+ Number, the maximum time in seconds to wait for a connection to
1152
+ complete after which the client will be told the database server
1153
+ is unavailable. This should be large enough to avoid triggering
1154
+ unnecessary database unavailability errors in sporadic delays in
1155
+ production use, but low enough to bounce clients off when the
1156
+ database server is having difficulty making progress.
1157
+
1158
+ In particular client HTTP threads will be tied up this long if
1159
+ the DB server is completely hanging: completing TCP connections
1160
+ but not the full database handshake, or if TCP connection itself
1161
+ is experiencing significant delays. Hence it's important to keep
1162
+ this value low enough for the web server not to get dogged down
1163
+ or fail with time-out errors itself.
1164
+
1165
+ .. attribute:: wakeup_period
1166
+
1167
+ Number, maximum time in seconds to wait in the worker thread main
1168
+ loop before checking any timed out connections. The server does
1169
+ automatically adjust the wake-up time depending on work needed,
1170
+ so there usually isn't any need to change this value. The value
1171
+ should not be decreased very low to avoid an idle server from
1172
+ waking up too often.
1173
+
1174
+ .. attribute:: num_signals
1175
+
1176
+ Number of condition variables to use for signalling connection
1177
+ completion. The pool creates this many condition variables and
1178
+ randomly picks one to signal connection completion between the
1179
+ worker and calling threads. Increase this if there is a high
1180
+ degree of thread contention on concurrent threads waiting for
1181
+ connection completion. The default should be fine for all but
1182
+ highest connection reuse rates.
1183
+
1184
+ .. attribute:: max_tries
1185
+
1186
+ The maximum number of times to attempt creating a connection
1187
+ before giving up. If a connection fails tests, it is discarded
1188
+ and another attempt is made with another connection, either an
1189
+ idle one or an entirely new connection if no idle ones remain.
1190
+ This variable sets the limit on how many times to try before
1191
+ giving up. This should be high enough a value to consume any
1192
+ cached bad connections rapidly enough after network or database
1193
+ failure. Hence the pool will reclaim any bad connections at the
1194
+ maximum rate of `get()` calls times `max_tries` per
1195
+ `connection_wait_time`.
1196
+
1197
+ Do not set this value to a very high value if there is a real
1198
+ possibility of major operational flukes leading to connection
1199
+ storms or account lock-downs, such as using partially incorrect
1200
+ credentials or applications with invalid/non-debugged SQL which
1201
+ cause connection to be black-listed and recycled. In other words,
1202
+ only change this parameter for applications which have undergone
1203
+ significant testing in a production environment, with clear data
1204
+ evidence the default value is not leading to sufficiently fast
1205
+ recovery after connections have started to go sour.
1206
+
1207
+ .. attribute:: dbspec
1208
+
1209
+ Private, the database specification given to the constructor.
1210
+
1211
+ .. attribute:: id
1212
+
1213
+ Private, the id given to the constructor for trace logging.
1214
+
1215
+ .. attribute:: sigready
1216
+
1217
+ Private, `num_signals` long list of condition variables for
1218
+ signalling connection attempt result.
1219
+
1220
+ .. attribute:: sigqueue
1221
+
1222
+ Private, condition variable for signalling changes to `queue`.
1223
+
1224
+ .. attribute:: queue
1225
+
1226
+ Private, list of pending requests to the worker thread, access
1227
+ to which is protected by `sigqueue`. Connection release requests
1228
+ go to the front of the list, connection create requests at the
1229
+ end. The worker thread takes the first request in queue, then
1230
+ executes the action with `sigqueue` released so new requests can
1231
+ be added while the worker is talking to the database.
1232
+
1233
+ .. attribute:: inuse
1234
+
1235
+ Private, list of connections actually handed out by `get()`. Note
1236
+ that if the client has already given up on the `get()` request by
1237
+ the time the connection is finally established, the connection is
1238
+ automatically discarded and not put no this list. This list may be
1239
+ accessed only in the worker thread as no locks are available to
1240
+ protect the access; `logstatus()` method provides the means to log
1241
+ the queue state safely in the worker thread.
1242
+
1243
+ .. attribute:: idle
1244
+
1245
+ Private, list of idle connections, each of which has ``expires``
1246
+ element to specify the absolute time when it will expire. The
1247
+ worker thread schedules to wake up within five seconds after the
1248
+ next earliest expire time, or in `wakeup_period` otherwise, and
1249
+ of course whenever new requests are added to `queue`. This list
1250
+ may be accessed only in the work thread as no locks are available
1251
+ to protect the access; `logstatus()` method provides the means to
1252
+ log the queue state safely in the worker thread.
1253
+
1254
+ .. rubric:: Constructor
1255
+
1256
+ The constructor automatically attaches this object to the cherrypy
1257
+ engine start/stop messages so the background worker thread starts or
1258
+ quits, respectively. The pool does not attempt to connect to the
1259
+ database on construction, only on the first call to `get()`, so it's
1260
+ safe to create the pool even if network or database are unavailable.
1261
+
1262
+ :arg dict dbspec: Connection specification as described above.
1263
+ :arg str id: Identifier used to label trace connection messages for
1264
+ this pool, such as the full class name of the owner."""
1265
+
1266
+ connection_wait_time = 8
1267
+ wakeup_period = 60
1268
+ num_signals = 4
1269
+ max_tries = 5
1270
+
1271
+ def __init__(self, id, dbspec):
1272
+ Thread.__init__(self, name=self.__class__.__name__)
1273
+ self.sigready = [Condition() for _ in range(0, self.num_signals)]
1274
+ self.sigqueue = Condition()
1275
+ self.queue = []
1276
+ self.idle = []
1277
+ self.inuse = []
1278
+ if type in dbspec and dbspec['type'].__name__ == 'MySQLdb':
1279
+ dbspec['dsn'] = dbspec['db']
1280
+ self.dbspec = dbspec
1281
+ self.id = id
1282
+ engine.subscribe("start", self.start, 100)
1283
+ engine.subscribe("stop", self.stop, 100)
1284
+
1285
+ def logstatus(self):
1286
+ """Pass a request to the worker thread to log the queue status.
1287
+
1288
+ It's recommended that the owner hook this method to a signal such
1289
+ as SIGUSR2 so it's possible to get the process dump its database
1290
+ connections status, especially the number of `inuse` connections,
1291
+ from outside the process.
1292
+
1293
+ The request is inserted in the front of current list of pending
1294
+ requests, but do note the request isn't executed directly. If the
1295
+ worker thread is currently blocked in a database or network call,
1296
+ log output is only generated when the worker resumes control.
1297
+
1298
+ :returns: Nothing."""
1299
+ self.sigqueue.acquire()
1300
+ self.queue.insert(0, (self._status, None))
1301
+ self.sigqueue.notifyAll()
1302
+ self.sigqueue.release()
1303
+
1304
+ def stop(self):
1305
+ """Tell the pool to stop processing requests and to exit from the
1306
+ worker thread.
1307
+
1308
+ The request is inserted in the front of current list of pending
1309
+ requests. The worker thread will react to it as soon as it's done
1310
+ processing any currently ongoing database or network call. If the
1311
+ database API layer is completely wedged, that might be never, in
1312
+ which case the application should arrange for other means to end,
1313
+ either by using a suicide alarm -- for example by calling
1314
+ signal.alarm() but not setting SIGALRM handler -- or externally
1315
+ by arranging SIGKILL to be delivered.
1316
+
1317
+ Since this request is inserted in the front of pending requests,
1318
+ existing connections, whether idle or in use, will not be closed
1319
+ or even rolled back. It's assumed the database server will clean
1320
+ up the connections once the process exits.
1321
+
1322
+ The constructor automatically hooks the cherrypy engine 'stop'
1323
+ message to call this method.
1324
+
1325
+ :returns: Nothing."""
1326
+ self.sigqueue.acquire()
1327
+ self.queue.insert(0, (None, None))
1328
+ self.sigqueue.notifyAll()
1329
+ self.sigqueue.release()
1330
+
1331
+ def get(self, id, module):
1332
+ """Get a new connection from the pool, identified to server side and
1333
+ the session monitor as to be used for action `id` by `module`.
1334
+
1335
+ This retrieves the next available idle connection from the pool, or
1336
+ creates a new connection if none are available. Before handing back
1337
+ the connection, it's been tested to be actually live and usable.
1338
+ If the database connection specification included a role attribute
1339
+ or session statements, they will have been respectively set and
1340
+ executed.
1341
+
1342
+ The connection request is appended to the current queue of requests.
1343
+ If the worker thread does not respond in `connection_wait_time`, the
1344
+ method gives up and indicates the database is not available. When
1345
+ that happens, the worker thread will still attempt to complete the
1346
+ connection, but will then discard it.
1347
+
1348
+ :arg str id: Identification string for this connection request.
1349
+ This will set the ``clientinfo`` and ``action``
1350
+ attributes on the connection for database session
1351
+ monitoring tools to visualise and possibly remote
1352
+ debugging of connection use.
1353
+
1354
+ :arg str module: Module using this connection, typically the fully
1355
+ qualified python class name. This will set the
1356
+ ``module`` attribute on the connection object for
1357
+ display in database session monitoring tools.
1358
+
1359
+ :returns: A `(HANDLE, ERROR)` tuple. If a connection was successfully
1360
+ made, `HANDLE` will contain a dict with connection data as
1361
+ described in the class documentation and `ERROR` is `None`.
1362
+ If no connection was made at all, returns `(None, None)`.
1363
+ Returns `(None, (ERROBJ, TRACEBACK))` if there was an error
1364
+ making the connection that wasn't resolved in `max_tries`
1365
+ attempts; `ERROBJ` is the last exception thrown, `TRACEBACK`
1366
+ the stack trace returned by `format_exc()` for it."""
1367
+
1368
+ sigready = random.choice(self.sigready)
1369
+ arg = {"error": None, "handle": None, "signal": sigready,
1370
+ "abandoned": False, "id": id, "module": module}
1371
+
1372
+ self.sigqueue.acquire()
1373
+ self.queue.append((self._connect, arg))
1374
+ self.sigqueue.notifyAll()
1375
+ self.sigqueue.release()
1376
+
1377
+ sigready.acquire()
1378
+ now = time.time()
1379
+ until = now + self.connection_wait_time
1380
+ while True:
1381
+ dbh = arg["handle"]
1382
+ err = arg["error"]
1383
+ if dbh or err or now >= until:
1384
+ arg["abandoned"] = True
1385
+ break
1386
+ sigready.wait(until - now)
1387
+ now = time.time()
1388
+ sigready.release()
1389
+ return dbh, err
1390
+
1391
+ def put(self, dbh, bad=False):
1392
+ """Add a database handle `dbh` back to the pool.
1393
+
1394
+ Normally `bad` would be False and the connection is added back to
1395
+ the pool as an idle connection, and will be reused for a subsequent
1396
+ connection.
1397
+
1398
+ Any pending operations on connections are automatically cancalled
1399
+ and rolled back before queuing them into the idle pool. These will
1400
+ be executed asynchronously in the database connection worker thread,
1401
+ not in the caller's thread. However note that if the connection
1402
+ became unusable, attempting to roll it back may block the worker.
1403
+ That is normally fine as attempts to create new connections will
1404
+ start to fail with timeout, leading to "database unavailable" errors.
1405
+
1406
+ If the client has had problems with the connection, it should most
1407
+ likely set `bad` to True, so the connection will be closed and
1408
+ discarded. It's safe to reuse connections after benign errors such
1409
+ as basic integrity violations. However there are a very large class
1410
+ of obscure errors which actually mean the connection handle has
1411
+ become unusable, so it's generally safer to flag the handle invalid
1412
+ on error -- with the caveat that errors should be rare to avoid
1413
+ excessive connection churn.
1414
+
1415
+ :arg dict dbh: A database handle previously returned by `get()`. It
1416
+ must be the exact same dict object, not a copy.
1417
+ :arg bool bad: If True, `dbh` is likely bad, so please try close it
1418
+ instead of queuing it for reuse.
1419
+ :returns: Nothing."""
1420
+
1421
+ self.sigqueue.acquire()
1422
+ self.queue.insert(0, ((bad and self._disconnect) or self._release, dbh))
1423
+ self.sigqueue.notifyAll()
1424
+ self.sigqueue.release()
1425
+
1426
+ def run(self):
1427
+ """Run the connection management thread."""
1428
+
1429
+ # Run forever, pulling work from "queue". Round wake-ups scheduled
1430
+ # from timeouts to five-second quantum to maximise the amount of
1431
+ # work done per round of clean-up and reducing wake-ups.
1432
+ next = self.wakeup_period
1433
+ while True:
1434
+ # Whatever reason we woke up, even if sporadically, process any
1435
+ # pending requests first.
1436
+ self.sigqueue.acquire()
1437
+ self.sigqueue.wait(max(next, 5))
1438
+ while self.queue:
1439
+ # Take next action and execute it. "None" means quit. Release
1440
+ # the queue lock while executing actions so callers can add
1441
+ # new requests, e.g. release connections while we work here.
1442
+ # The actions are not allowed to throw any exceptions.
1443
+ action, arg = self.queue.pop(0)
1444
+ self.sigqueue.release()
1445
+ if action:
1446
+ action(arg)
1447
+ else:
1448
+ return
1449
+ self.sigqueue.acquire()
1450
+ self.sigqueue.release()
1451
+
1452
+ # Check idle connections for timeout expiration. Calculate the
1453
+ # next wake-up as the earliest expire time, but note that it
1454
+ # gets rounded to minimum five seconds above to scheduling a
1455
+ # separate wake-up for every handle. Note that we may modify
1456
+ # 'idle' while traversing it, so need to clone it first.
1457
+ now = time.time()
1458
+ next = self.wakeup_period
1459
+ for old in self.idle[:]:
1460
+ if old["expires"] <= now:
1461
+ self.idle.remove(old)
1462
+ self._disconnect(old)
1463
+ else:
1464
+ next = min(next, old["expires"] - now)
1465
+
1466
+ def _status(self, *args):
1467
+ """Action handler to dump the queue status."""
1468
+ cherrypy.log("DATABASE CONNECTIONS: %s@%s %s: timeout=%d inuse=%d idle=%d"
1469
+ % (self.dbspec["user"], self.dbspec["dsn"], self.id,
1470
+ self.dbspec["timeout"], len(self.inuse), len(self.idle)))
1471
+
1472
+ def _error(self, title, rest, err, where):
1473
+ """Internal helper to generate error message somewhat similar to
1474
+ :func:`~.report_rest_error`.
1475
+
1476
+ :arg str title: All-capitals error message title part.
1477
+ :arg str rest: Possibly non-empty trailing error message part.
1478
+ :arg Exception err: Exception object reference.
1479
+ :arg str where: Traceback for `err` as returned by :ref:`format_exc()`."""
1480
+ errid = "%032x" % random.randrange(1 << 128)
1481
+ cherrypy.log("DATABASE THREAD %s ERROR %s@%s %s %s.%s %s%s (%s)"
1482
+ % (title, self.dbspec["user"], self.dbspec["dsn"], self.id,
1483
+ getattr(err, "__module__", "__builtins__"),
1484
+ err.__class__.__name__, errid, rest, str(err).rstrip()))
1485
+ for line in where.rstrip().split("\n"):
1486
+ cherrypy.log(" " + line)
1487
+
1488
+ def _connect(self, req):
1489
+ """Action handler to fulfill a connection request."""
1490
+ s = self.dbspec
1491
+ dbh, err = None, None
1492
+
1493
+ # If tracing, issue log line that identifies this connection series.
1494
+ trace = s["trace"] and ("RESTSQL:" + "".join(random.sample(letters, 12)))
1495
+ trace and cherrypy.log("%s ENTER %s@%s %s (%s) inuse=%d idle=%d" %
1496
+ (trace, s["user"], s["dsn"], self.id, req["id"],
1497
+ len(self.inuse), len(self.idle)))
1498
+
1499
+ # Attempt to connect max_tries times.
1500
+ for _ in range(0, self.max_tries):
1501
+ try:
1502
+ # Take next idle connection, or make a new one if none exist.
1503
+ # Then test and prepare that connection, linking it in trace
1504
+ # output to any previous uses of the same object.
1505
+ dbh = (self.idle and self.idle.pop()) or self._new(s, trace)
1506
+ assert dbh["pool"] == self
1507
+ assert dbh["connection"]
1508
+ prevtrace = dbh["trace"]
1509
+ dbh["trace"] = trace
1510
+ self._test(s, prevtrace, trace, req, dbh)
1511
+
1512
+ # The connection is ok. Kill expire limit and return this one.
1513
+ if "expires" in dbh:
1514
+ del dbh["expires"]
1515
+ break
1516
+ except Exception as e:
1517
+ # The connection didn't work, report and remember this exception.
1518
+ # Note that for every exception reported for the server itself
1519
+ # we may report up to max_tries exceptions for it first. That's
1520
+ # a little verbose, but it's more useful to have all the errors.
1521
+ err = (e, format_exc())
1522
+ self._error("CONNECT", "", *err)
1523
+ dbh and self._disconnect(dbh)
1524
+ dbh = None
1525
+
1526
+ # Return the result, and see if the caller abandoned this attempt.
1527
+ req["signal"].acquire()
1528
+ req["error"] = err
1529
+ req["handle"] = dbh
1530
+ abandoned = req["abandoned"]
1531
+ req["signal"].notifyAll()
1532
+ req["signal"].release()
1533
+
1534
+ # If the caller is known to get our response, record the connection
1535
+ # into 'inuse' list. Otherwise discard any connection we made.
1536
+ if not abandoned and dbh:
1537
+ self.inuse.append(dbh)
1538
+ elif abandoned and dbh:
1539
+ cherrypy.log("DATABASE THREAD CONNECTION ABANDONED %s@%s %s"
1540
+ % (self.dbspec["user"], self.dbspec["dsn"], self.id))
1541
+ self._disconnect(dbh)
1542
+
1543
+ def _new(self, s, trace):
1544
+ """Helper function to create a new connection with `trace` identifier."""
1545
+ trace and cherrypy.log("%s instantiating a new connection" % trace)
1546
+ ret = {"pool": self, "trace": trace, "type": s["type"]}
1547
+ if s['type'].__name__ == 'MySQLdb':
1548
+ ret.update({"connection": s["type"].connect(s['host'], s["user"], s["password"], s["db"], int(s["port"]))})
1549
+ else:
1550
+ ret.update({"connection": s["type"].connect(s["user"], s["password"], s["dsn"], threaded=True)})
1551
+
1552
+ return ret
1553
+
1554
+ def _test(self, s, prevtrace, trace, req, dbh):
1555
+ """Helper function to prepare and test an existing connection object."""
1556
+ # Set statement cache. Default is 50 statments but spec can override.
1557
+ c = dbh["connection"]
1558
+ c.stmtcachesize = s.get("stmtcache", 50)
1559
+
1560
+ # Emit log message to identify this connection object. If it was
1561
+ # previously used for something else, log that too for detailed
1562
+ # debugging involving problems with connection reuse.
1563
+ if s['type'].__name__ == 'MySQLdb':
1564
+ client_version = s["type"].get_client_info()
1565
+ version = ".".join(str(x) for x in s["type"].version_info)
1566
+ else:
1567
+ client_version = ".".join(str(x) for x in s["type"].clientversion())
1568
+ version = c.version
1569
+ prevtrace = ((prevtrace and prevtrace != trace and
1570
+ " (previously %s)" % prevtrace.split(":")[1]) or "")
1571
+ trace and cherrypy.log("%s%s connected, client: %s, server: %s, stmtcache: %d"
1572
+ % (trace, prevtrace, client_version,
1573
+ version, c.stmtcachesize))
1574
+
1575
+ # Set the target schema and identification attributes on this one.
1576
+ c.current_schema = s["schema"]
1577
+ c.client_identifier = s["clientid"]
1578
+ c.clientinfo = req["id"]
1579
+ c.module = req["module"]
1580
+ c.action = req["id"][:32]
1581
+
1582
+ # Ping the server. This will detect some but not all dead connections.
1583
+ trace and cherrypy.log("%s ping" % trace)
1584
+ c.ping()
1585
+
1586
+ # At least server responded, now try executing harmless SQL but one
1587
+ # that requires server to actually respond. This detects remaining
1588
+ # bad connections.
1589
+ trace and cherrypy.log("%s check [%s]" % (trace, s["liveness"]))
1590
+ c.cursor().execute(s["liveness"])
1591
+
1592
+ # If the pool requests authentication role, set it now. First reset
1593
+ # any roles we've acquired before, then attempt to re-acquire the
1594
+ # role. Hence if the role is deleted or its password is changed by
1595
+ # application admins, we'll shed any existing privileges and close
1596
+ # the connection right here. This ensures connection pooling cannot
1597
+ # be used to extend role privileges forever.
1598
+ if "auth-role" in s:
1599
+ trace and cherrypy.log("%s set role none")
1600
+ c.cursor().execute("set role none")
1601
+ trace and cherrypy.log("%s set role %s" % (trace, s["auth-role"][0]))
1602
+ c.cursor().execute("set role %s identified by %s" % s["auth-role"])
1603
+
1604
+ # Now execute session statements, e.g. tracing event requests.
1605
+ if "session-sql" in s:
1606
+ for sql in s["session-sql"]:
1607
+ trace and cherrypy.log("%s session-sql [%s]" % (trace, sql))
1608
+ c.cursor().execute(sql)
1609
+
1610
+ # OK, connection's all good.
1611
+ trace and cherrypy.log("%s connection established" % trace)
1612
+
1613
+ def _release(self, dbh):
1614
+ """Action handler to release a connection back to the pool."""
1615
+ try:
1616
+ # Check the handle didn't get corrupted.
1617
+ assert dbh["pool"] == self
1618
+ assert dbh["connection"]
1619
+ assert dbh in self.inuse
1620
+ assert dbh not in self.idle
1621
+ assert "expires" not in dbh
1622
+
1623
+ # Remove from 'inuse' list first in case the rest throws/hangs.
1624
+ s = self.dbspec
1625
+ trace = dbh["trace"]
1626
+ self.inuse.remove(dbh)
1627
+
1628
+ # Roll back any started transactions. Note that we don't want to
1629
+ # call cancel() on the connection here as it will most likely just
1630
+ # degenerate into useless "ORA-25408: can not safely replay call".
1631
+ trace and cherrypy.log("%s release with rollback" % trace)
1632
+ dbh["connection"].rollback()
1633
+
1634
+ # Record expire time and put to end of 'idle' list; _connect()
1635
+ # takes idle connections from the back of the list, so we tend
1636
+ # to reuse most recently used connections first, and to prune
1637
+ # the number of connections in use to the minimum.
1638
+ dbh["expires"] = time.time() + s["timeout"]
1639
+ self.idle.append(dbh)
1640
+ trace and cherrypy.log("%s RELEASED %s@%s timeout=%d inuse=%d idle=%d"
1641
+ % (trace, s["user"], s["dsn"], s["timeout"],
1642
+ len(self.inuse), len(self.idle)))
1643
+ except Exception as e:
1644
+ # Something went wrong, nuke the connection from orbit.
1645
+ self._error("RELEASE", " failed to release connection", e, format_exc())
1646
+
1647
+ try:
1648
+ self.inuse.remove(dbh)
1649
+ except ValueError:
1650
+ pass
1651
+
1652
+ try:
1653
+ self.idle.remove(dbh)
1654
+ except ValueError:
1655
+ pass
1656
+
1657
+ self._disconnect(dbh)
1658
+
1659
+ def _disconnect(self, dbh):
1660
+ """Action handler to discard the connection entirely."""
1661
+ try:
1662
+ # Assert internal consistency invariants; the handle may be
1663
+ # marked for use in case it's discarded with put(..., True).
1664
+ assert dbh not in self.idle
1665
+
1666
+ try:
1667
+ self.inuse.remove(dbh)
1668
+ except ValueError:
1669
+ pass
1670
+
1671
+ # Close the connection.
1672
+ s = self.dbspec
1673
+ trace = dbh["trace"]
1674
+ trace and cherrypy.log("%s disconnecting" % trace)
1675
+ dbh["connection"].close()
1676
+
1677
+ # Remove references to connection object as much as possible.
1678
+ del dbh["connection"]
1679
+ dbh["connection"] = None
1680
+
1681
+ # Note trace that this is now gone.
1682
+ trace and cherrypy.log("%s DISCONNECTED %s@%s timeout=%d inuse=%d idle=%d"
1683
+ % (trace, s["user"], s["dsn"], s["timeout"],
1684
+ len(self.inuse), len(self.idle)))
1685
+ except Exception as e:
1686
+ self._error("DISCONNECT", " (ignored)", e, format_exc())
1687
+
1688
+
1689
+ ######################################################################
1690
+ ######################################################################
1691
+ class DatabaseRESTApi(RESTApi):
1692
+ """A :class:`~.RESTApi` whose entities represent database contents.
1693
+
1694
+ This class wraps API calls into an environment which automatically sets
1695
+ up and tears down a database connection around the call, and translates
1696
+ all common database errors such as unique constraint violations into an
1697
+ appropriate and meaningful :class:`~.RESTError`-derived error.
1698
+
1699
+ This class is fundamentally instance-aware. The :meth:`_precall` hook is
1700
+ used to pop a required instance argument off the URL, before the API name,
1701
+ so URLs look like ``/app/prod/entity``. The instance name from the URL is
1702
+ used to select the database instance on which the API operates, and the
1703
+ HTTP method is used to select suitable connection pool, for example reader
1704
+ account for GET operations and a writer account for PUT/POST/DELETE ones.
1705
+
1706
+ Normally the entities do not use database connection directly, but use
1707
+ the convenience functions in this class. These methods perform many
1708
+ utility tasks needed for high quality implementation, plus make it very
1709
+ convenient to write readable REST entity implementations which conform
1710
+ to the preferred API conventions. For example the :meth:`query` method
1711
+ not only executes a query, but it runs a SQL statement cleaner, does all
1712
+ the SQL execution trace logging, keeps track of last statement executed
1713
+ with its binds (to be logged with error output in case an exception is
1714
+ raised later on), fills ``rest_generate_preamble["columns"]`` from the
1715
+ SQL columns titles, and returns a generator over the result, optionally
1716
+ filtering the results by a regular expression match criteria.
1717
+
1718
+ The API configuration must include ``db`` value, whose value should be
1719
+ the qualified name of the python object for the multi-instance database
1720
+ authentication specification. The object so imported should yield a
1721
+ dictionary whose keys are the supported instance names. The values should
1722
+ be dictionaries whose keys are HTTP methods and values are dictionaries
1723
+ specifying database contact details for that use; "*" can be used as a
1724
+ key for "all other methods". For example the following would define two
1725
+ instances, "prod" and "dev", with "prod" using a reader account for GETs
1726
+ and a writer account for other methods, and "dev" with a single account::
1727
+
1728
+ DB = { "prod": { "GET": { "user": "foo_reader", ... },
1729
+ "*": { "user": "foo_writer", ... } },
1730
+ "dev": { "*": { "user": "foo", ... } } }
1731
+
1732
+ The class automatically uses a thread-safe, resilient connection pool
1733
+ for the database connections (cf. :class:`~.DBConnectionPool`). New
1734
+ database connections are created as needed on demand. If the database is
1735
+ unavailable, the server will gracefully degrade to reporting HTTP "503
1736
+ Service Unavailable" status code. Database availability is not required
1737
+ on server start-up, connections are first attempted on the first HTTP
1738
+ request which requires a connection. In fact connections are prepared
1739
+ only *after* request validation has completed, mainly as a security
1740
+ measure to prevent unsafe parameter validation code from mixing up with
1741
+ database query execution. At the end of API method execution connection
1742
+ handles are automatically rolled back, so API should explicitly commit
1743
+ if it wants any changes made to last. Sending the server SIGUSR2 signal
1744
+ will log connection usage statistics and pool timeouts.
1745
+
1746
+ .. rubric:: Attributes
1747
+
1748
+ .. attribute:: _db
1749
+
1750
+ The database configuration imported from ``self.config.db``. Each
1751
+ connection specification has dictionary key ``"pool"`` added,
1752
+ pointing to the :class:`~.DBConnectionPool` managing that pool.
1753
+
1754
+ .. rubric:: Constructor
1755
+
1756
+ Constructor arguments are the same as for the base class.
1757
+ """
1758
+ _ALL_POOLS = []
1759
+
1760
+ def __init__(self, app, config, mount):
1761
+ RESTApi.__init__(self, app, config, mount)
1762
+ signal.signal(signal.SIGUSR2, self._logconnections)
1763
+ modname, item = config.db.rsplit(".", 1)
1764
+ module = __import__(modname, globals(), locals(), [item])
1765
+ self._db = getattr(module, item)
1766
+ myid = "%s.%s" % (self.__class__.__module__, self.__class__.__name__)
1767
+ for spec in viewvalues(self._db):
1768
+ for db in viewvalues(spec):
1769
+ if isinstance(db, dict):
1770
+ db["pool"] = DBConnectionPool(myid, db)
1771
+ DatabaseRESTApi._ALL_POOLS.append(db["pool"])
1772
+
1773
+ @staticmethod
1774
+ def _logconnections(*args):
1775
+ """SIGUSR2 signal handler to log status of all pools."""
1776
+ list([p.logstatus() for p in DatabaseRESTApi._ALL_POOLS])
1777
+
1778
+ def _add(self, entities):
1779
+ """Add entities.
1780
+
1781
+ See base class documentation for the semantics. This wraps the API
1782
+ method with :meth:`_wrap` to handle database related errors, and
1783
+ inserts :meth:`_dbenter` as the last validator in chain to wrap
1784
+ the method in database connection management. :meth:`_dbenter`
1785
+ will install :meth:`_dbexit` as a request clean-up callback to
1786
+ handle connection disposal if database connection succeeds."""
1787
+ self._addEntities(entities, self._dbenter, self._wrap)
1788
+
1789
+ def _wrap(self, handler):
1790
+ """Internal helper function to wrap calls to `handler` inside a
1791
+ database exception filter. Any exceptions raised will be passed to
1792
+ :meth:`_dberror`."""
1793
+
1794
+ @wraps(handler)
1795
+ def dbapi_wrapper(*xargs, **xkwargs):
1796
+ try:
1797
+ return handler(*xargs, **xkwargs)
1798
+ except Exception as e:
1799
+ self._dberror(e, format_exc(), False)
1800
+
1801
+ return dbapi_wrapper
1802
+
1803
+ def _dberror(self, errobj, trace, inconnect):
1804
+ """Internal helper routine to process exceptions while inside
1805
+ database code.
1806
+
1807
+ This method does necessary housekeeping to roll back and dispose any
1808
+ connection already made, remember enough of the context for error
1809
+ reporting, and convert the exception into a relevant REST error type
1810
+ (cf. :class:`~.RESTError`). For example constraint violations turn
1811
+ into :class:`~.ObjectAlreadyExists` or :class:`~.MissingObject`.
1812
+
1813
+ Errors not understood become :class:`~.DatabaseExecutionError`, with
1814
+ the SQL and bind values of last executed statement reported into the
1815
+ server logs (but not to the client). Note that common programming errors,
1816
+ :class:`~.KeyError`, :class:`~.ValueError` or :class:`~.NameError`
1817
+ raised in API implementation, become :class:`~.DatabaseExecutionError`
1818
+ too. This will make little difference to the client, and will still
1819
+ include all the relevant useful information in the server logs, but
1820
+ of course they are not database-related as such.
1821
+
1822
+ :arg Exception errobj: The exception object.
1823
+ :arg str trace: Associated traceback as returned by :func:`format_exc`.
1824
+ :arg bool inconnect: Whether exception occurred while trying to connect.
1825
+ :return: Doesn't, raises an exception."""
1826
+
1827
+ # Grab last sql executed and whatever binds were used so far,
1828
+ # the database type object, and null out the rest so that
1829
+ # post-request and any nested error handling will ignore it.
1830
+ db = request.db
1831
+ type = db["type"]
1832
+ instance = db["instance"]
1833
+ sql = (db["last_sql"],) + db["last_bind"]
1834
+
1835
+ # If this wasn't connection failure, just release the connection.
1836
+ # If that fails, force drop the connection. We ignore errors from
1837
+ # this since we are attempting to report another earlier error.
1838
+ db["handle"] and db["pool"].put(db["handle"], True)
1839
+
1840
+ # Set the db backend
1841
+ DB_BACKEND = db['type'].__name__
1842
+
1843
+ del request.db
1844
+ del db
1845
+
1846
+ # Raise an error of appropriate type.
1847
+ errinfo = {"errobj": errobj, "trace": trace}
1848
+ dberrinfo = {"errobj": errobj, "trace": trace,
1849
+ "lastsql": sql, "instance": instance}
1850
+ if inconnect:
1851
+ raise DatabaseUnavailable(**dberrinfo)
1852
+ elif isinstance(errobj, type.IntegrityError):
1853
+ errorcode = errobj[0] if DB_BACKEND == 'MySQLdb' else errobj.args[0].code
1854
+ if errorcode in {'cx_Oracle': (1, 2292), 'MySQLdb': (1062,)}[DB_BACKEND]:
1855
+ # ORA-00001: unique constraint (x) violated
1856
+ # ORA-02292: integrity constraint (x) violated - child record found
1857
+ # MySQL: 1062, Duplicate entry 'x' for key 'y'
1858
+ # MySQL: Both unique and integrity constraint falls into 1062 error
1859
+ raise ObjectAlreadyExists(**errinfo)
1860
+ elif errorcode in {'cx_Oracle': (1400, 2290), 'MySQLdb': (1048,)}[DB_BACKEND]:
1861
+ # ORA-01400: cannot insert null into (x)
1862
+ # ORA-02290: check constraint (x) violated
1863
+ # MySQL: 1048, Column (x) cannot be null
1864
+ # There are no check constraint in MySQL. Oracle 2290 equivalent does not exist
1865
+ raise InvalidParameter(**errinfo)
1866
+ elif errorcode == {'cx_Oracle': 2291, 'MySQLdb': 1452}[DB_BACKEND]:
1867
+ # ORA-02291: integrity constraint (x) violated - parent key not found
1868
+ # MySQL: 1452, Cannot add or update a child row: a foreign key constraint fails
1869
+ raise MissingObject(**errinfo)
1870
+ else:
1871
+ raise DatabaseExecutionError(**dberrinfo)
1872
+ elif isinstance(errobj, type.OperationalError):
1873
+ raise DatabaseUnavailable(**dberrinfo)
1874
+ elif isinstance(errobj, type.InterfaceError):
1875
+ raise DatabaseConnectionError(**dberrinfo)
1876
+ elif isinstance(errobj, (HTTPRedirect, RESTError)):
1877
+ raise
1878
+ else:
1879
+ raise DatabaseExecutionError(**dberrinfo)
1880
+
1881
+ def _precall(self, param):
1882
+ """Pop instance from the URL path arguments before :meth:`_call` takes
1883
+ entity name from it.
1884
+
1885
+ This orders URL to have entities inside an instance, which is logical.
1886
+ Checks the instance name is present and one of the known databases as
1887
+ registered to the constructor into :attr:`_db`, and matches the HTTP
1888
+ method to the right database connection pool.
1889
+
1890
+ If the instance argument is missing, unsafe, or unknown, raises an
1891
+ :class:`~.NoSuchInstance` exception. If there is no connection pool
1892
+ for that combination of HTTP method and instance, raises an
1893
+ :class:`~.DatabaseUnavailable` exception.
1894
+
1895
+ If successful, remembers the database choice in ``cherrypy.request.db``
1896
+ but does not yet attempt connection to it. This is a dictionary with
1897
+ the following fields.
1898
+
1899
+ ``instance``
1900
+ String, the instance name.
1901
+
1902
+ ``type``
1903
+ Reference to the DB API module providing database connection.
1904
+
1905
+ ``pool``
1906
+ Reference to the :class:`~.DBConnectionPool` providing connections.
1907
+
1908
+ ``handle``
1909
+ Reference to the database connection handle from the pool. Initially
1910
+ set to `None`.
1911
+
1912
+ ``last_sql``
1913
+ String, the last SQL statement executed on this connection. Initially
1914
+ set to `None`, filled in by the statement execution utility function
1915
+ :meth:`prepare` (which :meth:`execute` and :meth:`executemany` call).
1916
+ Used for error reporting in :meth:`_dberror`.
1917
+
1918
+ ``last_bind``
1919
+ A tuple *(values, keywords)* of the last bind values used on this
1920
+ connection. Initially set to *(None, None)* and reset back to that
1921
+ value by each call to :meth:`prepare`, filled in by the statement
1922
+ execution utility functions :meth:`executemany` and :meth:`execute`.
1923
+ Use for error reporting in :meth:`_dberror`. Note that if the data
1924
+ is security sensitive, or extremely voluminous with thousands of
1925
+ values, this might cause some concern.
1926
+
1927
+ :arg RESTArgs param: Incoming URL path and query arguments. If a valid
1928
+ instance name is found, pops the first path item off ``param.args``.
1929
+ :returns: Nothing."""
1930
+
1931
+ # Check we were given an instance argument and it's known.
1932
+ if not param.args or param.args[0] not in self._db:
1933
+ raise NoSuchInstance()
1934
+ if not re.match(r"^[a-z]+$", param.args[0]):
1935
+ raise NoSuchInstance("Invalid instance name")
1936
+
1937
+ instance = param.args.pop(0)
1938
+
1939
+ # Get database object.
1940
+ if request.method in self._db[instance]:
1941
+ db = self._db[instance][request.method]
1942
+ elif request.method == 'HEAD' and 'GET' in self._db[instance]:
1943
+ db = self._db[instance]['GET']
1944
+ elif '*' in self._db[instance]:
1945
+ db = self._db[instance]['*']
1946
+ else:
1947
+ raise DatabaseUnavailable()
1948
+
1949
+ # Remember database instance choice, but don't do anything about it yet.
1950
+ request.db = {"instance": instance, "type": db["type"], "pool": db["pool"],
1951
+ "handle": None, "last_sql": None, "last_bind": (None, None)}
1952
+
1953
+ def _dbenter(self, apiobj, method, api, param, safe):
1954
+ """Acquire database connection just before invoking the entity.
1955
+
1956
+ :meth:`_add` arranges this method to be invoked as the last validator
1957
+ function, just before the base class calls the actual API method. We
1958
+ acquire the database connection here, and arrange the connection to
1959
+ be released by :meth:`_dbexit` after the response has been generated.
1960
+
1961
+ It is not possible to release the database connection right after the
1962
+ response handler returns, as it is normally a generator which is still
1963
+ holding on to a database cursor for streaming out the response. Here we
1964
+ register connection release to happen in the CherryPy request clean-up
1965
+ phase ``on_end_request``. Clean-up is ensured even if errors occurred.
1966
+
1967
+ Database connections will be identified by the HTTP method, instance
1968
+ name and entity name, and with the fully qualified class name of the
1969
+ entity handling the request. This allows server activity to be more
1970
+ usefully monitored in the database session monitor tools.
1971
+
1972
+ Raises a :class:`~.DatabaseUnavailable` exception if connecting to the
1973
+ database fails, either the connection times out or generates errors.
1974
+ This exception is translated into "503 Service Unavailable" HTTP status.
1975
+ Note that :class:`~.DBConnectionPool` verifies the connections are
1976
+ actually usable before handing them back, so connectivity problems are
1977
+ normally all caught here, with the server degrading into unavailability.
1978
+
1979
+ A successfully made connection is recorded in ``request.db["handle"]``
1980
+ and ``request.rest_generate_data`` is initialised from the ``generate``
1981
+ :func:`restcall` parameter, as described in :meth:`.RESTApi._enter`.
1982
+ ``request.rest_generate_preamble`` is reset to empty; it will normally
1983
+ be filled in by :meth:`execute` and :meth:`executemany` calls.
1984
+
1985
+ :args: As documented for validation functions.
1986
+ :returns: Nothing."""
1987
+
1988
+ assert getattr(request, "db", None), "Expected DB args from _precall"
1989
+ assert isinstance(request.db, dict), "Expected DB args from _precall"
1990
+
1991
+ # Get a pool connection to the instance.
1992
+ request.rest_generate_data = None
1993
+ request.rest_generate_preamble = {}
1994
+ module = "%s.%s" % (apiobj['entity'].__class__.__module__,
1995
+ apiobj['entity'].__class__.__name__)
1996
+ id = "%s %s %s" % (method, request.db["instance"], api)
1997
+ dbh, err = request.db["pool"].get(id, module)
1998
+
1999
+ if err:
2000
+ self._dberror(err[0], err[1], True)
2001
+ elif not dbh:
2002
+ del request.db
2003
+ raise DatabaseUnavailable()
2004
+ else:
2005
+ request.db["handle"] = dbh
2006
+ request.rest_generate_data = apiobj.get("generate", None)
2007
+ request.hooks.attach('on_end_request', self._dbexit, failsafe=True)
2008
+
2009
+ def _dbexit(self):
2010
+ """CherryPy request clean-up handler to dispose the database connection.
2011
+
2012
+ Releases the connection back to the :class:`~.DBConnectionPool`. As
2013
+ described in :meth:`_dbenter`, call to this function is arranged at
2014
+ the end of the request processing inside CherryPy.
2015
+
2016
+ :returns: Nothing."""
2017
+
2018
+ if getattr(request, "db", None) and request.db["handle"]:
2019
+ request.db["pool"].put(request.db["handle"], False)
2020
+
2021
+ def sqlformat(self, schema, sql):
2022
+ """Database utility function which reformats the SQL statement by
2023
+ removing SQL ``--`` comments and leading and trailing white space
2024
+ on lines, and converting multi-line statements to a single line.
2025
+
2026
+ The main reason this function is used is that often SQL statements
2027
+ are written in code as multi-line quote blocks, and it's awkward
2028
+ to include them in database tracing and error logging output. It
2029
+ is much more readable to have single line statements with canonical
2030
+ spacing, so we put all executed statements into that form before use.
2031
+
2032
+ If `schema` is provided, the statement is munged to insert its value
2033
+ as a schema prefix to all tables, sequences, indexes and keys that
2034
+ are not already prefixed by a schema name. This is not normally used,
2035
+ the preferred mechanism is to set ``current_schema`` attribute on the
2036
+ connections as per :class:`~.DBConnectionPool` documentation. For the
2037
+ munging to work, it is assumed all table names start with ``t_``, all
2038
+ sequence names start with ``seq_``, all index names start with ``ix_``,
2039
+ all primary key names start with ``pk_`` and all foreign key names
2040
+ start with ``fk_``.
2041
+
2042
+ This method does not remove ``/* ... */`` comments to avoid removing
2043
+ any deliberate query hints.
2044
+
2045
+ :arg str sql: SQL statement to clean up.
2046
+ :arg str schema: If not `None`, schema prefix to insert.
2047
+ :returns: Cleaned up SQL statement string."""
2048
+
2049
+ sql = re.sub(r"--.*", "", sql)
2050
+ sql = re.sub(r"^\s+", "", sql)
2051
+ sql = re.sub(r"\s+$", "", sql)
2052
+ sql = re.sub(r"\n\s+", " ", sql)
2053
+ if schema:
2054
+ sql = re.sub(r"(?<=\s)((t|seq|ix|pk|fk)_[A-Za-z0-9_]+)(?!\.)",
2055
+ r"%s.\1" % schema, sql)
2056
+ sql = re.sub(r"(?<=\s)((from|join)\s+)([A-Za-z0-9_]+)(?=$|\s)",
2057
+ r"\1%s.\3" % schema, sql)
2058
+ return sql
2059
+
2060
+ def prepare(self, sql):
2061
+ """Prepare a SQL statement.
2062
+
2063
+ This utility cleans up SQL statement `sql` with :meth:`sqlformat`,
2064
+ obtains a new cursor from the current database connection, and
2065
+ calls the ``prepare()`` on it, then returns the cursor.
2066
+
2067
+ The cleaned up statement is remembered as ``request.db["last_sql"]``
2068
+ and if database tracing is enabled, logged for the current connection.
2069
+ In log output, sensitive contents like passwords are censored out
2070
+ (cf. :obj:`~._RX_CENSOR`). The binds in ``request.db["last_bind"]``
2071
+ are reset to (None, None).
2072
+
2073
+ This method can be called only between :meth:`_dbenter` and
2074
+ :meth:`_dbexit`.
2075
+
2076
+ :arg str sql: SQL statement.
2077
+ :returns: Cursor on which statement was prepared."""
2078
+
2079
+ assert request.db["handle"], "DB connection missing"
2080
+ sql = self.sqlformat(None, sql) # FIXME: schema prefix?
2081
+ trace = request.db["handle"]["trace"]
2082
+ logsql = re.sub(_RX_CENSOR, r"\1 <censored>", sql)
2083
+ request.db["last_bind"] = None, None
2084
+ request.db["last_sql"] = logsql
2085
+ trace and cherrypy.log("%s prepare [%s]" % (trace, logsql))
2086
+ c = request.db["handle"]["connection"].cursor()
2087
+ if request.db['type'].__name__ == 'MySQLdb':
2088
+ return c
2089
+ c.prepare(sql)
2090
+ return c
2091
+
2092
+ def execute(self, sql, *binds, **kwbinds):
2093
+ """Execute a SQL statement with bind variables.
2094
+
2095
+ This method mirrors the DB API :func:`execute` method on cursors. It
2096
+ executes the `sql` statement after invoking :meth:`prepare` on it,
2097
+ saving the bind variables into ``request.db["last_bind"]`` and
2098
+ logging the binds if tracing is enabled. It returns both the cursor
2099
+ and the return value from the underlying DB API call as a tuple.
2100
+
2101
+ The convention for passing binds is the same as for the corresponding
2102
+ DB API :func:`execute` method on cursors. You may find :meth:`bindmap`
2103
+ useful to convert dict-of-lists keyword arguments of a REST entity to
2104
+ the commonly used list-of-dicts to `binds` argument for this method.
2105
+
2106
+ This method can be called only between :meth:`_dbenter` and
2107
+ :meth:`_dbexit`.
2108
+
2109
+ :arg str sql: SQL statement string.
2110
+ :arg list binds: Positional binds.
2111
+ :arg dict kwbinds: Keyword binds.
2112
+ :returns: Tuple *(cursor, variables)*, where *cursor* is the cursor
2113
+ returned by :meth:`prepare` for the SQL statement, and *variables*
2114
+ is the list of variables if `sql` is a query, or None, as returned
2115
+ by the corresponding DB API :func:`execute` method."""
2116
+
2117
+ c = self.prepare(sql)
2118
+ trace = request.db["handle"]["trace"]
2119
+ request.db["last_bind"] = (binds, kwbinds)
2120
+ trace and cherrypy.log("%s execute: %s %s" % (trace, binds, kwbinds))
2121
+ if request.db['type'].__name__ == 'MySQLdb':
2122
+ return c, c.execute(sql, kwbinds)
2123
+ return c, c.execute(None, *binds, **kwbinds)
2124
+
2125
+ def executemany(self, sql, *binds, **kwbinds):
2126
+ """Execute a SQL statement many times with bind variables.
2127
+
2128
+ This method mirrors the DB DBI :func:`executemany` method on cursors.
2129
+ It executes the statement over a sequence of bind values; otherwise
2130
+ it is the same as :meth:`execute`.
2131
+
2132
+ :args: See :meth:`execute`.
2133
+ :returns: See :meth:`execute`."""
2134
+
2135
+ c = self.prepare(sql)
2136
+ trace = request.db["handle"]["trace"]
2137
+ request.db["last_bind"] = (binds, kwbinds)
2138
+ trace and cherrypy.log("%s executemany: %s %s" % (trace, binds, kwbinds))
2139
+ if request.db['type'].__name__ == 'MySQLdb':
2140
+ return c, c.executemany(sql, binds[0])
2141
+ return c, c.executemany(None, *binds, **kwbinds)
2142
+
2143
+ def query(self, match, select, sql, *binds, **kwbinds):
2144
+ """Convenience function to :meth:`execute` a query, set ``"columns"`` in
2145
+ the REST response preamble to the column titles of the query result, and
2146
+ return a generator over the cursor as a result, possibly filtered.
2147
+
2148
+ The SQL statement is expected to be a SELECT query statement. The `sql`,
2149
+ `binds` and `kwbinds` are passed to :meth:`execute`. The returned cursor's
2150
+ ``description``, the column titles of the returned rows, are used to set
2151
+ ``request.rest_generate_preamble["columns"]`` description preamble. The
2152
+ assumption is those column titles are compatible with the argument names
2153
+ accepted by REST methods such that "columns" returned here are compatible
2154
+ with the argument names accepted by the GET/PUT/POST/DELETE methods.
2155
+
2156
+ If `match` is not `None`, it is assumed to be a regular expression object
2157
+ to be passed to :func:`rxfilter` to filter results by column value, and
2158
+ `select` should be a callable which returns the value to filter on given
2159
+ the row values, typically an :func:`operator.itemgetter`. If `match` is
2160
+ `None`, this function returns the trivial generator :func:`rows`.
2161
+
2162
+ The convention for passing binds is the same as for the corresponding
2163
+ DB API :func:`execute` method on cursors. You may find :meth:`bindmap`
2164
+ useful to convert dict-of-lists keyword arguments of a REST entity to
2165
+ the commonly used list-of-dicts to `binds` argument for this method.
2166
+
2167
+ This method can be called only between :meth:`_dbenter` and
2168
+ :meth:`_dbexit`.
2169
+
2170
+ :arg re.RegexObject match: Either `None` or a regular expression for
2171
+ filtering the results by a colum value.
2172
+ :arg callable select: If `match` is not `None`, a callable which
2173
+ retrieves the column value to run `match` against. It must return
2174
+ a string value. It would normally be :func:`operator.itemgetter`,
2175
+ may need additional code if type conversion is needed, or if the
2176
+ caller wants to join values of multiple columns for filtering.
2177
+ :arg str sql: SQL statement string.
2178
+ :arg list binds: Positional binds.
2179
+ :arg dict kwbinds: Keyword binds.
2180
+ :returns: A generator over the query results."""
2181
+
2182
+ c, _ = self.execute(sql, *binds, **kwbinds)
2183
+ request.rest_generate_preamble["columns"] = \
2184
+ [x[0].lower() for x in c.description]
2185
+ if match:
2186
+ return rxfilter(match, select, c)
2187
+ else:
2188
+ return rows(c)
2189
+
2190
+ def modify(self, sql, *binds, **kwbinds):
2191
+ """Convenience function to :meth:`executemany` a query, and return a
2192
+ a result with the number of affected rows, after verifying it matches
2193
+ exactly the number of inputs given and committing the transaction.
2194
+
2195
+ The SQL statement is expected to be a modifying query statement: INSERT,
2196
+ UPDATE, DELETE, MERGE, and so on. If `binds` is non-empty, it is passed
2197
+ as-is to :meth:`executemany` with `kwbinds`, and the expected number of
2198
+ rows to be affected is set to *len(binds[0]).* Otherwise it's assumed
2199
+ arguments are given as lists by keyword, and they are transposed to a
2200
+ list of dictionaries with :meth:`bindmap`; for REST methods this is
2201
+ expected to be the most common calling convention since REST API calls
2202
+ themselves will receive arguments as lists by keyword argument. The
2203
+ expected number of affected rows is set to the length of the list
2204
+ returned by :meth:`bindmap`; note that :meth:`bindmap` requires all
2205
+ arguments to have the same number of values.
2206
+
2207
+ Since :meth:`executemany` is used, the operation is intrinsically array
2208
+ oriented. This property is useful for making REST methods natively
2209
+ collection oriented, allowing operations to act efficiently on large
2210
+ quantities of input data with very little special coding.
2211
+
2212
+ After executing the statement but before committing, this method runs
2213
+ :meth:`rowstatus` to check that the number of rows affected matches
2214
+ exactly the number of inputs. :meth:`rowstatus` throws an exception
2215
+ if that is not the case, and otherwise returns a trivial result object
2216
+ of the form ``[{ "modified": rowcount }]`` meant to be returned to REST
2217
+ API callers.
2218
+
2219
+ Once :meth:`rowstatus` is happy with the result, the transaction is
2220
+ committed, with appropriate debug output if trace logging is enabled.
2221
+
2222
+ :arg str sql: SQL modify statement.
2223
+ :arg list binds: Bind variables by position: list of dictionaries.
2224
+ :arg dict kwbinds: Bind variables by keyword: dictionary of lists.
2225
+ :result: See :meth:`rowstatus` and description above."""
2226
+
2227
+ if binds:
2228
+ c, _ = self.executemany(sql, *binds, **kwbinds)
2229
+ expected = len(binds[0])
2230
+ else:
2231
+ kwbinds = self.bindmap(**kwbinds)
2232
+ c, _ = self.executemany(sql, kwbinds, *binds)
2233
+ expected = len(kwbinds)
2234
+ result = self.rowstatus(c, expected)
2235
+ trace = request.db["handle"]["trace"]
2236
+ trace and cherrypy.log("%s commit" % trace)
2237
+ request.db["handle"]["connection"].commit()
2238
+ return result
2239
+
2240
+ def rowstatus(self, c, expected):
2241
+ """Verify the last statement executed on cursor `c` touched exactly
2242
+ `expected` number of rows.
2243
+
2244
+ The last SQL statement executed on `c` is assumed to have been a
2245
+ modifying such as INSERT, DELETE, UPDATE or MERGE. If the number of
2246
+ rows affected by the statement is less than `expected`, raises a
2247
+ :class:`~.MissingObject` exception. If the number is greater than
2248
+ `expected`, raises a :class:`~.TooManyObjects` exception. If the
2249
+ number is exactly right, returns a generator over a trivial result
2250
+ object of the form ``[{ "modified": rowcount }]`` which is meant to
2251
+ be returned as the result from the underlying REST method.
2252
+
2253
+ Normally you would use this method via :meth:`modify`, but it can be
2254
+ called directly to verify each step of multi-statement update operation.
2255
+
2256
+ This method exists so that a REST entity can be written easily to have
2257
+ a documented invariant, which is likely to always pass, and needs to
2258
+ be checked with very low overhead -- aka each modify operation touches
2259
+ the exact number of database objects requested, and attempts to act on
2260
+ non-existent objects are caught and properly reported to clients. It
2261
+ enables trivially one to use ``INSERT INTO ... SELECT ...`` statement
2262
+ style for insertions, deletions and updates. If the SELECT part returns
2263
+ fewer or more results than expected, an exception will be raised and the
2264
+ transaction rolled back even if the schema constraints do not catch the
2265
+ problem.
2266
+
2267
+ In other words the check here makes sure that any app logic or database
2268
+ schema constraint mistakes turn into hard API errors rather than making
2269
+ hash out of the database contents, or lying to clients that an operation
2270
+ succeeded when it did not actually fully perform the requested task. It
2271
+ eliminates the need to proliferate result checking code through the app
2272
+ and to worry about a large class of possible race conditions in clustered
2273
+ web services talking to databases.
2274
+
2275
+ This method can be called on any valid cursor object. It's in no way
2276
+ dependent on the use or non-use of the other database utility methods.
2277
+
2278
+ :arg c: DB API cursor object.
2279
+ :arg int expected: Number of rows expected to be affected.
2280
+ :returns: See description above."""
2281
+
2282
+ if c.rowcount < expected:
2283
+ raise MissingObject(info="%d vs. %d expected" % (c.rowcount, expected))
2284
+ elif c.rowcount > expected:
2285
+ raise TooManyObjects(info="%d vs. %d expected" % (c.rowcount, expected))
2286
+ return rows([{"modified": c.rowcount}])
2287
+
2288
+ def bindmap(self, **kwargs):
2289
+ """Given `kwargs` of equal length list keyword arguments, returns the
2290
+ data transposed as list of dictionaries each of which has a value for
2291
+ every key from each of the lists.
2292
+
2293
+ This method is convenient for arranging HTTP request keyword array
2294
+ parameters for bind arrays suitable for `executemany` call.
2295
+
2296
+ For example the call ``api.bindmap(a = [1, 2], b = [3, 4])`` returns a
2297
+ list of dictionaries ``[{ "a": 1, "b": 3 }, { "a": 2, "b": 4 }]``."""
2298
+
2299
+ keys = list(kwargs)
2300
+ return [dict(list(zip(keys, vals))) for vals in zip(*listvalues(kwargs))]
2301
+
2302
+
2303
+ ######################################################################
2304
+ ######################################################################
2305
+ class RESTEntity(object):
2306
+ """Base class for entities in :class:`~.RESTApi`-based interfaces.
2307
+
2308
+ This class doesn't offer any service other than holding on to the
2309
+ arguments given in the constructor. The derived class must implement at
2310
+ least a :meth:`validate` method, and some number of HTTP request method
2311
+ functions as described in :class:`~.RESTApi` documentation.
2312
+
2313
+ Do note that keyword arguments with defaults are pointless on HTTP method
2314
+ handlers: the methods are never called in a fashion that would actually
2315
+ use the declaration defaults. Specifically, every argument stored into
2316
+ ``safe.kwargs`` by a validator will always be given *some* value. If the
2317
+ parameter was declared optional to the validator, it will automatically
2318
+ be given `None` value when not present in the request arguments.
2319
+
2320
+ .. rubric:: Attributes
2321
+
2322
+ .. attribute:: app
2323
+
2324
+ Reference to the :class:`~.RESTMain` application.
2325
+
2326
+ .. attribute:: api
2327
+
2328
+ Reference to the :class:`~.RESTApi` owner of this entity.
2329
+
2330
+ .. attribute:: config
2331
+
2332
+ Reference to the :class:`WMCore.ConfigSection` for the
2333
+ :class:`~.RESTApi` owner.
2334
+
2335
+ .. attribute:: mount
2336
+
2337
+ The URL mount point of the :class:`~.RESTApi` owner. Does not
2338
+ include the name of this entity.
2339
+ """
2340
+
2341
+ def __init__(self, app, api, config, mount):
2342
+ self.app = app
2343
+ self.api = api
2344
+ self.config = config
2345
+ self.mount = mount
2346
+
2347
+
2348
+ ######################################################################
2349
+ ######################################################################
2350
+ def restcall(func=None, args=None, generate="result", **kwargs):
2351
+ """Mark a method for use in REST API calls.
2352
+
2353
+ This is a decorator to mark a callable, such as :class:`~.RESTEntity`
2354
+ method, an exposed REST interface. It must be applied on functions and
2355
+ methods given to :meth:`.MiniRESTApi._addAPI` and :meth:`.RESTApi._add`.
2356
+ It can be used either as bare-word or function-like decorator: both
2357
+ ``@restcall def f()`` and ``@restcall(foo=bar) def f()`` forms work.
2358
+
2359
+ The `args` should be the parameter names accepted by the decorated
2360
+ function. If `args` is the default None, the possible arguments are
2361
+ extracted with :func:`inspect.getfullargspec`.
2362
+
2363
+ The `generate` argument sets the label used by output formatters to
2364
+ surround the output. The default is "result", yielding for example
2365
+ JSON output ``{"result": [ ... ]}`` and XML output ``<app><result>
2366
+ ... </result></app>``
2367
+
2368
+ The `kwargs` can supply any configuration variables which are known to
2369
+ other parts of the REST API implementation. The arguments eventually
2370
+ become fields in the "API object" created by :meth:`.MiniRESTApi._addAPI`,
2371
+ and accessible among other things to the validator functions. The most
2372
+ commonly used arguments include those listed below. See the rest of
2373
+ the module documentation for the details on them.
2374
+
2375
+ =================== ======================================================
2376
+ Keyword Purpose
2377
+ =================== ======================================================
2378
+ generate Name of the response wrapper as described above.
2379
+ columns List of column names for row-oriented data.
2380
+ expires Response expire time in seconds.
2381
+ expires_opts Additional "Cache-Control" options.
2382
+ formats "Accept" formats and associated formatter objects.
2383
+ etag_limit Amount of output to buffer for ETag calculation.
2384
+ compression "Accept-Encoding" methods, empty disables compression.
2385
+ compression_level ZLIB compression level for output (0 .. 9).
2386
+ compression_chunk Approximate amount of output to compress at once.
2387
+ =================== ======================================================
2388
+
2389
+ :returns: The original function suitably enriched with attributes if
2390
+ invoked as a function-like decorator, or a function which will apply
2391
+ the decoration if invoked as bare-word style decorator.
2392
+ """
2393
+
2394
+ def apply_restcall_opts(func, args=args, generate=generate, kwargs=kwargs):
2395
+ if not func:
2396
+ raise ValueError("'restcall' must be applied to a function")
2397
+ if args == None:
2398
+ args = [a for a in inspect.getfullargspec(func).args if a != 'self']
2399
+ if args == None or not isinstance(args, list):
2400
+ raise ValueError("'args' must be defined")
2401
+ kwargs.update(generate=generate)
2402
+ setattr(func, 'rest.exposed', True)
2403
+ setattr(func, 'rest.args', args or [])
2404
+ setattr(func, 'rest.params', kwargs)
2405
+ return func
2406
+
2407
+ return (func and apply_restcall_opts(func)) or apply_restcall_opts
2408
+
2409
+
2410
+ def rows(cursor):
2411
+ """Utility function to convert a sequence `cursor` to a generator."""
2412
+ for row in cursor:
2413
+ yield row
2414
+
2415
+
2416
+ def rxfilter(rx, select, cursor):
2417
+ """Utility function to convert a sequence `cursor` to a generator, but
2418
+ applying a filtering predicate to select which rows to return.
2419
+
2420
+ The assumption is that `cursor` yields uniform sequence objects ("rows"),
2421
+ and the `select` operator can be invoked with ``select(row)`` for each
2422
+ retrieved row to return a string. If the value returned matches the
2423
+ regular expression `rx`, the original row is included in the generator
2424
+ output, otherwise it's skipped.
2425
+
2426
+ :arg re.RegexObject rx: Regular expression to match against, or at least
2427
+ any object which supports ``rx.match(value)`` on the value returned by
2428
+ the ``select(row)`` operator.
2429
+ :arg callable select: An operator which returns the item to filter on,
2430
+ given a row of values, typically an :func:`operator.itemgetter`.
2431
+ :arg sequence cursor: Input sequence."""
2432
+
2433
+ for row in cursor:
2434
+ if rx.match(select(row)):
2435
+ yield row