flowtask 5.8.4__cp310-cp310-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-310-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-310-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-310-x86_64-linux-gnu.so +0 -0
  377. flowtask/parsers/json.c +11968 -0
  378. flowtask/parsers/json.cpython-310-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-310-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-310-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-310-x86_64-linux-gnu.so +0 -0
  440. flowtask/utils/json.cpp +13349 -0
  441. flowtask/utils/json.cpython-310-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-310-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,592 @@
1
+ from collections.abc import Callable
2
+ import asyncio
3
+ from datetime import datetime, date, timedelta
4
+ from pathlib import Path
5
+ from typing import Any
6
+ import numpy as np
7
+ import pandas as pd
8
+ import calendar
9
+ import requests
10
+ from sklearn.neighbors import BallTree
11
+ from geopy.distance import geodesic # For calculating distances
12
+ from ..exceptions import DataNotFound, ComponentError, ConfigError
13
+ from .flow import FlowComponent
14
+
15
+
16
+ # OSRM base URL for routing requests
17
+ OSRM_BASE_URL = "http://router.project-osrm.org"
18
+
19
+ class SchedulingVisits(FlowComponent):
20
+ """Generating the Schedule of Employee Visits with Market Constraints and Visit Cadence.
21
+
22
+ Overview:
23
+ The SchedulingVisits class is a Flowtask component for generating a schedule of employee visits based on
24
+ a set of rules and constraints. This component can be used to optimize the order of visits,
25
+ minimize travel time, and balance workloads across employees.
26
+ The schedule is generated by solving a combinatorial optimization
27
+ problem with support for custom objective functions and constraints.
28
+
29
+ Example of row consumed:
30
+ ```
31
+ associate_oid -> object -> G3Q86F5E1JXN1XVM
32
+ corporate_email -> object -> buko@trocglobal.com
33
+ employee_position -> object -> (3442724.8764311927, -10973885.176252203)
34
+ store_id -> object -> BBY0178
35
+ store_position -> object -> (3564143.804984759, -10887222.41833608)
36
+ market -> object -> Market1
37
+ visit_rule -> int64 -> 2
38
+ visit_frequency -> object -> Monthly
39
+ ```
40
+
41
+ Example of Row Generated:
42
+ ```
43
+
44
+ ```
45
+
46
+
47
+ Example:
48
+
49
+ ```yaml
50
+ SchedulingVisits:
51
+ use_ghost_employees: true
52
+ ghost_employees: 1
53
+ in_store_percentage: 0.6
54
+ in_store_visit: 0.75
55
+ max_stores: 4
56
+ max_distance: 120
57
+ year: 2024
58
+ month: 12
59
+ exception_dates:
60
+ - '2024-12-25'
61
+ exceptions_filename: /home/ubuntu/symbits/Scheduling-Visits-Exceptions.xlsx
62
+ ```
63
+
64
+ """
65
+ def __init__(
66
+ self,
67
+ loop: asyncio.AbstractEventLoop = None,
68
+ job: Callable = None,
69
+ stat: Callable = None,
70
+ **kwargs,
71
+ ):
72
+ # TODO: add support for Masks
73
+ # total hours worked per day
74
+ self.day_duration: float = kwargs.pop('day_duration', 8.0)
75
+ # 60% of the day in store
76
+ self.in_store_percentage: float = kwargs.pop('in_store_percentage', 0.6)
77
+ # near to 45 minutes in store
78
+ self.in_store_visit: float = kwargs.pop('in_store_visit', 0.75)
79
+ self.max_stores: int = kwargs.pop('max_stores', 4)
80
+ # no more than 600 miles covered at day
81
+ self.max_distance: int = kwargs.pop('max_distance', 600)
82
+ # Average Speed:
83
+ self.average_speed: float = kwargs.pop('average_speed', 40)
84
+ # Objective function: minimize total travel time
85
+ self.use_ghost_employees: bool = kwargs.pop('use_ghost_employees', False)
86
+ # Using 3 ghost employees per market if no employees are available.
87
+ self.ghost_employees: int = kwargs.pop('ghost_employees', 3)
88
+ # calculate year and month of current day:
89
+ today = date.today()
90
+ self._today = today
91
+ self.year: int = kwargs.pop('year', today.year)
92
+ self.month: int = kwargs.pop('month', today.month)
93
+ super(SchedulingVisits, self).__init__(
94
+ loop=loop,
95
+ job=job,
96
+ stat=stat,
97
+ **kwargs
98
+ )
99
+ # Ghost Account:
100
+ self._ghost_account = kwargs.pop('ghost_account', 'ghost_{}@company.com')
101
+ # exception days:
102
+ self._exception_dates = kwargs.pop('exception_dates', [])
103
+
104
+ def get_workdays(self, year: int, month: int, exception_dates: list = None):
105
+ """Get all workdays (Monday to Friday) in a given month, excluding exception dates."""
106
+ first_day = date(year, month, 1)
107
+ last_day = date(year, month, calendar.monthrange(year, month)[1])
108
+ workdays = pd.bdate_range(first_day, last_day)
109
+ if exception_dates:
110
+ # Convert exception_dates to datetime
111
+ exception_dates = pd.to_datetime(exception_dates)
112
+ workdays = workdays.difference(exception_dates)
113
+ return workdays
114
+
115
+ def _get_fdom(self, year, month):
116
+ """Function to get the first Monday of a given month
117
+ (which is Labor Day in the US)."""
118
+ cal = calendar.Calendar()
119
+ first_monday = None
120
+ for day in cal.itermonthdays2(year, month):
121
+ if day[0] != 0 and day[1] == 0: # day[1] == 0 means Monday
122
+ first_monday = day[0]
123
+ break
124
+ return datetime(year, month, first_monday)
125
+
126
+ def get_distance(self, coord1, coord2):
127
+ """Function to calculate distance
128
+ between two points (latitude, longitude)."""
129
+ return geodesic(coord1, coord2).miles
130
+
131
+ def to_miles(self, distance) -> float:
132
+ return distance * 0.621371 # Convert to miles
133
+
134
+ def to_hours(self, minutes) -> float:
135
+ return minutes / 60 # Convert to hours
136
+
137
+ def get_labor_days(self, year: int = 2024, month: int = 9):
138
+ """Function to get all workdays (Monday to Friday) in a given month."""
139
+ # Get first Labor Day (first Monday) of the month
140
+ labor_day = self._get_fdom(year, month)
141
+ # Generate list of weekdays (excluding weekends) starting from Labor Day
142
+ workdays = []
143
+ current_day = labor_day
144
+ while current_day.month == month:
145
+ if current_day.weekday() < 5: # Only Monday to Friday (weekday < 5)
146
+ workdays.append(current_day)
147
+ current_day += timedelta(days=1)
148
+ return workdays
149
+
150
+ def get_travel(self, waypoints, transportation: str = 'driving'):
151
+ # Build the request URL for OSRM driving route
152
+ # including all waypoints
153
+ osrm_url = f"{OSRM_BASE_URL}/route/v1/{transportation}/{waypoints}?overview=false"
154
+ # Send the request to OSRM API
155
+ response = requests.get(osrm_url)
156
+ # Check if the request was successful
157
+ if response.status_code == 200:
158
+ route_data = response.json()
159
+ # Extract total travel duration and distance (in seconds and meters)
160
+ # Total duration
161
+ duration_seconds = route_data['routes'][0]['duration']
162
+ # Total distance
163
+ distance_meters = route_data['routes'][0]['distance']
164
+ # Convert to more readable formats
165
+ duration_minutes = duration_seconds / 60
166
+ distance_km = distance_meters / 1000
167
+ return duration_minutes, distance_km
168
+ else:
169
+ return 0, 0
170
+
171
+ def get_travel_duration(self, origin, destination):
172
+ """Helper function to get distance and duration between two points.
173
+ """
174
+ waypoints = f"{origin[1]},{origin[0]};{destination[1]},{destination[0]}"
175
+ duration_minutes, distance_km = self.get_travel(waypoints, transportation='driving')
176
+ distance_miles = self.to_miles(distance_km)
177
+ return distance_miles, duration_minutes
178
+
179
+ def get_scheduled_dates(
180
+ self,
181
+ cadence: str,
182
+ visit_rule: int,
183
+ visit_frequency: Any,
184
+ workdays: pd.DatetimeIndex,
185
+ store_index: int
186
+ ):
187
+ """Given the visit_rule and visit_frequency, return a list of scheduled dates for the visits."""
188
+ scheduled_dates = []
189
+ # Set visit_frequency and visit_rule based on cadence if provided
190
+ if cadence:
191
+ cadence = cadence.lower()
192
+ if 'xweek' in cadence:
193
+ num = int(cadence[0])
194
+ visit_rule = num
195
+ visit_frequency = 'weekly'
196
+ elif 'xmonth' in cadence:
197
+ num = int(cadence[0])
198
+ visit_rule = num
199
+ visit_frequency = 'monthly'
200
+ elif 'xqtr' in cadence:
201
+ num = int(cadence[0])
202
+ visit_rule = num
203
+ visit_frequency = 'quarterly'
204
+
205
+ if visit_frequency.lower() == 'quarterly':
206
+ # For simplicity, schedule as monthly with 1 visit per month
207
+ visit_frequency = 'monthly'
208
+ visit_rule = 1
209
+
210
+ if visit_frequency.lower() == 'weekly':
211
+ # Get the weeks in the month
212
+ workdays_df = pd.DataFrame({'date': workdays})
213
+ workdays_df['week'] = workdays_df['date'].dt.isocalendar().week
214
+ weeks = workdays_df['week'].unique()
215
+ for week in weeks:
216
+ week_days = workdays_df[workdays_df['week'] == week]['date'].reset_index(drop=True)
217
+ num_days = len(week_days)
218
+ # If visit_rule > num_days in week, limit to num_days
219
+ num_visits = min(visit_rule, num_days)
220
+ for i in range(num_visits):
221
+ day_index = (store_index + i) % num_days
222
+ scheduled_date = week_days[day_index]
223
+ scheduled_dates.append(scheduled_date)
224
+ elif visit_frequency.lower() == 'monthly':
225
+ # Visit 'visit_rule' times per month
226
+ total_days = len(workdays)
227
+ if visit_rule == 0:
228
+ visit_rule = 1
229
+ interval = total_days // visit_rule
230
+ for i in range(visit_rule):
231
+ day_index = i * interval
232
+ if day_index >= total_days:
233
+ day_index = total_days - 1
234
+ scheduled_dates.append(workdays[day_index])
235
+ else:
236
+ # Default to monthly
237
+ total_days = len(workdays)
238
+ interval = total_days // visit_rule
239
+ for i in range(visit_rule):
240
+ day_index = i * interval
241
+ if day_index >= total_days:
242
+ day_index = total_days - 1
243
+ scheduled_dates.append(workdays[day_index])
244
+ return scheduled_dates
245
+
246
+ async def start(self, **kwargs):
247
+ if self.previous:
248
+ self.data: pd.DataFrame = self.input
249
+ if not isinstance(self.data, pd.DataFrame):
250
+ raise ConfigError(
251
+ "Incompatible Pandas Dataframe", status=404
252
+ )
253
+ else:
254
+ raise DataNotFound(
255
+ "Data Not Found", status=404
256
+ )
257
+ await super().start(**kwargs)
258
+ # if dataframe doesn't have a store_position attribute
259
+ if 'store_position' not in self.data.columns:
260
+ # Create the store_position column
261
+ self.data['store_position'] = self.data.apply(
262
+ lambda row: (row['latitude'], row['longitude']),
263
+ axis=1
264
+ )
265
+ # Exceptions Filename:
266
+ self._exceptions_file = None
267
+ if hasattr(self, 'exceptions_filename'):
268
+ self._exceptions_file = Path(self.exceptions_filename).resolve()
269
+ return True
270
+
271
+ async def close(self):
272
+ pass
273
+
274
+ async def run(self):
275
+ self._logger.debug('=== RUNNING FUNCTION SCHEDULING VISITS ===')
276
+
277
+ # Get workdays
278
+ workdays = self.get_workdays(self.year, self.month, self._exception_dates)
279
+
280
+ # Initialize a dictionary to keep track of assignments and exceptions
281
+ schedule_rows = []
282
+ exception_rows = []
283
+
284
+ if self.use_ghost_employees or 'associate_oid' not in self.data.columns:
285
+ # Create multiple ghost employees per market
286
+ markets = self.data['market'].unique()
287
+ ghost_employees = {}
288
+ self.data['associate_oid'] = None # Initialize associate_oid column
289
+ for market in markets:
290
+ market_data = self.data[self.data['market'] == market]
291
+ positions = np.array([pos for pos in market_data['store_position']])
292
+ mean_position = positions.mean(axis=0)
293
+ # Create self.ghost_employees ghost employees per market
294
+ ghost_employee_ids = [f'{market}_ghost_{i+1}' for i in range(self.ghost_employees)]
295
+ # Generate unique emails for ghost employees
296
+ ghost_employee_emails = [self._ghost_account.format(i + 1) for i in range(self.ghost_employees)]
297
+ # Generate positions with small variations
298
+ ghost_employee_positions = []
299
+ for i in range(self.ghost_employees):
300
+ # Generate small random offsets in degrees (~50 meters variation)
301
+ # 1 degree latitude ~ 111 km, so 50 meters ~ 0.00045 degrees
302
+ lat_offset = np.random.uniform(-0.00045, 0.00045)
303
+ lon_offset = np.random.uniform(-0.00045, 0.00045)
304
+ ghost_position = (mean_position[0] + lat_offset, mean_position[1] + lon_offset)
305
+ ghost_employee_positions.append(ghost_position)
306
+
307
+ # Assign stores to ghost employees in a round-robin fashion
308
+ market_store_indices = market_data.index
309
+ num_stores = len(market_store_indices)
310
+ for idx, store_idx in enumerate(market_store_indices):
311
+ assigned_employee_index = idx % self.ghost_employees
312
+ assigned_employee_id = ghost_employee_ids[assigned_employee_index]
313
+ self.data.at[store_idx, 'associate_oid'] = assigned_employee_id
314
+
315
+ # Store the email and position for each ghost employee
316
+ for i, assigned_employee_id in enumerate(ghost_employee_ids):
317
+ ghost_employees[assigned_employee_id] = {
318
+ 'position': ghost_employee_positions[i],
319
+ 'email': ghost_employee_emails[i],
320
+ 'market': market
321
+ }
322
+
323
+ # After assigning stores to ghost employees
324
+ store_assignments = self.data.groupby('store_id')['associate_oid'].nunique()
325
+ overlapping_stores = store_assignments[store_assignments > 1]
326
+ if not overlapping_stores.empty:
327
+ print("Stores assigned to multiple employees:")
328
+ print(overlapping_stores)
329
+ else:
330
+ print("All stores uniquely assigned.")
331
+
332
+ # Now group by associate_oid
333
+ employee_groups = self.data.groupby('associate_oid')
334
+
335
+ # Check if employee information is available
336
+ elif 'associate_oid' in self.data.columns:
337
+ employee_groups = self.data.groupby('associate_oid')
338
+ # Group the data by employee
339
+ ghost_employees = {} # Not needed but kept for consistency
340
+ else:
341
+ raise ComponentError("No employee information available.")
342
+
343
+ # Prepare a list to collect scheduled visits
344
+ scheduled_visits = []
345
+
346
+ # Iterate over employees
347
+ for employee_id, employee_data in employee_groups:
348
+ # Get employee information
349
+ employee_info = employee_data.iloc[0]
350
+ if 'corporate_email' in employee_info:
351
+ employee_email = employee_info['corporate_email']
352
+ else:
353
+ # Get from ghost_employees
354
+ employee_email = ghost_employees[employee_id]['email']
355
+ # Get employee position
356
+ if 'employee_position' in employee_info:
357
+ employee_position = employee_info['employee_position']
358
+ else:
359
+ # Get from ghost_employees
360
+ employee_position = ghost_employees[employee_id]['position']
361
+
362
+ # Get unique stores for the employee
363
+ stores = employee_data.drop_duplicates('store_id').reset_index(drop=True)
364
+
365
+ self._logger.notice(
366
+ f"Generating schedule for: {employee_email} ({employee_position}) for {len(stores)} stores."
367
+ )
368
+
369
+ # For each store, generate scheduled visits
370
+ for idx, store_row in stores.iterrows():
371
+ store_id = store_row['store_id']
372
+ store_position = store_row['store_position']
373
+ store_name = store_row.get('store_name', 'Unknown')
374
+ try:
375
+ visit_rule = store_row['visit_rule']
376
+ visit_frequency = store_row['visit_frequency']
377
+ except KeyError:
378
+ visit_rule = 1
379
+ visit_frequency = 'weekly'
380
+ visit_market = store_row['market']
381
+ try:
382
+ cadence = store_row['cadence']
383
+ except KeyError:
384
+ cadence = None
385
+
386
+ # Generate scheduled dates for this store
387
+ # Use store_id hash as the unique identifier
388
+ store_unique_id = hash(store_row['store_id']) % (10 ** 8)
389
+ scheduled_dates = self.get_scheduled_dates(
390
+ cadence,
391
+ visit_rule,
392
+ visit_frequency,
393
+ workdays,
394
+ store_index=store_unique_id
395
+ )
396
+
397
+ for scheduled_date in scheduled_dates:
398
+ scheduled_visit = {
399
+ 'associate_oid': employee_id,
400
+ 'corporate_email': employee_email,
401
+ 'employee_position': employee_position,
402
+ 'store_id': store_id,
403
+ 'store_name': store_name,
404
+ 'market': visit_market,
405
+ 'store_position': store_position,
406
+ 'scheduled_date': scheduled_date,
407
+ 'visit_rule': visit_rule,
408
+ 'visit_frequency': visit_frequency
409
+ }
410
+ scheduled_visits.append(scheduled_visit)
411
+
412
+ # Convert scheduled visits to DataFrame
413
+ scheduled_visits_df = pd.DataFrame(scheduled_visits)
414
+
415
+ # Ensure that 'scheduled_date' is of datetime type
416
+ scheduled_visits_df['scheduled_date'] = pd.to_datetime(scheduled_visits_df['scheduled_date'])
417
+ scheduled_visits_df['week'] = scheduled_visits_df['scheduled_date'].dt.isocalendar().week
418
+ duplicates = scheduled_visits_df.duplicated(subset=['store_id', 'week'], keep=False)
419
+ duplicate_visits = scheduled_visits_df[duplicates]
420
+ if not duplicate_visits.empty:
421
+ print("Duplicate visits found for the same store in the same week:")
422
+ print(duplicate_visits)
423
+ else:
424
+ print("No duplicate visits found.")
425
+
426
+ # Group the scheduled visits by associate_oid
427
+ employee_scheduled_visits = scheduled_visits_df.groupby('associate_oid')
428
+
429
+ for employee_id, employee_visits in employee_scheduled_visits:
430
+ # Get employee information
431
+ employee_info = employee_visits.iloc[0]
432
+ employee_email = employee_info['corporate_email']
433
+ employee_position = employee_info['employee_position']
434
+ market = employee_info['market']
435
+
436
+ # Group visits by week
437
+ employee_visits['week'] = employee_visits['scheduled_date'].dt.isocalendar().week
438
+ visits_by_week = employee_visits.groupby('week')
439
+
440
+ # Group workdays by week
441
+ workdays_series = pd.Series(workdays)
442
+ workdays_series.index = workdays_series.dt.isocalendar().week
443
+ workdays_by_week = workdays_series.groupby(level=0)
444
+
445
+ for week_number, week_visits in visits_by_week:
446
+ # Get the workdays for this week
447
+ if week_number in workdays_by_week.groups:
448
+ week_workdays = workdays_by_week.get_group(week_number).values
449
+ else:
450
+ continue # No workdays in this week
451
+
452
+ # Initialize day schedules for each day in the week
453
+ day_schedules = {}
454
+ for day in week_workdays:
455
+ day_schedules[day] = {
456
+ 'associate_oid': employee_id,
457
+ 'corporate_email': employee_email,
458
+ 'start_position': employee_position,
459
+ 'market': market,
460
+ 'day': day,
461
+ 'month': self.month,
462
+ 'year': self.year,
463
+ 'total_time_minutes': 0,
464
+ 'total_time_hours': 0,
465
+ 'total_in_store_time': 0,
466
+ 'total_travel_time': 0,
467
+ 'total_distance': 0,
468
+ 'stores_visited_count': 0,
469
+ 'visited_stores': {},
470
+ 'store_ids': []
471
+ }
472
+
473
+ unvisited_stores = week_visits.copy().reset_index(drop=True)
474
+ unvisited_store_reasons = {}
475
+
476
+ for idx, store_row in unvisited_stores.iterrows():
477
+ store_id = store_row['store_id']
478
+ scheduled_date = store_row['scheduled_date']
479
+ scheduled_dates_to_try = [day for day in [scheduled_date] + list(week_workdays) if day in day_schedules]
480
+ store_scheduled = False
481
+ reasons_for_store = []
482
+
483
+ for day in scheduled_dates_to_try:
484
+
485
+ if day not in day_schedules:
486
+ reasons_for_store.append(
487
+ f'Day {day} is not in day_schedules'
488
+ )
489
+ continue # Skip days not in day_schedules
490
+
491
+ day_schedule = day_schedules[day]
492
+ # Check if day has capacity
493
+ if day_schedule['stores_visited_count'] >= self.max_stores:
494
+ reasons_for_store.append(
495
+ f'Day {day}: Max stores reached'
496
+ )
497
+ continue # Day is full
498
+
499
+ # Set current position to the last store visited on the day, or employee_position
500
+ if day_schedule['stores_visited_count'] > 0:
501
+ last_store = list(day_schedule['visited_stores'].values())[-1]
502
+ current_position = (last_store['latitude'], last_store['longitude'])
503
+ else:
504
+ current_position = employee_position
505
+
506
+ # Calculate distance and time to this store
507
+ distance_miles = self.get_distance(current_position, store_row['store_position'])
508
+ travel_time = (distance_miles / self.average_speed) * 60 # in minutes
509
+ time_in_store = self.in_store_visit * 60 # in minutes
510
+ potential_total_time = day_schedule['total_time_minutes'] + travel_time + time_in_store
511
+
512
+ # Check constraints
513
+ if potential_total_time > self.day_duration * 60:
514
+ reasons_for_store.append(
515
+ f'Day {day}: Exceeds day duration'
516
+ )
517
+ continue # Cannot schedule on this day due to time constraint
518
+ if day_schedule['total_distance'] + distance_miles > self.max_distance:
519
+ reasons_for_store.append(
520
+ f'Day {day} with distance {distance_miles} in greater than {self.max_distance}'
521
+ )
522
+ continue # Cannot schedule on this day due to distance constraint
523
+
524
+ # Schedule the store
525
+ day_schedule['total_time_minutes'] = potential_total_time
526
+ day_schedule['total_distance'] += distance_miles
527
+ day_schedule['total_in_store_time'] += time_in_store
528
+ day_schedule['total_travel_time'] += travel_time
529
+ day_schedule['stores_visited_count'] += 1
530
+ day_schedule['visited_stores'][store_id] = {
531
+ 'store_id': store_id,
532
+ 'store_name': store_row.get('store_name', 'Unknown'),
533
+ 'latitude': store_row['store_position'][0],
534
+ 'longitude': store_row['store_position'][1],
535
+ 'visit_rule': store_row.get('visit_rule', None),
536
+ 'visit_frequency': store_row.get('visit_frequency', None),
537
+ 'market': store_row['market']
538
+ }
539
+ day_schedule['store_ids'].append(store_id)
540
+ store_scheduled = True
541
+ break # Break the loop over days, since store is scheduled
542
+
543
+ if not store_scheduled:
544
+ # Record the reasons
545
+ unvisited_store_reasons[store_id] = reasons_for_store
546
+ # Add to exception_rows
547
+ reason = '; '.join(reasons_for_store) if reasons_for_store else 'Could not schedule on any day'
548
+ exception_row = {
549
+ 'associate_oid': employee_id,
550
+ 'corporate_email': employee_email,
551
+ 'market': store_row['market'],
552
+ 'year': self.year,
553
+ 'month': self.month,
554
+ 'store_id': store_id,
555
+ 'store_name': store_row.get('store_name', 'Unknown'),
556
+ 'store_position': store_row['store_position'],
557
+ 'scheduled_date': store_row['scheduled_date'],
558
+ 'reason': reason
559
+ }
560
+ exception_rows.append(exception_row)
561
+
562
+ # After attempting to schedule all stores
563
+ # Add day_schedules to schedule_rows
564
+ for day_schedule in day_schedules.values():
565
+ if day_schedule['stores_visited_count'] > 0:
566
+ day_schedule['total_time_hours'] = day_schedule['total_time_minutes'] / 60
567
+ schedule_rows.append(day_schedule)
568
+ # ============
569
+ # Save the schedule and exceptions
570
+ schedule_df = pd.DataFrame(schedule_rows)
571
+ exception_stores_df = pd.DataFrame(exception_rows)
572
+
573
+ # Set the final results
574
+ self.schedule_df = schedule_df
575
+ self.exception_stores_df = exception_stores_df
576
+
577
+ print(' === Visit Schedule === ')
578
+ print(schedule_df.head())
579
+ print('=== Exception Stores ====')
580
+ print(exception_stores_df.head())
581
+ # Saving the Exception Stores Dataframe to filesystem:
582
+ if self._exceptions_file:
583
+ if self._exceptions_file.suffix == '.xlsx':
584
+ exception_stores_df.to_excel(self._exceptions_file, index=False)
585
+ else:
586
+ exception_stores_df.to_csv(self._exceptions_file, index=False)
587
+
588
+ self._result = schedule_df
589
+
590
+ self._print_data_(self._result, 'Schedule')
591
+
592
+ return self._result