flowtask 5.8.4__cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (470) hide show
  1. flowtask/__init__.py +93 -0
  2. flowtask/__main__.py +38 -0
  3. flowtask/bots/__init__.py +6 -0
  4. flowtask/bots/check.py +93 -0
  5. flowtask/bots/codebot.py +51 -0
  6. flowtask/components/ASPX.py +148 -0
  7. flowtask/components/AddDataset.py +352 -0
  8. flowtask/components/Amazon.py +523 -0
  9. flowtask/components/AutoTask.py +314 -0
  10. flowtask/components/Azure.py +80 -0
  11. flowtask/components/AzureUsers.py +106 -0
  12. flowtask/components/BaseAction.py +91 -0
  13. flowtask/components/BaseLoop.py +198 -0
  14. flowtask/components/BestBuy.py +800 -0
  15. flowtask/components/CSVToGCS.py +120 -0
  16. flowtask/components/CompanyScraper/__init__.py +1 -0
  17. flowtask/components/CompanyScraper/parsers/__init__.py +6 -0
  18. flowtask/components/CompanyScraper/parsers/base.py +102 -0
  19. flowtask/components/CompanyScraper/parsers/explorium.py +192 -0
  20. flowtask/components/CompanyScraper/parsers/leadiq.py +206 -0
  21. flowtask/components/CompanyScraper/parsers/rocket.py +133 -0
  22. flowtask/components/CompanyScraper/parsers/siccode.py +109 -0
  23. flowtask/components/CompanyScraper/parsers/visualvisitor.py +130 -0
  24. flowtask/components/CompanyScraper/parsers/zoominfo.py +118 -0
  25. flowtask/components/CompanyScraper/scrapper.py +1054 -0
  26. flowtask/components/CopyTo.py +177 -0
  27. flowtask/components/CopyToBigQuery.py +243 -0
  28. flowtask/components/CopyToMongoDB.py +291 -0
  29. flowtask/components/CopyToPg.py +609 -0
  30. flowtask/components/CopyToRethink.py +207 -0
  31. flowtask/components/CreateGCSBucket.py +102 -0
  32. flowtask/components/CreateReport/CreateReport.py +228 -0
  33. flowtask/components/CreateReport/__init__.py +9 -0
  34. flowtask/components/CreateReport/charts/__init__.py +15 -0
  35. flowtask/components/CreateReport/charts/bar.py +51 -0
  36. flowtask/components/CreateReport/charts/base.py +66 -0
  37. flowtask/components/CreateReport/charts/pie.py +64 -0
  38. flowtask/components/CreateReport/utils.py +9 -0
  39. flowtask/components/CustomerSatisfaction.py +196 -0
  40. flowtask/components/DataInput.py +200 -0
  41. flowtask/components/DateList.py +255 -0
  42. flowtask/components/DbClient.py +163 -0
  43. flowtask/components/DialPad.py +146 -0
  44. flowtask/components/DocumentDBQuery.py +200 -0
  45. flowtask/components/DownloadFrom.py +371 -0
  46. flowtask/components/DownloadFromD2L.py +113 -0
  47. flowtask/components/DownloadFromFTP.py +181 -0
  48. flowtask/components/DownloadFromIMAP.py +315 -0
  49. flowtask/components/DownloadFromS3.py +198 -0
  50. flowtask/components/DownloadFromSFTP.py +265 -0
  51. flowtask/components/DownloadFromSharepoint.py +110 -0
  52. flowtask/components/DownloadFromSmartSheet.py +114 -0
  53. flowtask/components/DownloadS3File.py +229 -0
  54. flowtask/components/Dummy.py +59 -0
  55. flowtask/components/DuplicatePhoto.py +411 -0
  56. flowtask/components/EmployeeEvaluation.py +237 -0
  57. flowtask/components/ExecuteSQL.py +323 -0
  58. flowtask/components/ExtractHTML.py +178 -0
  59. flowtask/components/FileBase.py +178 -0
  60. flowtask/components/FileCopy.py +181 -0
  61. flowtask/components/FileDelete.py +82 -0
  62. flowtask/components/FileExists.py +146 -0
  63. flowtask/components/FileIteratorDelete.py +112 -0
  64. flowtask/components/FileList.py +194 -0
  65. flowtask/components/FileOpen.py +75 -0
  66. flowtask/components/FileRead.py +120 -0
  67. flowtask/components/FileRename.py +106 -0
  68. flowtask/components/FilterIf.py +284 -0
  69. flowtask/components/FilterRows/FilterRows.py +200 -0
  70. flowtask/components/FilterRows/__init__.py +10 -0
  71. flowtask/components/FilterRows/functions.py +4 -0
  72. flowtask/components/GCSToBigQuery.py +103 -0
  73. flowtask/components/GoogleA4.py +150 -0
  74. flowtask/components/GoogleGeoCoding.py +344 -0
  75. flowtask/components/GooglePlaces.py +315 -0
  76. flowtask/components/GoogleSearch.py +539 -0
  77. flowtask/components/HTTPClient.py +268 -0
  78. flowtask/components/ICIMS.py +146 -0
  79. flowtask/components/IF.py +179 -0
  80. flowtask/components/IcimsFolderCopy.py +173 -0
  81. flowtask/components/ImageFeatures/__init__.py +5 -0
  82. flowtask/components/ImageFeatures/process.py +233 -0
  83. flowtask/components/IteratorBase.py +251 -0
  84. flowtask/components/LangchainLoader/__init__.py +5 -0
  85. flowtask/components/LangchainLoader/loader.py +194 -0
  86. flowtask/components/LangchainLoader/loaders/__init__.py +22 -0
  87. flowtask/components/LangchainLoader/loaders/abstract.py +362 -0
  88. flowtask/components/LangchainLoader/loaders/basepdf.py +50 -0
  89. flowtask/components/LangchainLoader/loaders/docx.py +91 -0
  90. flowtask/components/LangchainLoader/loaders/html.py +119 -0
  91. flowtask/components/LangchainLoader/loaders/pdfblocks.py +146 -0
  92. flowtask/components/LangchainLoader/loaders/pdfmark.py +79 -0
  93. flowtask/components/LangchainLoader/loaders/pdftables.py +135 -0
  94. flowtask/components/LangchainLoader/loaders/qa.py +67 -0
  95. flowtask/components/LangchainLoader/loaders/txt.py +55 -0
  96. flowtask/components/LeadIQ.py +650 -0
  97. flowtask/components/Loop.py +253 -0
  98. flowtask/components/Lowes.py +334 -0
  99. flowtask/components/MS365Usage.py +156 -0
  100. flowtask/components/MSTeamsMessages.py +320 -0
  101. flowtask/components/MarketClustering.py +1051 -0
  102. flowtask/components/MergeFiles.py +362 -0
  103. flowtask/components/MilvusOutput.py +87 -0
  104. flowtask/components/NearByStores.py +175 -0
  105. flowtask/components/NetworkNinja/__init__.py +6 -0
  106. flowtask/components/NetworkNinja/models/__init__.py +52 -0
  107. flowtask/components/NetworkNinja/models/abstract.py +177 -0
  108. flowtask/components/NetworkNinja/models/account.py +39 -0
  109. flowtask/components/NetworkNinja/models/client.py +19 -0
  110. flowtask/components/NetworkNinja/models/district.py +14 -0
  111. flowtask/components/NetworkNinja/models/events.py +101 -0
  112. flowtask/components/NetworkNinja/models/forms.py +499 -0
  113. flowtask/components/NetworkNinja/models/market.py +16 -0
  114. flowtask/components/NetworkNinja/models/organization.py +34 -0
  115. flowtask/components/NetworkNinja/models/photos.py +125 -0
  116. flowtask/components/NetworkNinja/models/project.py +44 -0
  117. flowtask/components/NetworkNinja/models/region.py +28 -0
  118. flowtask/components/NetworkNinja/models/store.py +203 -0
  119. flowtask/components/NetworkNinja/models/user.py +151 -0
  120. flowtask/components/NetworkNinja/router.py +854 -0
  121. flowtask/components/Odoo.py +175 -0
  122. flowtask/components/OdooInjector.py +192 -0
  123. flowtask/components/OpenFromXML.py +126 -0
  124. flowtask/components/OpenWeather.py +41 -0
  125. flowtask/components/OpenWithBase.py +616 -0
  126. flowtask/components/OpenWithPandas.py +715 -0
  127. flowtask/components/PGPDecrypt.py +199 -0
  128. flowtask/components/PandasIterator.py +187 -0
  129. flowtask/components/PandasToFile.py +189 -0
  130. flowtask/components/Paradox.py +339 -0
  131. flowtask/components/ParamIterator.py +117 -0
  132. flowtask/components/ParseHTML.py +84 -0
  133. flowtask/components/PlacerStores.py +249 -0
  134. flowtask/components/Pokemon.py +507 -0
  135. flowtask/components/PositiveBot.py +62 -0
  136. flowtask/components/PowerPointSlide.py +400 -0
  137. flowtask/components/PrintMessage.py +127 -0
  138. flowtask/components/ProductCompetitors/__init__.py +5 -0
  139. flowtask/components/ProductCompetitors/parsers/__init__.py +7 -0
  140. flowtask/components/ProductCompetitors/parsers/base.py +72 -0
  141. flowtask/components/ProductCompetitors/parsers/bestbuy.py +86 -0
  142. flowtask/components/ProductCompetitors/parsers/lowes.py +103 -0
  143. flowtask/components/ProductCompetitors/scrapper.py +155 -0
  144. flowtask/components/ProductCompliant.py +169 -0
  145. flowtask/components/ProductInfo/__init__.py +1 -0
  146. flowtask/components/ProductInfo/parsers/__init__.py +5 -0
  147. flowtask/components/ProductInfo/parsers/base.py +83 -0
  148. flowtask/components/ProductInfo/parsers/brother.py +97 -0
  149. flowtask/components/ProductInfo/parsers/canon.py +167 -0
  150. flowtask/components/ProductInfo/parsers/epson.py +118 -0
  151. flowtask/components/ProductInfo/parsers/hp.py +131 -0
  152. flowtask/components/ProductInfo/parsers/samsung.py +97 -0
  153. flowtask/components/ProductInfo/scraper.py +319 -0
  154. flowtask/components/ProductPricing.py +118 -0
  155. flowtask/components/QS.py +261 -0
  156. flowtask/components/QSBase.py +201 -0
  157. flowtask/components/QueryIterator.py +273 -0
  158. flowtask/components/QueryToInsert.py +327 -0
  159. flowtask/components/QueryToPandas.py +432 -0
  160. flowtask/components/RESTClient.py +195 -0
  161. flowtask/components/RethinkDBQuery.py +189 -0
  162. flowtask/components/Rsync.py +74 -0
  163. flowtask/components/RunSSH.py +59 -0
  164. flowtask/components/RunShell.py +71 -0
  165. flowtask/components/SalesForce.py +20 -0
  166. flowtask/components/SaveImageBank/__init__.py +257 -0
  167. flowtask/components/SchedulingVisits.py +592 -0
  168. flowtask/components/ScrapPage.py +216 -0
  169. flowtask/components/ScrapSearch.py +79 -0
  170. flowtask/components/SendNotify.py +257 -0
  171. flowtask/components/SentimentAnalysis.py +694 -0
  172. flowtask/components/ServiceScrapper/__init__.py +5 -0
  173. flowtask/components/ServiceScrapper/parsers/__init__.py +1 -0
  174. flowtask/components/ServiceScrapper/parsers/base.py +94 -0
  175. flowtask/components/ServiceScrapper/parsers/costco.py +93 -0
  176. flowtask/components/ServiceScrapper/scrapper.py +199 -0
  177. flowtask/components/SetVariables.py +156 -0
  178. flowtask/components/SubTask.py +182 -0
  179. flowtask/components/SuiteCRM.py +48 -0
  180. flowtask/components/Switch.py +175 -0
  181. flowtask/components/TableBase.py +148 -0
  182. flowtask/components/TableDelete.py +312 -0
  183. flowtask/components/TableInput.py +143 -0
  184. flowtask/components/TableOutput/TableOutput.py +384 -0
  185. flowtask/components/TableOutput/__init__.py +3 -0
  186. flowtask/components/TableSchema.py +534 -0
  187. flowtask/components/Target.py +223 -0
  188. flowtask/components/ThumbnailGenerator.py +156 -0
  189. flowtask/components/ToPandas.py +67 -0
  190. flowtask/components/TransformRows/TransformRows.py +507 -0
  191. flowtask/components/TransformRows/__init__.py +9 -0
  192. flowtask/components/TransformRows/functions.py +559 -0
  193. flowtask/components/TransposeRows.py +176 -0
  194. flowtask/components/UPCDatabase.py +86 -0
  195. flowtask/components/UnGzip.py +171 -0
  196. flowtask/components/Uncompress.py +172 -0
  197. flowtask/components/UniqueRows.py +126 -0
  198. flowtask/components/Unzip.py +107 -0
  199. flowtask/components/UpdateOperationalVars.py +147 -0
  200. flowtask/components/UploadTo.py +299 -0
  201. flowtask/components/UploadToS3.py +136 -0
  202. flowtask/components/UploadToSFTP.py +160 -0
  203. flowtask/components/UploadToSharepoint.py +205 -0
  204. flowtask/components/UserFunc.py +122 -0
  205. flowtask/components/VivaTracker.py +140 -0
  206. flowtask/components/WSDLClient.py +123 -0
  207. flowtask/components/Wait.py +18 -0
  208. flowtask/components/Walmart.py +199 -0
  209. flowtask/components/Workplace.py +134 -0
  210. flowtask/components/XMLToPandas.py +267 -0
  211. flowtask/components/Zammad/__init__.py +41 -0
  212. flowtask/components/Zammad/models.py +0 -0
  213. flowtask/components/ZoomInfoScraper.py +409 -0
  214. flowtask/components/__init__.py +104 -0
  215. flowtask/components/abstract.py +18 -0
  216. flowtask/components/flow.py +530 -0
  217. flowtask/components/google.py +335 -0
  218. flowtask/components/group.py +221 -0
  219. flowtask/components/py.typed +0 -0
  220. flowtask/components/reviewscrap.py +132 -0
  221. flowtask/components/tAutoincrement.py +117 -0
  222. flowtask/components/tConcat.py +109 -0
  223. flowtask/components/tExplode.py +119 -0
  224. flowtask/components/tFilter.py +184 -0
  225. flowtask/components/tGroup.py +236 -0
  226. flowtask/components/tJoin.py +270 -0
  227. flowtask/components/tMap/__init__.py +9 -0
  228. flowtask/components/tMap/functions.py +54 -0
  229. flowtask/components/tMap/tMap.py +450 -0
  230. flowtask/components/tMelt.py +112 -0
  231. flowtask/components/tMerge.py +114 -0
  232. flowtask/components/tOrder.py +93 -0
  233. flowtask/components/tPandas.py +94 -0
  234. flowtask/components/tPivot.py +71 -0
  235. flowtask/components/tPluckCols.py +76 -0
  236. flowtask/components/tUnnest.py +82 -0
  237. flowtask/components/user.py +401 -0
  238. flowtask/conf.py +457 -0
  239. flowtask/download.py +102 -0
  240. flowtask/events/__init__.py +11 -0
  241. flowtask/events/events/__init__.py +20 -0
  242. flowtask/events/events/abstract.py +95 -0
  243. flowtask/events/events/alerts/__init__.py +362 -0
  244. flowtask/events/events/alerts/colfunctions.py +131 -0
  245. flowtask/events/events/alerts/functions.py +158 -0
  246. flowtask/events/events/dummy.py +12 -0
  247. flowtask/events/events/exec.py +124 -0
  248. flowtask/events/events/file/__init__.py +7 -0
  249. flowtask/events/events/file/base.py +51 -0
  250. flowtask/events/events/file/copy.py +23 -0
  251. flowtask/events/events/file/delete.py +16 -0
  252. flowtask/events/events/interfaces/__init__.py +9 -0
  253. flowtask/events/events/interfaces/client.py +67 -0
  254. flowtask/events/events/interfaces/credentials.py +28 -0
  255. flowtask/events/events/interfaces/notifications.py +58 -0
  256. flowtask/events/events/jira.py +122 -0
  257. flowtask/events/events/log.py +26 -0
  258. flowtask/events/events/logerr.py +52 -0
  259. flowtask/events/events/notify.py +59 -0
  260. flowtask/events/events/notify_event.py +160 -0
  261. flowtask/events/events/publish.py +54 -0
  262. flowtask/events/events/sendfile.py +104 -0
  263. flowtask/events/events/task.py +97 -0
  264. flowtask/events/events/teams.py +98 -0
  265. flowtask/events/events/webhook.py +58 -0
  266. flowtask/events/manager.py +287 -0
  267. flowtask/exceptions.c +39393 -0
  268. flowtask/exceptions.cpython-39-x86_64-linux-gnu.so +0 -0
  269. flowtask/extensions/__init__.py +3 -0
  270. flowtask/extensions/abstract.py +82 -0
  271. flowtask/extensions/logging/__init__.py +65 -0
  272. flowtask/hooks/__init__.py +9 -0
  273. flowtask/hooks/actions/__init__.py +22 -0
  274. flowtask/hooks/actions/abstract.py +66 -0
  275. flowtask/hooks/actions/dummy.py +23 -0
  276. flowtask/hooks/actions/jira.py +74 -0
  277. flowtask/hooks/actions/rest.py +320 -0
  278. flowtask/hooks/actions/sampledata.py +37 -0
  279. flowtask/hooks/actions/sensor.py +23 -0
  280. flowtask/hooks/actions/task.py +9 -0
  281. flowtask/hooks/actions/ticket.py +37 -0
  282. flowtask/hooks/actions/zammad.py +55 -0
  283. flowtask/hooks/hook.py +62 -0
  284. flowtask/hooks/models.py +17 -0
  285. flowtask/hooks/service.py +187 -0
  286. flowtask/hooks/step.py +91 -0
  287. flowtask/hooks/types/__init__.py +23 -0
  288. flowtask/hooks/types/base.py +129 -0
  289. flowtask/hooks/types/brokers/__init__.py +11 -0
  290. flowtask/hooks/types/brokers/base.py +54 -0
  291. flowtask/hooks/types/brokers/mqtt.py +35 -0
  292. flowtask/hooks/types/brokers/rabbitmq.py +82 -0
  293. flowtask/hooks/types/brokers/redis.py +83 -0
  294. flowtask/hooks/types/brokers/sqs.py +44 -0
  295. flowtask/hooks/types/fs.py +232 -0
  296. flowtask/hooks/types/http.py +49 -0
  297. flowtask/hooks/types/imap.py +200 -0
  298. flowtask/hooks/types/jira.py +279 -0
  299. flowtask/hooks/types/mail.py +205 -0
  300. flowtask/hooks/types/postgres.py +98 -0
  301. flowtask/hooks/types/responses/__init__.py +8 -0
  302. flowtask/hooks/types/responses/base.py +5 -0
  303. flowtask/hooks/types/sharepoint.py +288 -0
  304. flowtask/hooks/types/ssh.py +141 -0
  305. flowtask/hooks/types/tagged.py +59 -0
  306. flowtask/hooks/types/upload.py +85 -0
  307. flowtask/hooks/types/watch.py +71 -0
  308. flowtask/hooks/types/web.py +36 -0
  309. flowtask/interfaces/AzureClient.py +137 -0
  310. flowtask/interfaces/AzureGraph.py +839 -0
  311. flowtask/interfaces/Boto3Client.py +326 -0
  312. flowtask/interfaces/DropboxClient.py +173 -0
  313. flowtask/interfaces/ExcelHandler.py +94 -0
  314. flowtask/interfaces/FTPClient.py +131 -0
  315. flowtask/interfaces/GoogleCalendar.py +201 -0
  316. flowtask/interfaces/GoogleClient.py +133 -0
  317. flowtask/interfaces/GoogleDrive.py +127 -0
  318. flowtask/interfaces/GoogleGCS.py +89 -0
  319. flowtask/interfaces/GoogleGeocoding.py +93 -0
  320. flowtask/interfaces/GoogleLang.py +114 -0
  321. flowtask/interfaces/GooglePub.py +61 -0
  322. flowtask/interfaces/GoogleSheet.py +68 -0
  323. flowtask/interfaces/IMAPClient.py +137 -0
  324. flowtask/interfaces/O365Calendar.py +113 -0
  325. flowtask/interfaces/O365Client.py +220 -0
  326. flowtask/interfaces/OneDrive.py +284 -0
  327. flowtask/interfaces/Outlook.py +155 -0
  328. flowtask/interfaces/ParrotBot.py +130 -0
  329. flowtask/interfaces/SSHClient.py +378 -0
  330. flowtask/interfaces/Sharepoint.py +496 -0
  331. flowtask/interfaces/__init__.py +36 -0
  332. flowtask/interfaces/azureauth.py +119 -0
  333. flowtask/interfaces/cache.py +201 -0
  334. flowtask/interfaces/client.py +82 -0
  335. flowtask/interfaces/compress.py +525 -0
  336. flowtask/interfaces/credentials.py +124 -0
  337. flowtask/interfaces/d2l.py +239 -0
  338. flowtask/interfaces/databases/__init__.py +5 -0
  339. flowtask/interfaces/databases/db.py +223 -0
  340. flowtask/interfaces/databases/documentdb.py +55 -0
  341. flowtask/interfaces/databases/rethink.py +39 -0
  342. flowtask/interfaces/dataframes/__init__.py +11 -0
  343. flowtask/interfaces/dataframes/abstract.py +21 -0
  344. flowtask/interfaces/dataframes/arrow.py +71 -0
  345. flowtask/interfaces/dataframes/dt.py +69 -0
  346. flowtask/interfaces/dataframes/pandas.py +167 -0
  347. flowtask/interfaces/dataframes/polars.py +60 -0
  348. flowtask/interfaces/db.py +263 -0
  349. flowtask/interfaces/env.py +46 -0
  350. flowtask/interfaces/func.py +137 -0
  351. flowtask/interfaces/http.py +1780 -0
  352. flowtask/interfaces/locale.py +40 -0
  353. flowtask/interfaces/log.py +75 -0
  354. flowtask/interfaces/mask.py +143 -0
  355. flowtask/interfaces/notification.py +154 -0
  356. flowtask/interfaces/playwright.py +339 -0
  357. flowtask/interfaces/powerpoint.py +368 -0
  358. flowtask/interfaces/py.typed +0 -0
  359. flowtask/interfaces/qs.py +376 -0
  360. flowtask/interfaces/result.py +87 -0
  361. flowtask/interfaces/selenium_service.py +779 -0
  362. flowtask/interfaces/smartsheet.py +154 -0
  363. flowtask/interfaces/stat.py +39 -0
  364. flowtask/interfaces/task.py +96 -0
  365. flowtask/interfaces/template.py +118 -0
  366. flowtask/interfaces/vectorstores/__init__.py +1 -0
  367. flowtask/interfaces/vectorstores/abstract.py +133 -0
  368. flowtask/interfaces/vectorstores/milvus.py +669 -0
  369. flowtask/interfaces/zammad.py +107 -0
  370. flowtask/models.py +193 -0
  371. flowtask/parsers/__init__.py +15 -0
  372. flowtask/parsers/_yaml.c +11978 -0
  373. flowtask/parsers/_yaml.cpython-39-x86_64-linux-gnu.so +0 -0
  374. flowtask/parsers/argparser.py +235 -0
  375. flowtask/parsers/base.c +15155 -0
  376. flowtask/parsers/base.cpython-39-x86_64-linux-gnu.so +0 -0
  377. flowtask/parsers/json.c +11968 -0
  378. flowtask/parsers/json.cpython-39-x86_64-linux-gnu.so +0 -0
  379. flowtask/parsers/maps.py +49 -0
  380. flowtask/parsers/toml.c +11968 -0
  381. flowtask/parsers/toml.cpython-39-x86_64-linux-gnu.so +0 -0
  382. flowtask/plugins/__init__.py +16 -0
  383. flowtask/plugins/components/__init__.py +0 -0
  384. flowtask/plugins/handler/__init__.py +45 -0
  385. flowtask/plugins/importer.py +31 -0
  386. flowtask/plugins/sources/__init__.py +0 -0
  387. flowtask/runner.py +283 -0
  388. flowtask/scheduler/__init__.py +9 -0
  389. flowtask/scheduler/functions.py +493 -0
  390. flowtask/scheduler/handlers/__init__.py +8 -0
  391. flowtask/scheduler/handlers/manager.py +504 -0
  392. flowtask/scheduler/handlers/models.py +58 -0
  393. flowtask/scheduler/handlers/service.py +72 -0
  394. flowtask/scheduler/notifications.py +65 -0
  395. flowtask/scheduler/scheduler.py +993 -0
  396. flowtask/services/__init__.py +0 -0
  397. flowtask/services/bots/__init__.py +0 -0
  398. flowtask/services/bots/telegram.py +264 -0
  399. flowtask/services/files/__init__.py +11 -0
  400. flowtask/services/files/manager.py +522 -0
  401. flowtask/services/files/model.py +37 -0
  402. flowtask/services/files/service.py +767 -0
  403. flowtask/services/jira/__init__.py +3 -0
  404. flowtask/services/jira/jira_actions.py +191 -0
  405. flowtask/services/tasks/__init__.py +13 -0
  406. flowtask/services/tasks/launcher.py +213 -0
  407. flowtask/services/tasks/manager.py +323 -0
  408. flowtask/services/tasks/service.py +275 -0
  409. flowtask/services/tasks/task_manager.py +376 -0
  410. flowtask/services/tasks/tasks.py +155 -0
  411. flowtask/storages/__init__.py +16 -0
  412. flowtask/storages/exceptions.py +12 -0
  413. flowtask/storages/files/__init__.py +8 -0
  414. flowtask/storages/files/abstract.py +29 -0
  415. flowtask/storages/files/filesystem.py +66 -0
  416. flowtask/storages/tasks/__init__.py +19 -0
  417. flowtask/storages/tasks/abstract.py +26 -0
  418. flowtask/storages/tasks/database.py +33 -0
  419. flowtask/storages/tasks/filesystem.py +108 -0
  420. flowtask/storages/tasks/github.py +119 -0
  421. flowtask/storages/tasks/memory.py +45 -0
  422. flowtask/storages/tasks/row.py +25 -0
  423. flowtask/tasks/__init__.py +0 -0
  424. flowtask/tasks/abstract.py +526 -0
  425. flowtask/tasks/command.py +118 -0
  426. flowtask/tasks/pile.py +486 -0
  427. flowtask/tasks/py.typed +0 -0
  428. flowtask/tasks/task.py +778 -0
  429. flowtask/template/__init__.py +161 -0
  430. flowtask/tests.py +257 -0
  431. flowtask/types/__init__.py +8 -0
  432. flowtask/types/typedefs.c +11347 -0
  433. flowtask/types/typedefs.cpython-39-x86_64-linux-gnu.so +0 -0
  434. flowtask/utils/__init__.py +24 -0
  435. flowtask/utils/constants.py +117 -0
  436. flowtask/utils/encoders.py +21 -0
  437. flowtask/utils/executor.py +112 -0
  438. flowtask/utils/functions.cpp +14280 -0
  439. flowtask/utils/functions.cpython-39-x86_64-linux-gnu.so +0 -0
  440. flowtask/utils/json.cpp +13349 -0
  441. flowtask/utils/json.cpython-39-x86_64-linux-gnu.so +0 -0
  442. flowtask/utils/mail.py +63 -0
  443. flowtask/utils/parseqs.c +13324 -0
  444. flowtask/utils/parserqs.cpython-39-x86_64-linux-gnu.so +0 -0
  445. flowtask/utils/stats.py +308 -0
  446. flowtask/utils/transformations.py +74 -0
  447. flowtask/utils/uv.py +12 -0
  448. flowtask/utils/validators.py +97 -0
  449. flowtask/version.py +11 -0
  450. flowtask-5.8.4.dist-info/LICENSE +201 -0
  451. flowtask-5.8.4.dist-info/METADATA +209 -0
  452. flowtask-5.8.4.dist-info/RECORD +470 -0
  453. flowtask-5.8.4.dist-info/WHEEL +6 -0
  454. flowtask-5.8.4.dist-info/entry_points.txt +3 -0
  455. flowtask-5.8.4.dist-info/top_level.txt +2 -0
  456. plugins/components/CreateQR.py +39 -0
  457. plugins/components/TestComponent.py +28 -0
  458. plugins/components/Use1.py +13 -0
  459. plugins/components/Workplace.py +117 -0
  460. plugins/components/__init__.py +3 -0
  461. plugins/sources/__init__.py +0 -0
  462. plugins/sources/get_populartimes.py +78 -0
  463. plugins/sources/google.py +150 -0
  464. plugins/sources/hubspot.py +679 -0
  465. plugins/sources/icims.py +679 -0
  466. plugins/sources/mobileinsight.py +501 -0
  467. plugins/sources/newrelic.py +262 -0
  468. plugins/sources/uap.py +268 -0
  469. plugins/sources/venu.py +244 -0
  470. plugins/sources/vocinity.py +314 -0
@@ -0,0 +1,854 @@
1
+ from collections.abc import Callable
2
+ import asyncio
3
+ import logging
4
+ from datetime import datetime, timezone
5
+ import time
6
+ from typing import List, Union
7
+ import pandas as pd
8
+ import backoff
9
+ from datamodel import BaseModel
10
+ from asyncdb import AsyncDB, AsyncPool
11
+ from datamodel.parsers.json import json_encoder, json_decoder
12
+ from datamodel.exceptions import ValidationError
13
+ from querysource.interfaces.databases.mongo import DocumentDB
14
+ from querysource.conf import default_dsn, async_default_dsn
15
+ from querysource.outputs.tables import PgOutput
16
+ from ...exceptions import (
17
+ ComponentError,
18
+ DataError,
19
+ ConfigError,
20
+ DataNotFound
21
+ )
22
+ from ...components import FlowComponent
23
+ from ...interfaces.http import HTTPService
24
+ from ...conf import (
25
+ NETWORKNINJA_API_KEY,
26
+ NETWORKNINJA_BASE_URL,
27
+ NETWORKNINJA_ENV
28
+ )
29
+ from .models import NetworkNinja_Map, NN_Order
30
+ from .models.abstract import AbstractPayload
31
+
32
+
33
+ logging.getLogger('pymongo').setLevel(logging.INFO)
34
+ logging.getLogger('pymongo.client').setLevel(logging.WARNING)
35
+
36
+
37
+ class EmptyQueue(BaseException):
38
+ """Exception raised when the Queue is empty.
39
+
40
+ Attributes:
41
+ message -- explanation of the error
42
+
43
+
44
+ Example:
45
+
46
+ ```yaml
47
+ NetworkNinja:
48
+ comment: Download Batch from NetworkNinja.
49
+ action: get_batch
50
+ avoid_acceptance: true
51
+ ```
52
+
53
+ """
54
+ pass
55
+
56
+ class NetworkNinja(HTTPService, FlowComponent):
57
+ """
58
+ NetworkNinja.
59
+
60
+ Overview: Router for processing NetworkNinja Payloads.
61
+
62
+ Properties:
63
+ +---------------+----------+------------------------------------------+
64
+ | Name | Required | Description |
65
+ +---------------+----------+------------------------------------------+
66
+ | action | Yes | Type of operation (get_batch, etc) |
67
+ +---------------+----------+------------------------------------------+
68
+ | credentials | No | API credentials (taken from config) |
69
+ +---------------+----------+------------------------------------------+
70
+ | payload | No | Additional payload parameters |
71
+ +---------------+----------+------------------------------------------+
72
+
73
+ Supported Types:
74
+ - get_batch: Retrieves batch acceptance data
75
+ """
76
+ accept: str = 'application/json'
77
+
78
+ def __init__(
79
+ self,
80
+ loop: asyncio.AbstractEventLoop = None,
81
+ job: Callable = None,
82
+ stat: Callable = None,
83
+ **kwargs,
84
+ ) -> None:
85
+ self._action: str = kwargs.pop('action', None)
86
+ self.chunk_size: int = kwargs.get('chunk_size', 100)
87
+ self.use_proxies: bool = kwargs.pop('use_proxies', False)
88
+ self.paid_proxy: bool = kwargs.pop('paid_proxy', False)
89
+ self.base_url = NETWORKNINJA_BASE_URL
90
+ self.avoid_acceptance: bool = kwargs.get('avoid_acceptance', False)
91
+ self.batch_id: Union[str, list] = kwargs.get('batch_id', None)
92
+ super(NetworkNinja, self).__init__(loop=loop, job=job, stat=stat, **kwargs)
93
+ self.semaphore = asyncio.Semaphore(10) # Adjust the limit as needed
94
+ self._model_caching: dict = kwargs.get('model_cache', {})
95
+ self._max_size: int = kwargs.get('max_size', 20)
96
+ self._returning: str = kwargs.get('returning', None)
97
+ self.avoid_insert_of: List[str] = kwargs.get('avoid_insert_of', [])
98
+ self._result: dict = {}
99
+ self._pool: AsyncPool = None
100
+ self.accept = 'application/json'
101
+ # AWS DocumentDB connection:
102
+ self._document = DocumentDB(use_pandas=False)
103
+ self._pgoutput = PgOutput(dsn=async_default_dsn, use_async=True)
104
+
105
+ async def close(self):
106
+ if self._pool:
107
+ await self._pool.close()
108
+
109
+ def _evaluate_input(self):
110
+ if self.previous or self.input is not None:
111
+ self.data = self.input
112
+
113
+ def chunkify(self, lst, n):
114
+ """Split list lst into chunks of size n."""
115
+ for i in range(0, len(lst), n):
116
+ yield lst[i:i + n]
117
+
118
+ async def start(self, **kwargs):
119
+ self._counter: int = 0
120
+ self._evaluate_input()
121
+ if not self._action:
122
+ raise RuntimeError(
123
+ 'NetworkNinja component requires a *action* Function'
124
+ )
125
+ if self._action in {'get_batch', 'get_batches'}:
126
+ # Calling Download Batch from NN Queue.
127
+ # Set up headers with API key
128
+ self.headers.update({
129
+ "X-Api-Key": NETWORKNINJA_API_KEY
130
+ })
131
+ return await super(NetworkNinja, self).start(**kwargs)
132
+ # in other cases, NN requires a previous dataset downloaded.
133
+ if not isinstance(self.data, (dict, list)):
134
+ raise ComponentError(
135
+ "NetworkNinja requires a Dictionary or a List as Payload"
136
+ )
137
+ return True
138
+
139
+ async def run(self):
140
+ """Run NetworkNinja Router."""
141
+ tasks = []
142
+ fn = getattr(self, self._action)
143
+ self.add_metric("ACTION", self._action)
144
+ self._result = {}
145
+ self._pool = AsyncPool('pg', dsn=default_dsn, max_clients=1000)
146
+ await self._pool.connect()
147
+ if self._action == 'get_batch':
148
+ try:
149
+ result = await fn()
150
+ except EmptyQueue:
151
+ result = {}
152
+
153
+ # Check Length of the Payload:
154
+ try:
155
+ if isinstance(result, list):
156
+ num_elements = len(result)
157
+ elif isinstance(result, dict):
158
+ num_elements = len(result.get('data', []))
159
+ except TypeError:
160
+ num_elements = 0
161
+
162
+ # Add metrics
163
+ self.add_metric("PAYLOAD_LENGTH", num_elements)
164
+
165
+ if not result:
166
+ raise DataNotFound(
167
+ "No data returned from Network Ninja"
168
+ )
169
+
170
+ self._result = result
171
+ return self._result
172
+ elif self._action == 'get_batches':
173
+ results = await fn()
174
+ try:
175
+ num_elements = len(results)
176
+ except TypeError:
177
+ num_elements = 0
178
+ self._result = results
179
+ return self._result
180
+ elif self._action == 'process_payload':
181
+ if isinstance(self.data, dict):
182
+ # Typical NN payload extract data from dictionary:
183
+ tasks = [
184
+ fn(
185
+ idx,
186
+ row,
187
+ ) for idx, row in enumerate(self.data.get('data', []))
188
+ ]
189
+ elif isinstance(self.data, list):
190
+ if self.data[0].get('data', []):
191
+ # If Data has "data" entries, we need to extract them
192
+ tasks = []
193
+ for data in self.data:
194
+ for idx, row in enumerate(data.get('data', [])):
195
+ tasks.append(
196
+ fn(
197
+ idx,
198
+ row,
199
+ )
200
+ )
201
+ else:
202
+ # Is directly the result of "get_batches"
203
+ tasks = [
204
+ fn(
205
+ idx,
206
+ row,
207
+ ) for idx, row in enumerate(self.data)
208
+ ]
209
+ elif isinstance(self.data, pd.DataFrame):
210
+ tasks = [
211
+ fn(
212
+ idx,
213
+ row,
214
+ ) for idx, row in self.data.iterrows()
215
+ ]
216
+ # Execute tasks concurrently
217
+ await self._processing_tasks(tasks)
218
+ # Processing and saving the elements into DB:
219
+ self._result = await self._saving_results()
220
+ # Check if the result is empty
221
+ if self._result is None:
222
+ raise DataNotFound(
223
+ "No Forms were returned from Network Ninja"
224
+ )
225
+ if isinstance(self._result, pd.DataFrame) and self._result.empty:
226
+ raise DataNotFound(
227
+ "No Forms were returned from Network Ninja"
228
+ )
229
+ return self._result
230
+
231
+ async def _saving_results(self):
232
+ """Using Concurrency to save results.
233
+ """
234
+ tasks = []
235
+ data_types = []
236
+ results_by_type = {}
237
+ for data_type in NN_Order:
238
+ if data_type in self._result:
239
+ data = self._result[data_type]
240
+ self._logger.notice(
241
+ f"Processing Data Type: {data_type}: {len(data)}"
242
+ )
243
+ try:
244
+ tasks.append(
245
+ self._saving_payload(data_type, data)
246
+ )
247
+ data_types.append(data_type)
248
+ except Exception as e:
249
+ self._logger.error(
250
+ f"Error saving {data_type}: {str(e)}"
251
+ )
252
+ results_by_type[data_type] = None # or some error indicator
253
+ # Execute all saving operations concurrently
254
+ result = await asyncio.gather(*tasks)
255
+ # Create a dictionary mapping data_types to their results
256
+ results_by_type = dict(zip(data_types, result))
257
+ if self._returning:
258
+ return results_by_type.get(self._returning, None)
259
+ else:
260
+ return results_by_type
261
+
262
+ async def _processing_tasks(self, tasks: list) -> pd.DataFrame:
263
+ """Process tasks concurrently."""
264
+ results = []
265
+ for chunk in self.chunkify(tasks, self.chunk_size):
266
+ result = await asyncio.gather(*chunk, return_exceptions=True)
267
+ if result:
268
+ results.extend(result)
269
+ return results
270
+
271
+ async def _process_subtasks(self, tasks: list, batch_size: int = 10) -> None:
272
+ """Process tasks concurrently in batches.
273
+ Args:
274
+ tasks: List of coroutine tasks to execute
275
+ batch_size: Number of tasks to process concurrently in each batch
276
+ Returns:
277
+ List of results from all tasks
278
+ """
279
+ results = []
280
+ for chunk in self.chunkify(tasks, batch_size):
281
+ try:
282
+ result = await asyncio.gather(*chunk, return_exceptions=True)
283
+ if result:
284
+ results.extend(result)
285
+ await asyncio.sleep(0.01) # Yield control to the event loop
286
+ except Exception as e:
287
+ # Log the error but continue with next batches
288
+ logging.error(
289
+ f"Error in batch processing: {e}"
290
+ )
291
+ return results
292
+
293
+ @backoff.on_exception(
294
+ backoff.expo,
295
+ (asyncio.TimeoutError),
296
+ max_tries=2
297
+ )
298
+ async def process_payload(
299
+ self,
300
+ idx,
301
+ row
302
+ ):
303
+ async with self.semaphore:
304
+ # Processing first the Metadata:
305
+ metadata = row.get('metadata', {})
306
+ payload = row.get('payload', {})
307
+ # payload_time = metadata.get('timestamp')
308
+ orgid = metadata.get('orgid', None)
309
+ if not orgid:
310
+ orgid = payload.get('orgid', None)
311
+ # Data Type:
312
+ data_type = metadata.get('type', None)
313
+ if not data_type:
314
+ raise DataError(
315
+ (
316
+ "NetworkNinja: Data Type not found in Metadata"
317
+ f" Current Meta: {metadata}"
318
+ )
319
+ )
320
+ # Creating the Model Instances:
321
+ if data_type not in self._result:
322
+ self._result[data_type] = []
323
+ # Get the Model for the Data Type
324
+ try:
325
+ mdl = NetworkNinja_Map.get(data_type)
326
+ except Exception as e:
327
+ raise DataError(
328
+ f"NetworkNinja: Model not found for Data Type: {data_type}"
329
+ )
330
+ error = None
331
+ try:
332
+ # First: adding client and org to payload:
333
+ payload['orgid'] = orgid
334
+ # Validate the Data
335
+ data = mdl(**dict(payload))
336
+ self._result[data_type].append(data)
337
+ return data, error
338
+ except ValidationError as e:
339
+ self._logger.warning('Error: ==== ', e)
340
+ error = e.payload
341
+ self._logger.warning(
342
+ f'Validation Errors: {e.payload}'
343
+ )
344
+ # TODO: save bad payload to DB
345
+ return None, error
346
+ except Exception as e:
347
+ print(f'Error: {e}')
348
+ error = str(e)
349
+ return None, error
350
+
351
+ @backoff.on_exception(
352
+ backoff.expo,
353
+ (asyncio.TimeoutError),
354
+ max_tries=2
355
+ )
356
+ async def _saving_payload(
357
+ self,
358
+ data_type: str,
359
+ data: list[dict]
360
+ ):
361
+ """
362
+ Save the Payload into the Database.
363
+ """
364
+ async with self.semaphore:
365
+ # Iterate over all keys in data:
366
+ results = None
367
+ if not data:
368
+ return results
369
+ tasks = []
370
+
371
+ async def process_item(item, pk):
372
+ conn = None
373
+ async with await self._pool.acquire() as conn:
374
+ try:
375
+ await item.on_sync(conn)
376
+ except Exception as e:
377
+ self._logger.error(
378
+ f"Error Sync {data_type} item: {str(e)}"
379
+ )
380
+ try:
381
+ await item.save(conn, pk=pk)
382
+ except Exception as e:
383
+ self._logger.error(
384
+ f"DB Error on {item} item: {str(e)}"
385
+ )
386
+ if conn is not None:
387
+ await self._pool.release(conn)
388
+
389
+ async def sync_item(item):
390
+ async with await self._pool.acquire() as conn:
391
+ await item.on_sync(conn)
392
+
393
+ for item in data:
394
+ if data_type in self.avoid_insert_of:
395
+ continue
396
+ if data_type == 'client':
397
+ tasks.append(
398
+ process_item(item, pk=['client_id'])
399
+ )
400
+ elif data_type == 'store':
401
+ tasks.append(process_item(item, pk=['store_number']))
402
+ elif data_type == 'form_metadata':
403
+ item.column_name = str(item.column_name)
404
+ if isinstance(item.orgid, BaseModel):
405
+ item.orgid = item.orgid.orgid
406
+ if isinstance(item.formid, BaseModel):
407
+ # Sync the form with the database
408
+ item.form_name = item.formid.form_name
409
+ item.formid = item.formid.formid
410
+ if isinstance(item.client_id, BaseModel):
411
+ item.client_id = item.client_id.client_id
412
+ tasks.append(
413
+ process_item(item, pk=['client_id', 'column_name', 'formid'])
414
+ )
415
+ elif data_type == 'store_photo':
416
+ # Saving the Store Photo
417
+ item.column_name = str(item.column_name)
418
+ item.question_name = str(item.question_name)
419
+ item.url_parts = json_encoder(
420
+ item.url_parts
421
+ )
422
+ if item.categories:
423
+ item.categories = json_encoder(
424
+ item.categories
425
+ )
426
+ tasks.append(
427
+ process_item(item, pk=['photo_id'])
428
+ )
429
+ elif data_type == 'staffing_user':
430
+ # Saving the Staffing User
431
+ # Converting lists to JSON:
432
+ item.custom_fields = json_encoder(
433
+ item.custom_fields
434
+ )
435
+ # item.client_name = json_encoder(
436
+ # item.client_name
437
+ # )
438
+ tasks.append(
439
+ process_item(item, pk=['user_id'])
440
+ )
441
+ elif data_type == 'user':
442
+ # Saving the User
443
+ tasks.append(
444
+ process_item(item, pk=['user_id'])
445
+ )
446
+ elif data_type == 'form_data':
447
+ # Saving the Form Data
448
+ store = item.store
449
+ store.custom_fields = json_encoder(
450
+ store.custom_fields
451
+ )
452
+ tasks.append(
453
+ sync_item(store)
454
+ )
455
+ # Then, saving the Visitor User:
456
+ try:
457
+ user = item.user_id
458
+ tasks.append(
459
+ process_item(user, pk=['user_id'])
460
+ )
461
+ if user:
462
+ item.user_id = user.user_id
463
+ except (AttributeError, TypeError) as exc:
464
+ self._logger.error(
465
+ f"Failed Saving User with Id ({user}) for Form ID: {item.form_id}, Error: {exc}"
466
+ )
467
+ # Check if there is a previous Form:
468
+ if item.previous_form_id:
469
+ try:
470
+ # swapped:
471
+ item.current_form_id = item.form_id
472
+ # Update the form_id on existing form:
473
+ await item.update_form()
474
+ except Exception as e:
475
+ self._logger.error(
476
+ f"Error swapping form_id: {e}"
477
+ )
478
+ # Then, create the tasks for form responses:
479
+ responses = []
480
+ if item.has_column('field_responses'):
481
+ for custom in item.field_responses:
482
+ custom.form_id = item.form_id
483
+ custom.formid = item.formid
484
+ # Set Client and OrgId:
485
+ custom.client_id = item.client_id
486
+ custom.orgid = item.orgid
487
+ custom.event_id = item.event_id
488
+ responses.append(custom)
489
+ if responses:
490
+ try:
491
+ async with self._pgoutput as conn:
492
+ await conn.upsert_many(
493
+ responses,
494
+ table_name='form_responses',
495
+ schema='networkninja',
496
+ primary_keys=["form_id", "formid", "column_name"],
497
+ )
498
+ except Exception as e:
499
+ self._logger.warning(
500
+ f"Error saving Field Responses: {e}"
501
+ )
502
+ tasks.append(
503
+ process_item(item, pk=['form_id', 'formid'])
504
+ )
505
+ elif data_type == 'form':
506
+ # Sincronize Organization and Client:
507
+ orgid = item.client_id.orgid
508
+ if isinstance(orgid, AbstractPayload):
509
+ tasks.append(
510
+ process_item(orgid, pk=['orgid'])
511
+ )
512
+ client = item.client_id
513
+ client.orgid = orgid
514
+ if isinstance(client, AbstractPayload):
515
+ tasks.append(
516
+ process_item(client, pk=['client_id'])
517
+ )
518
+ # Convert the Question Blocks to JSON:
519
+ item.question_blocks = json_encoder(
520
+ item.question_blocks
521
+ )
522
+ tasks.append(
523
+ process_item(item, pk=['formid'])
524
+ )
525
+ elif data_type == 'event_cico':
526
+ tasks.append(
527
+ process_item(item, pk=['cico_id'])
528
+ )
529
+ elif data_type == 'event':
530
+ tasks.append(
531
+ process_item(item, pk=['event_id'])
532
+ )
533
+ elif data_type in ('retailer', 'store_geography'):
534
+ if data_type == 'retailer':
535
+ tasks.append(
536
+ process_item(item, pk=['account_id'])
537
+ )
538
+ elif data_type == 'store_geography':
539
+ tasks.append(
540
+ process_item(item, pk=['geoid'])
541
+ )
542
+ elif data_type == 'store_type':
543
+ tasks.append(
544
+ process_item(item, pk=['store_type_id'])
545
+ )
546
+ elif data_type == 'project':
547
+ tasks.append(
548
+ process_item(item, pk=['project_id'])
549
+ )
550
+ elif data_type == 'photo_category':
551
+ tasks.append(
552
+ process_item(item, pk=['category_id'])
553
+ )
554
+ elif data_type == 'role':
555
+ tasks.append(
556
+ process_item(item, pk=['role_id'])
557
+ )
558
+ # Now process on_sync operations concurrently
559
+ try:
560
+ await self._process_subtasks(tasks, batch_size=50)
561
+ except Exception as e:
562
+ self._logger.error(
563
+ f"Error in gather for {data_type}: {str(e)}"
564
+ )
565
+ self._logger.debug(
566
+ f"Successfully saved {data_type}: {len(data)} items."
567
+ )
568
+ # Processing the Form Data as a Dataframe:
569
+ if data_type == 'form_data':
570
+ visits = pd.DataFrame(data)
571
+ # Drop Unused columns:
572
+ visits = visits.drop(
573
+ columns=['store_custom_fields'],
574
+ errors='ignore'
575
+ )
576
+
577
+ # Remove duplicate fields from field_responses before exploding
578
+ def clean_field_responses(responses):
579
+ if isinstance(responses, list):
580
+ for response in responses:
581
+ if isinstance(response, dict):
582
+ # Remove fields that will conflict with main columns
583
+ response.pop('orgid', None)
584
+ response.pop('client_id', None)
585
+ response.pop('form_id', None)
586
+ response.pop('formid', None)
587
+ response.pop('event_id', None)
588
+ return responses
589
+ # Explode the field_responses column
590
+ # Apply the cleaning function
591
+ visits['field_responses'] = visits['field_responses'].apply(clean_field_responses)
592
+ visits = visits.explode('field_responses', ignore_index=True)
593
+ # Convert each dict in 'field_responses' into separate columns
594
+ visits = pd.concat(
595
+ [
596
+ visits.drop(['field_responses'], axis=1),
597
+ visits['field_responses'].apply(pd.Series)
598
+ ],
599
+ axis=1
600
+ )
601
+ results = visits
602
+ return results
603
+
604
+ async def get_batches(self):
605
+ # I need to call get_batch until:
606
+ # - raise EmptyQueue (no more batches)
607
+ # - Max lenght (self._max_size) is reached
608
+ # - Error occurs
609
+ results = []
610
+ num_elements = 0
611
+ batches = []
612
+ while True:
613
+ try:
614
+ result = await self.get_batch()
615
+ if not result:
616
+ break
617
+ batch = result.get('data', [])
618
+ try:
619
+ batches.append(result.get('batch_id', None))
620
+ except Exception as e:
621
+ self._logger.warning(
622
+ f"Error getting Batch Id: {e}"
623
+ )
624
+ batch_len = len(batch)
625
+ num_elements += batch_len
626
+ results.extend(batch)
627
+ self._logger.debug(
628
+ f"Batch Length: {batch_len}"
629
+ )
630
+ except EmptyQueue:
631
+ break
632
+ except Exception as e:
633
+ self._logger.error(
634
+ f"Error getting Batch: {e}"
635
+ )
636
+ break
637
+ if num_elements >= self._max_size:
638
+ # We have reached the maximum size
639
+ break
640
+ self.add_metric('BATCHES', batches)
641
+ return results
642
+
643
+ async def get_multi_batches(self):
644
+ """
645
+ Get Multiples batches at once.
646
+ """
647
+ base_url = f"{self.base_url}/{NETWORKNINJA_ENV}/get_batch"
648
+ results = []
649
+ num_elements = 0
650
+ batches = []
651
+ for batch in self.batch_id:
652
+ url = f"{base_url}?batch_id={batch}"
653
+ args = {
654
+ "method": "get",
655
+ "url": url,
656
+ "use_proxy": False,
657
+ "return_response": True,
658
+ }
659
+ response, result, error = await self.session(**args)
660
+ if response.status_code == 204:
661
+ continue
662
+ if not result or error is not None:
663
+ break
664
+ batch = result.get('data', [])
665
+ try:
666
+ batches.append(result.get('batch_id', None))
667
+ except Exception as e:
668
+ self._logger.warning(
669
+ f"Error getting Batch Id: {e}"
670
+ )
671
+ batch_len = len(batch)
672
+ num_elements += batch_len
673
+ results.extend(batch)
674
+ self._logger.debug(
675
+ f"Batch Length: {batch_len}"
676
+ )
677
+ self.add_metric('BATCHES', batches)
678
+ return results
679
+
680
+ async def get_batch(self):
681
+ """Handle get_batch operation type
682
+
683
+ Uses to download a Batch from NetworkNinja SQS Queue.
684
+ """
685
+ url = f"{self.base_url}/{NETWORKNINJA_ENV}/get_batch"
686
+ if isinstance(self.batch_id, list):
687
+ return await self.get_multi_batches()
688
+ if self.batch_id:
689
+ url += f"?batch_id={self.batch_id}"
690
+ self.avoid_acceptance = True # avoid accepting the batch
691
+
692
+ args = {
693
+ "method": "get",
694
+ "url": url,
695
+ "use_proxy": False,
696
+ "return_response": True,
697
+ }
698
+
699
+ response, result, error = await self.session(**args)
700
+ if response.status_code == 204:
701
+ # There is no data to download and is normal.
702
+ raise EmptyQueue(
703
+ "No data to download from NetworkNinja"
704
+ )
705
+
706
+ if error:
707
+ raise ComponentError(
708
+ f"Error calling Network Ninja API: {error}"
709
+ )
710
+
711
+ if not result:
712
+ raise DataNotFound(
713
+ "No data returned from Network Ninja"
714
+ )
715
+
716
+ # Saving Batch for security/auditing purposes in DocumentDB:
717
+ try:
718
+ await self._document.write(
719
+ table='batches',
720
+ schema='networkninja',
721
+ data=[result],
722
+ on_conflict='replace'
723
+ )
724
+ except Exception as e:
725
+ self._logger.error(
726
+ f"Error saving Batch into *batches* auditing catalog: {e}"
727
+ )
728
+
729
+ # Then, extract batch id and report to NN:
730
+ batch_id = result.get('batch_id')
731
+ self._logger.notice(
732
+ f"Batch Id: {batch_id}"
733
+ )
734
+ self.add_metric('LAST_BATCH', batch_id)
735
+ if self.avoid_acceptance is False:
736
+ # Only report the batch if acceptance is enabled.
737
+ await self.report_batch(batch_id)
738
+ return result
739
+
740
+ async def report_batch(self, batch_id: str, report_code: int = 200):
741
+ """Handle report_batch operation type
742
+
743
+ Uses to report a Batch to NetworkNinja SQS Queue.
744
+ """
745
+ url = f"{self.base_url}/{NETWORKNINJA_ENV}/report_batch_activity"
746
+
747
+ payload = {
748
+ "batch_id": batch_id,
749
+ "status_code": report_code
750
+ }
751
+ args = {
752
+ "url": url,
753
+ "use_proxy": False,
754
+ "payload": payload,
755
+ "full_response": True
756
+ }
757
+ result = None
758
+ try:
759
+ response = await self.api_post(**args)
760
+ # TODO: handle error codes (504, 404, etc)
761
+ if response.status_code != 200:
762
+ if response.status_code == 404:
763
+ self._logger.error(
764
+ f"HTTP error: {response.status_code} - Batch {batch_id} not found"
765
+ )
766
+ elif response.status_code == 504:
767
+ self._logger.error(
768
+ f"HTTP error: {response.status_code} - Network Ninja API is unavailable"
769
+ )
770
+ else:
771
+ result = response.json()
772
+ except Exception as e:
773
+ self._logger.error(
774
+ f"Error Reporting Batch with id {batch_id}: {e}"
775
+ )
776
+
777
+ if not result:
778
+ raise DataNotFound(
779
+ "No data returned from Network Ninja"
780
+ )
781
+
782
+ # # Then, saving the acknowledgement in DocumentDB:
783
+ # payload = {
784
+ # "current_timestamp": datetime.now(tz=timezone.utc),
785
+ # **result
786
+ # }
787
+ # try:
788
+ # await self._document.write(
789
+ # table='acknowledgements',
790
+ # schema='networkninja',
791
+ # data=[result],
792
+ # on_conflict='replace'
793
+ # )
794
+ # except Exception as e:
795
+ # self._logger.error(
796
+ # f"Error saving Acknowledgement: {e}"
797
+ # )
798
+ return result
799
+
800
+ async def upsert_record(self, obj: Union[dict, BaseModel], **kwargs):
801
+ # TODO: Discover primary Keys from Model instead from Database.
802
+ if isinstance(obj, BaseModel):
803
+ name = obj.modelName
804
+ pk = self._model_caching.get(name, None)
805
+ if not pk:
806
+ pk = []
807
+ fields = obj.columns()
808
+ for _, field in fields.items():
809
+ if field.primary_key:
810
+ pk.append(field.name)
811
+ self._model_caching[name] = pk
812
+ table_name = obj.Meta.name
813
+ schema = obj.Meta.schema
814
+ else:
815
+ pk = kwargs.get('pk', [])
816
+ table_name = kwargs.get('table_name', None)
817
+ schema = kwargs.get('schema', None)
818
+ async with self._pgoutput as conn:
819
+ await conn.do_upsert(
820
+ obj,
821
+ table_name=table_name,
822
+ schema=schema,
823
+ primary_keys=pk,
824
+ use_conn=conn.get_connection()
825
+ )
826
+ return True
827
+
828
+ async def replace_record(self, obj: Union[dict, BaseModel], **kwargs):
829
+ # TODO: Discover primary Keys from Model instead from Database.
830
+ if isinstance(obj, BaseModel):
831
+ name = obj.modelName
832
+ pk = self._model_caching.get(name, None)
833
+ if not pk:
834
+ pk = []
835
+ fields = obj.columns()
836
+ for _, field in fields.items():
837
+ if field.primary_key:
838
+ pk.append(field.name)
839
+ self._model_caching[name] = pk
840
+ table_name = obj.Meta.name
841
+ schema = obj.Meta.schema
842
+ else:
843
+ pk = kwargs.get('pk', [])
844
+ table_name = kwargs.get('table_name', None)
845
+ schema = kwargs.get('schema', None)
846
+ async with self._pgoutput as conn:
847
+ await conn.do_replace(
848
+ obj,
849
+ table_name=table_name,
850
+ schema=schema,
851
+ primary_keys=pk,
852
+ use_conn=conn.get_connection()
853
+ )
854
+ return True