wmglobalqueue 2.3.10rc10__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.
Potentially problematic release.
This version of wmglobalqueue might be problematic. Click here for more details.
- Utils/CPMetrics.py +270 -0
- Utils/CertTools.py +62 -0
- Utils/EmailAlert.py +50 -0
- Utils/ExtendedUnitTestCase.py +62 -0
- Utils/FileTools.py +182 -0
- Utils/IteratorTools.py +80 -0
- Utils/MathUtils.py +31 -0
- Utils/MemoryCache.py +119 -0
- Utils/Patterns.py +24 -0
- Utils/Pipeline.py +137 -0
- Utils/PortForward.py +97 -0
- Utils/ProcessStats.py +103 -0
- Utils/PythonVersion.py +17 -0
- Utils/Signals.py +36 -0
- Utils/TemporaryEnvironment.py +27 -0
- Utils/Throttled.py +227 -0
- Utils/Timers.py +130 -0
- Utils/Timestamps.py +86 -0
- Utils/TokenManager.py +143 -0
- Utils/Tracing.py +60 -0
- Utils/TwPrint.py +98 -0
- Utils/Utilities.py +308 -0
- Utils/__init__.py +11 -0
- WMCore/ACDC/Collection.py +57 -0
- WMCore/ACDC/CollectionTypes.py +12 -0
- WMCore/ACDC/CouchCollection.py +67 -0
- WMCore/ACDC/CouchFileset.py +238 -0
- WMCore/ACDC/CouchService.py +73 -0
- WMCore/ACDC/DataCollectionService.py +485 -0
- WMCore/ACDC/Fileset.py +94 -0
- WMCore/ACDC/__init__.py +11 -0
- WMCore/Algorithms/Alarm.py +39 -0
- WMCore/Algorithms/MathAlgos.py +274 -0
- WMCore/Algorithms/MiscAlgos.py +67 -0
- WMCore/Algorithms/ParseXMLFile.py +115 -0
- WMCore/Algorithms/Permissions.py +27 -0
- WMCore/Algorithms/Singleton.py +58 -0
- WMCore/Algorithms/SubprocessAlgos.py +129 -0
- WMCore/Algorithms/__init__.py +7 -0
- WMCore/Cache/GenericDataCache.py +98 -0
- WMCore/Cache/WMConfigCache.py +572 -0
- WMCore/Cache/__init__.py +0 -0
- WMCore/Configuration.py +651 -0
- WMCore/DAOFactory.py +47 -0
- WMCore/DataStructs/File.py +177 -0
- WMCore/DataStructs/Fileset.py +140 -0
- WMCore/DataStructs/Job.py +182 -0
- WMCore/DataStructs/JobGroup.py +142 -0
- WMCore/DataStructs/JobPackage.py +49 -0
- WMCore/DataStructs/LumiList.py +734 -0
- WMCore/DataStructs/Mask.py +219 -0
- WMCore/DataStructs/MathStructs/ContinuousSummaryHistogram.py +197 -0
- WMCore/DataStructs/MathStructs/DiscreteSummaryHistogram.py +92 -0
- WMCore/DataStructs/MathStructs/SummaryHistogram.py +117 -0
- WMCore/DataStructs/MathStructs/__init__.py +0 -0
- WMCore/DataStructs/Pickleable.py +24 -0
- WMCore/DataStructs/Run.py +256 -0
- WMCore/DataStructs/Subscription.py +175 -0
- WMCore/DataStructs/WMObject.py +47 -0
- WMCore/DataStructs/WorkUnit.py +112 -0
- WMCore/DataStructs/Workflow.py +60 -0
- WMCore/DataStructs/__init__.py +8 -0
- WMCore/Database/CMSCouch.py +1349 -0
- WMCore/Database/ConfigDBMap.py +29 -0
- WMCore/Database/CouchUtils.py +118 -0
- WMCore/Database/DBCore.py +198 -0
- WMCore/Database/DBCreator.py +113 -0
- WMCore/Database/DBExceptionHandler.py +57 -0
- WMCore/Database/DBFactory.py +110 -0
- WMCore/Database/DBFormatter.py +177 -0
- WMCore/Database/Dialects.py +13 -0
- WMCore/Database/ExecuteDAO.py +327 -0
- WMCore/Database/MongoDB.py +241 -0
- WMCore/Database/MySQL/Destroy.py +42 -0
- WMCore/Database/MySQL/ListUserContent.py +20 -0
- WMCore/Database/MySQL/__init__.py +9 -0
- WMCore/Database/MySQLCore.py +132 -0
- WMCore/Database/Oracle/Destroy.py +56 -0
- WMCore/Database/Oracle/ListUserContent.py +19 -0
- WMCore/Database/Oracle/__init__.py +9 -0
- WMCore/Database/ResultSet.py +44 -0
- WMCore/Database/Transaction.py +91 -0
- WMCore/Database/__init__.py +9 -0
- WMCore/Database/ipy_profile_couch.py +438 -0
- WMCore/GlobalWorkQueue/CherryPyThreads/CleanUpTask.py +29 -0
- WMCore/GlobalWorkQueue/CherryPyThreads/HeartbeatMonitor.py +105 -0
- WMCore/GlobalWorkQueue/CherryPyThreads/LocationUpdateTask.py +28 -0
- WMCore/GlobalWorkQueue/CherryPyThreads/ReqMgrInteractionTask.py +35 -0
- WMCore/GlobalWorkQueue/CherryPyThreads/__init__.py +0 -0
- WMCore/GlobalWorkQueue/__init__.py +0 -0
- WMCore/GroupUser/CouchObject.py +127 -0
- WMCore/GroupUser/Decorators.py +51 -0
- WMCore/GroupUser/Group.py +33 -0
- WMCore/GroupUser/Interface.py +73 -0
- WMCore/GroupUser/User.py +96 -0
- WMCore/GroupUser/__init__.py +11 -0
- WMCore/Lexicon.py +836 -0
- WMCore/REST/Auth.py +202 -0
- WMCore/REST/CherryPyPeriodicTask.py +166 -0
- WMCore/REST/Error.py +333 -0
- WMCore/REST/Format.py +642 -0
- WMCore/REST/HeartbeatMonitorBase.py +90 -0
- WMCore/REST/Main.py +623 -0
- WMCore/REST/Server.py +2435 -0
- WMCore/REST/Services.py +24 -0
- WMCore/REST/Test.py +120 -0
- WMCore/REST/Tools.py +38 -0
- WMCore/REST/Validation.py +250 -0
- WMCore/REST/__init__.py +1 -0
- WMCore/ReqMgr/DataStructs/RequestStatus.py +209 -0
- WMCore/ReqMgr/DataStructs/RequestType.py +13 -0
- WMCore/ReqMgr/DataStructs/__init__.py +0 -0
- WMCore/ReqMgr/__init__.py +1 -0
- WMCore/Services/AlertManager/AlertManagerAPI.py +111 -0
- WMCore/Services/AlertManager/__init__.py +0 -0
- WMCore/Services/CRIC/CRIC.py +238 -0
- WMCore/Services/CRIC/__init__.py +0 -0
- WMCore/Services/DBS/DBS3Reader.py +1044 -0
- WMCore/Services/DBS/DBSConcurrency.py +44 -0
- WMCore/Services/DBS/DBSErrors.py +113 -0
- WMCore/Services/DBS/DBSReader.py +23 -0
- WMCore/Services/DBS/DBSUtils.py +139 -0
- WMCore/Services/DBS/DBSWriterObjects.py +381 -0
- WMCore/Services/DBS/ProdException.py +133 -0
- WMCore/Services/DBS/__init__.py +8 -0
- WMCore/Services/FWJRDB/FWJRDBAPI.py +118 -0
- WMCore/Services/FWJRDB/__init__.py +0 -0
- WMCore/Services/HTTPS/HTTPSAuthHandler.py +66 -0
- WMCore/Services/HTTPS/__init__.py +0 -0
- WMCore/Services/LogDB/LogDB.py +201 -0
- WMCore/Services/LogDB/LogDBBackend.py +191 -0
- WMCore/Services/LogDB/LogDBExceptions.py +11 -0
- WMCore/Services/LogDB/LogDBReport.py +85 -0
- WMCore/Services/LogDB/__init__.py +0 -0
- WMCore/Services/MSPileup/__init__.py +0 -0
- WMCore/Services/MSUtils/MSUtils.py +54 -0
- WMCore/Services/MSUtils/__init__.py +0 -0
- WMCore/Services/McM/McM.py +173 -0
- WMCore/Services/McM/__init__.py +8 -0
- WMCore/Services/MonIT/Grafana.py +133 -0
- WMCore/Services/MonIT/__init__.py +0 -0
- WMCore/Services/PyCondor/PyCondorAPI.py +154 -0
- WMCore/Services/PyCondor/PyCondorUtils.py +105 -0
- WMCore/Services/PyCondor/__init__.py +0 -0
- WMCore/Services/ReqMgr/ReqMgr.py +261 -0
- WMCore/Services/ReqMgr/__init__.py +0 -0
- WMCore/Services/ReqMgrAux/ReqMgrAux.py +419 -0
- WMCore/Services/ReqMgrAux/__init__.py +0 -0
- WMCore/Services/RequestDB/RequestDBReader.py +267 -0
- WMCore/Services/RequestDB/RequestDBWriter.py +39 -0
- WMCore/Services/RequestDB/__init__.py +0 -0
- WMCore/Services/Requests.py +624 -0
- WMCore/Services/Rucio/Rucio.py +1287 -0
- WMCore/Services/Rucio/RucioUtils.py +74 -0
- WMCore/Services/Rucio/__init__.py +0 -0
- WMCore/Services/RucioConMon/RucioConMon.py +128 -0
- WMCore/Services/RucioConMon/__init__.py +0 -0
- WMCore/Services/Service.py +400 -0
- WMCore/Services/StompAMQ/__init__.py +0 -0
- WMCore/Services/TagCollector/TagCollector.py +155 -0
- WMCore/Services/TagCollector/XMLUtils.py +98 -0
- WMCore/Services/TagCollector/__init__.py +0 -0
- WMCore/Services/UUIDLib.py +13 -0
- WMCore/Services/UserFileCache/UserFileCache.py +160 -0
- WMCore/Services/UserFileCache/__init__.py +8 -0
- WMCore/Services/WMAgent/WMAgent.py +63 -0
- WMCore/Services/WMAgent/__init__.py +0 -0
- WMCore/Services/WMArchive/CMSSWMetrics.py +526 -0
- WMCore/Services/WMArchive/DataMap.py +463 -0
- WMCore/Services/WMArchive/WMArchive.py +33 -0
- WMCore/Services/WMArchive/__init__.py +0 -0
- WMCore/Services/WMBS/WMBS.py +97 -0
- WMCore/Services/WMBS/__init__.py +0 -0
- WMCore/Services/WMStats/DataStruct/RequestInfoCollection.py +300 -0
- WMCore/Services/WMStats/DataStruct/__init__.py +0 -0
- WMCore/Services/WMStats/WMStatsPycurl.py +145 -0
- WMCore/Services/WMStats/WMStatsReader.py +445 -0
- WMCore/Services/WMStats/WMStatsWriter.py +273 -0
- WMCore/Services/WMStats/__init__.py +0 -0
- WMCore/Services/WMStatsServer/WMStatsServer.py +134 -0
- WMCore/Services/WMStatsServer/__init__.py +0 -0
- WMCore/Services/WorkQueue/WorkQueue.py +492 -0
- WMCore/Services/WorkQueue/__init__.py +0 -0
- WMCore/Services/__init__.py +8 -0
- WMCore/Services/pycurl_manager.py +574 -0
- WMCore/WMBase.py +50 -0
- WMCore/WMConnectionBase.py +164 -0
- WMCore/WMException.py +183 -0
- WMCore/WMExceptions.py +269 -0
- WMCore/WMFactory.py +76 -0
- WMCore/WMInit.py +228 -0
- WMCore/WMLogging.py +108 -0
- WMCore/WMSpec/ConfigSectionTree.py +442 -0
- WMCore/WMSpec/Persistency.py +135 -0
- WMCore/WMSpec/Steps/BuildMaster.py +87 -0
- WMCore/WMSpec/Steps/BuildTools.py +201 -0
- WMCore/WMSpec/Steps/Builder.py +97 -0
- WMCore/WMSpec/Steps/Diagnostic.py +89 -0
- WMCore/WMSpec/Steps/Emulator.py +62 -0
- WMCore/WMSpec/Steps/ExecuteMaster.py +208 -0
- WMCore/WMSpec/Steps/Executor.py +210 -0
- WMCore/WMSpec/Steps/StepFactory.py +213 -0
- WMCore/WMSpec/Steps/TaskEmulator.py +75 -0
- WMCore/WMSpec/Steps/Template.py +204 -0
- WMCore/WMSpec/Steps/Templates/AlcaHarvest.py +76 -0
- WMCore/WMSpec/Steps/Templates/CMSSW.py +613 -0
- WMCore/WMSpec/Steps/Templates/DQMUpload.py +59 -0
- WMCore/WMSpec/Steps/Templates/DeleteFiles.py +70 -0
- WMCore/WMSpec/Steps/Templates/LogArchive.py +84 -0
- WMCore/WMSpec/Steps/Templates/LogCollect.py +105 -0
- WMCore/WMSpec/Steps/Templates/StageOut.py +105 -0
- WMCore/WMSpec/Steps/Templates/__init__.py +10 -0
- WMCore/WMSpec/Steps/WMExecutionFailure.py +21 -0
- WMCore/WMSpec/Steps/__init__.py +8 -0
- WMCore/WMSpec/Utilities.py +63 -0
- WMCore/WMSpec/WMSpecErrors.py +12 -0
- WMCore/WMSpec/WMStep.py +347 -0
- WMCore/WMSpec/WMTask.py +1980 -0
- WMCore/WMSpec/WMWorkload.py +2288 -0
- WMCore/WMSpec/WMWorkloadTools.py +370 -0
- WMCore/WMSpec/__init__.py +9 -0
- WMCore/WorkQueue/DataLocationMapper.py +269 -0
- WMCore/WorkQueue/DataStructs/ACDCBlock.py +47 -0
- WMCore/WorkQueue/DataStructs/Block.py +48 -0
- WMCore/WorkQueue/DataStructs/CouchWorkQueueElement.py +148 -0
- WMCore/WorkQueue/DataStructs/WorkQueueElement.py +274 -0
- WMCore/WorkQueue/DataStructs/WorkQueueElementResult.py +152 -0
- WMCore/WorkQueue/DataStructs/WorkQueueElementsSummary.py +185 -0
- WMCore/WorkQueue/DataStructs/__init__.py +0 -0
- WMCore/WorkQueue/Policy/End/EndPolicyInterface.py +44 -0
- WMCore/WorkQueue/Policy/End/SingleShot.py +22 -0
- WMCore/WorkQueue/Policy/End/__init__.py +32 -0
- WMCore/WorkQueue/Policy/PolicyInterface.py +17 -0
- WMCore/WorkQueue/Policy/Start/Block.py +258 -0
- WMCore/WorkQueue/Policy/Start/Dataset.py +180 -0
- WMCore/WorkQueue/Policy/Start/MonteCarlo.py +131 -0
- WMCore/WorkQueue/Policy/Start/ResubmitBlock.py +171 -0
- WMCore/WorkQueue/Policy/Start/StartPolicyInterface.py +316 -0
- WMCore/WorkQueue/Policy/Start/__init__.py +34 -0
- WMCore/WorkQueue/Policy/__init__.py +57 -0
- WMCore/WorkQueue/WMBSHelper.py +772 -0
- WMCore/WorkQueue/WorkQueue.py +1237 -0
- WMCore/WorkQueue/WorkQueueBackend.py +741 -0
- WMCore/WorkQueue/WorkQueueBase.py +39 -0
- WMCore/WorkQueue/WorkQueueExceptions.py +44 -0
- WMCore/WorkQueue/WorkQueueReqMgrInterface.py +278 -0
- WMCore/WorkQueue/WorkQueueUtils.py +130 -0
- WMCore/WorkQueue/__init__.py +13 -0
- WMCore/Wrappers/JsonWrapper/JSONThunker.py +342 -0
- WMCore/Wrappers/JsonWrapper/__init__.py +7 -0
- WMCore/Wrappers/__init__.py +6 -0
- WMCore/__init__.py +10 -0
- wmglobalqueue-2.3.10rc10.data/data/bin/wmc-dist-patch +15 -0
- wmglobalqueue-2.3.10rc10.data/data/bin/wmc-dist-unpatch +8 -0
- wmglobalqueue-2.3.10rc10.data/data/bin/wmc-httpd +3 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/.couchapprc +1 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/README.md +40 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/_attachments/index.html +264 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/_attachments/js/ElementInfoByWorkflow.js +96 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/_attachments/js/StuckElementInfo.js +57 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/_attachments/js/WorkloadInfoTable.js +80 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/_attachments/js/dataTable.js +70 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/_attachments/js/namespace.js +23 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/_attachments/style/main.css +75 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/couchapp.json +4 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/filters/childQueueFilter.js +13 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/filters/filterDeletedDocs.js +3 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/filters/queueFilter.js +11 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/language +1 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/lib/mustache.js +333 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/lib/validate.js +27 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/lib/workqueue_utils.js +61 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/lists/elementsDetail.js +28 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/lists/filter.js +86 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/lists/stuckElements.js +38 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/lists/workRestrictions.js +153 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/lists/workflowSummary.js +28 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/rewrites.json +73 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/shows/redirect.js +23 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/shows/status.js +40 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/templates/ElementSummaryByWorkflow.html +27 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/templates/StuckElementSummary.html +26 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/templates/TaskStatus.html +23 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/templates/WorkflowSummary.html +27 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/templates/partials/workqueue-common-lib.html +2 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/templates/partials/yui-lib-remote.html +16 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/templates/partials/yui-lib.html +18 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/updates/in-place.js +50 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/validate_doc_update.js +8 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/vendor/couchapp/_attachments/jquery.couch.app.js +235 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/vendor/couchapp/_attachments/jquery.pathbinder.js +173 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/activeData/map.js +8 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/activeData/reduce.js +2 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/activeParentData/map.js +8 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/activeParentData/reduce.js +2 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/activePileupData/map.js +8 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/activePileupData/reduce.js +2 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/analyticsData/map.js +11 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/analyticsData/reduce.js +1 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/availableByPriority/map.js +6 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/conflicts/map.js +5 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/elements/map.js +5 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/elementsByData/map.js +8 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/elementsByParent/map.js +8 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/elementsByParentData/map.js +8 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/elementsByPileupData/map.js +8 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/elementsByStatus/map.js +8 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/elementsBySubscription/map.js +6 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/elementsByWorkflow/map.js +8 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/elementsByWorkflow/reduce.js +3 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/elementsDetailByWorkflowAndStatus/map.js +26 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/jobInjectStatusByRequest/map.js +10 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/jobInjectStatusByRequest/reduce.js +1 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/jobStatusByRequest/map.js +6 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/jobStatusByRequest/reduce.js +1 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/jobsByChildQueueAndPriority/map.js +6 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/jobsByChildQueueAndPriority/reduce.js +1 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/jobsByChildQueueAndStatus/map.js +6 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/jobsByChildQueueAndStatus/reduce.js +1 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/jobsByRequest/map.js +6 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/jobsByRequest/reduce.js +1 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/jobsByStatus/map.js +6 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/jobsByStatus/reduce.js +1 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/jobsByStatusAndPriority/map.js +6 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/jobsByStatusAndPriority/reduce.js +1 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/openRequests/map.js +6 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/recent-items/map.js +5 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/siteWhitelistByRequest/map.js +6 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/siteWhitelistByRequest/reduce.js +1 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/specsByWorkflow/map.js +5 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/stuckElements/map.js +38 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/wmbsInjectStatusByRequest/map.js +12 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/wmbsInjectStatusByRequest/reduce.js +3 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/wmbsUrl/map.js +6 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/wmbsUrl/reduce.js +2 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/wmbsUrlByRequest/map.js +6 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/wmbsUrlByRequest/reduce.js +2 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/workflowSummary/map.js +9 -0
- wmglobalqueue-2.3.10rc10.data/data/data/couchapps/WorkQueue/views/workflowSummary/reduce.js +10 -0
- wmglobalqueue-2.3.10rc10.dist-info/METADATA +26 -0
- wmglobalqueue-2.3.10rc10.dist-info/RECORD +345 -0
- wmglobalqueue-2.3.10rc10.dist-info/WHEEL +5 -0
- wmglobalqueue-2.3.10rc10.dist-info/licenses/LICENSE +202 -0
- wmglobalqueue-2.3.10rc10.dist-info/licenses/NOTICE +16 -0
- wmglobalqueue-2.3.10rc10.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
|