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.
- flowtask/__init__.py +93 -0
- flowtask/__main__.py +38 -0
- flowtask/bots/__init__.py +6 -0
- flowtask/bots/check.py +93 -0
- flowtask/bots/codebot.py +51 -0
- flowtask/components/ASPX.py +148 -0
- flowtask/components/AddDataset.py +352 -0
- flowtask/components/Amazon.py +523 -0
- flowtask/components/AutoTask.py +314 -0
- flowtask/components/Azure.py +80 -0
- flowtask/components/AzureUsers.py +106 -0
- flowtask/components/BaseAction.py +91 -0
- flowtask/components/BaseLoop.py +198 -0
- flowtask/components/BestBuy.py +800 -0
- flowtask/components/CSVToGCS.py +120 -0
- flowtask/components/CompanyScraper/__init__.py +1 -0
- flowtask/components/CompanyScraper/parsers/__init__.py +6 -0
- flowtask/components/CompanyScraper/parsers/base.py +102 -0
- flowtask/components/CompanyScraper/parsers/explorium.py +192 -0
- flowtask/components/CompanyScraper/parsers/leadiq.py +206 -0
- flowtask/components/CompanyScraper/parsers/rocket.py +133 -0
- flowtask/components/CompanyScraper/parsers/siccode.py +109 -0
- flowtask/components/CompanyScraper/parsers/visualvisitor.py +130 -0
- flowtask/components/CompanyScraper/parsers/zoominfo.py +118 -0
- flowtask/components/CompanyScraper/scrapper.py +1054 -0
- flowtask/components/CopyTo.py +177 -0
- flowtask/components/CopyToBigQuery.py +243 -0
- flowtask/components/CopyToMongoDB.py +291 -0
- flowtask/components/CopyToPg.py +609 -0
- flowtask/components/CopyToRethink.py +207 -0
- flowtask/components/CreateGCSBucket.py +102 -0
- flowtask/components/CreateReport/CreateReport.py +228 -0
- flowtask/components/CreateReport/__init__.py +9 -0
- flowtask/components/CreateReport/charts/__init__.py +15 -0
- flowtask/components/CreateReport/charts/bar.py +51 -0
- flowtask/components/CreateReport/charts/base.py +66 -0
- flowtask/components/CreateReport/charts/pie.py +64 -0
- flowtask/components/CreateReport/utils.py +9 -0
- flowtask/components/CustomerSatisfaction.py +196 -0
- flowtask/components/DataInput.py +200 -0
- flowtask/components/DateList.py +255 -0
- flowtask/components/DbClient.py +163 -0
- flowtask/components/DialPad.py +146 -0
- flowtask/components/DocumentDBQuery.py +200 -0
- flowtask/components/DownloadFrom.py +371 -0
- flowtask/components/DownloadFromD2L.py +113 -0
- flowtask/components/DownloadFromFTP.py +181 -0
- flowtask/components/DownloadFromIMAP.py +315 -0
- flowtask/components/DownloadFromS3.py +198 -0
- flowtask/components/DownloadFromSFTP.py +265 -0
- flowtask/components/DownloadFromSharepoint.py +110 -0
- flowtask/components/DownloadFromSmartSheet.py +114 -0
- flowtask/components/DownloadS3File.py +229 -0
- flowtask/components/Dummy.py +59 -0
- flowtask/components/DuplicatePhoto.py +411 -0
- flowtask/components/EmployeeEvaluation.py +237 -0
- flowtask/components/ExecuteSQL.py +323 -0
- flowtask/components/ExtractHTML.py +178 -0
- flowtask/components/FileBase.py +178 -0
- flowtask/components/FileCopy.py +181 -0
- flowtask/components/FileDelete.py +82 -0
- flowtask/components/FileExists.py +146 -0
- flowtask/components/FileIteratorDelete.py +112 -0
- flowtask/components/FileList.py +194 -0
- flowtask/components/FileOpen.py +75 -0
- flowtask/components/FileRead.py +120 -0
- flowtask/components/FileRename.py +106 -0
- flowtask/components/FilterIf.py +284 -0
- flowtask/components/FilterRows/FilterRows.py +200 -0
- flowtask/components/FilterRows/__init__.py +10 -0
- flowtask/components/FilterRows/functions.py +4 -0
- flowtask/components/GCSToBigQuery.py +103 -0
- flowtask/components/GoogleA4.py +150 -0
- flowtask/components/GoogleGeoCoding.py +344 -0
- flowtask/components/GooglePlaces.py +315 -0
- flowtask/components/GoogleSearch.py +539 -0
- flowtask/components/HTTPClient.py +268 -0
- flowtask/components/ICIMS.py +146 -0
- flowtask/components/IF.py +179 -0
- flowtask/components/IcimsFolderCopy.py +173 -0
- flowtask/components/ImageFeatures/__init__.py +5 -0
- flowtask/components/ImageFeatures/process.py +233 -0
- flowtask/components/IteratorBase.py +251 -0
- flowtask/components/LangchainLoader/__init__.py +5 -0
- flowtask/components/LangchainLoader/loader.py +194 -0
- flowtask/components/LangchainLoader/loaders/__init__.py +22 -0
- flowtask/components/LangchainLoader/loaders/abstract.py +362 -0
- flowtask/components/LangchainLoader/loaders/basepdf.py +50 -0
- flowtask/components/LangchainLoader/loaders/docx.py +91 -0
- flowtask/components/LangchainLoader/loaders/html.py +119 -0
- flowtask/components/LangchainLoader/loaders/pdfblocks.py +146 -0
- flowtask/components/LangchainLoader/loaders/pdfmark.py +79 -0
- flowtask/components/LangchainLoader/loaders/pdftables.py +135 -0
- flowtask/components/LangchainLoader/loaders/qa.py +67 -0
- flowtask/components/LangchainLoader/loaders/txt.py +55 -0
- flowtask/components/LeadIQ.py +650 -0
- flowtask/components/Loop.py +253 -0
- flowtask/components/Lowes.py +334 -0
- flowtask/components/MS365Usage.py +156 -0
- flowtask/components/MSTeamsMessages.py +320 -0
- flowtask/components/MarketClustering.py +1051 -0
- flowtask/components/MergeFiles.py +362 -0
- flowtask/components/MilvusOutput.py +87 -0
- flowtask/components/NearByStores.py +175 -0
- flowtask/components/NetworkNinja/__init__.py +6 -0
- flowtask/components/NetworkNinja/models/__init__.py +52 -0
- flowtask/components/NetworkNinja/models/abstract.py +177 -0
- flowtask/components/NetworkNinja/models/account.py +39 -0
- flowtask/components/NetworkNinja/models/client.py +19 -0
- flowtask/components/NetworkNinja/models/district.py +14 -0
- flowtask/components/NetworkNinja/models/events.py +101 -0
- flowtask/components/NetworkNinja/models/forms.py +499 -0
- flowtask/components/NetworkNinja/models/market.py +16 -0
- flowtask/components/NetworkNinja/models/organization.py +34 -0
- flowtask/components/NetworkNinja/models/photos.py +125 -0
- flowtask/components/NetworkNinja/models/project.py +44 -0
- flowtask/components/NetworkNinja/models/region.py +28 -0
- flowtask/components/NetworkNinja/models/store.py +203 -0
- flowtask/components/NetworkNinja/models/user.py +151 -0
- flowtask/components/NetworkNinja/router.py +854 -0
- flowtask/components/Odoo.py +175 -0
- flowtask/components/OdooInjector.py +192 -0
- flowtask/components/OpenFromXML.py +126 -0
- flowtask/components/OpenWeather.py +41 -0
- flowtask/components/OpenWithBase.py +616 -0
- flowtask/components/OpenWithPandas.py +715 -0
- flowtask/components/PGPDecrypt.py +199 -0
- flowtask/components/PandasIterator.py +187 -0
- flowtask/components/PandasToFile.py +189 -0
- flowtask/components/Paradox.py +339 -0
- flowtask/components/ParamIterator.py +117 -0
- flowtask/components/ParseHTML.py +84 -0
- flowtask/components/PlacerStores.py +249 -0
- flowtask/components/Pokemon.py +507 -0
- flowtask/components/PositiveBot.py +62 -0
- flowtask/components/PowerPointSlide.py +400 -0
- flowtask/components/PrintMessage.py +127 -0
- flowtask/components/ProductCompetitors/__init__.py +5 -0
- flowtask/components/ProductCompetitors/parsers/__init__.py +7 -0
- flowtask/components/ProductCompetitors/parsers/base.py +72 -0
- flowtask/components/ProductCompetitors/parsers/bestbuy.py +86 -0
- flowtask/components/ProductCompetitors/parsers/lowes.py +103 -0
- flowtask/components/ProductCompetitors/scrapper.py +155 -0
- flowtask/components/ProductCompliant.py +169 -0
- flowtask/components/ProductInfo/__init__.py +1 -0
- flowtask/components/ProductInfo/parsers/__init__.py +5 -0
- flowtask/components/ProductInfo/parsers/base.py +83 -0
- flowtask/components/ProductInfo/parsers/brother.py +97 -0
- flowtask/components/ProductInfo/parsers/canon.py +167 -0
- flowtask/components/ProductInfo/parsers/epson.py +118 -0
- flowtask/components/ProductInfo/parsers/hp.py +131 -0
- flowtask/components/ProductInfo/parsers/samsung.py +97 -0
- flowtask/components/ProductInfo/scraper.py +319 -0
- flowtask/components/ProductPricing.py +118 -0
- flowtask/components/QS.py +261 -0
- flowtask/components/QSBase.py +201 -0
- flowtask/components/QueryIterator.py +273 -0
- flowtask/components/QueryToInsert.py +327 -0
- flowtask/components/QueryToPandas.py +432 -0
- flowtask/components/RESTClient.py +195 -0
- flowtask/components/RethinkDBQuery.py +189 -0
- flowtask/components/Rsync.py +74 -0
- flowtask/components/RunSSH.py +59 -0
- flowtask/components/RunShell.py +71 -0
- flowtask/components/SalesForce.py +20 -0
- flowtask/components/SaveImageBank/__init__.py +257 -0
- flowtask/components/SchedulingVisits.py +592 -0
- flowtask/components/ScrapPage.py +216 -0
- flowtask/components/ScrapSearch.py +79 -0
- flowtask/components/SendNotify.py +257 -0
- flowtask/components/SentimentAnalysis.py +694 -0
- flowtask/components/ServiceScrapper/__init__.py +5 -0
- flowtask/components/ServiceScrapper/parsers/__init__.py +1 -0
- flowtask/components/ServiceScrapper/parsers/base.py +94 -0
- flowtask/components/ServiceScrapper/parsers/costco.py +93 -0
- flowtask/components/ServiceScrapper/scrapper.py +199 -0
- flowtask/components/SetVariables.py +156 -0
- flowtask/components/SubTask.py +182 -0
- flowtask/components/SuiteCRM.py +48 -0
- flowtask/components/Switch.py +175 -0
- flowtask/components/TableBase.py +148 -0
- flowtask/components/TableDelete.py +312 -0
- flowtask/components/TableInput.py +143 -0
- flowtask/components/TableOutput/TableOutput.py +384 -0
- flowtask/components/TableOutput/__init__.py +3 -0
- flowtask/components/TableSchema.py +534 -0
- flowtask/components/Target.py +223 -0
- flowtask/components/ThumbnailGenerator.py +156 -0
- flowtask/components/ToPandas.py +67 -0
- flowtask/components/TransformRows/TransformRows.py +507 -0
- flowtask/components/TransformRows/__init__.py +9 -0
- flowtask/components/TransformRows/functions.py +559 -0
- flowtask/components/TransposeRows.py +176 -0
- flowtask/components/UPCDatabase.py +86 -0
- flowtask/components/UnGzip.py +171 -0
- flowtask/components/Uncompress.py +172 -0
- flowtask/components/UniqueRows.py +126 -0
- flowtask/components/Unzip.py +107 -0
- flowtask/components/UpdateOperationalVars.py +147 -0
- flowtask/components/UploadTo.py +299 -0
- flowtask/components/UploadToS3.py +136 -0
- flowtask/components/UploadToSFTP.py +160 -0
- flowtask/components/UploadToSharepoint.py +205 -0
- flowtask/components/UserFunc.py +122 -0
- flowtask/components/VivaTracker.py +140 -0
- flowtask/components/WSDLClient.py +123 -0
- flowtask/components/Wait.py +18 -0
- flowtask/components/Walmart.py +199 -0
- flowtask/components/Workplace.py +134 -0
- flowtask/components/XMLToPandas.py +267 -0
- flowtask/components/Zammad/__init__.py +41 -0
- flowtask/components/Zammad/models.py +0 -0
- flowtask/components/ZoomInfoScraper.py +409 -0
- flowtask/components/__init__.py +104 -0
- flowtask/components/abstract.py +18 -0
- flowtask/components/flow.py +530 -0
- flowtask/components/google.py +335 -0
- flowtask/components/group.py +221 -0
- flowtask/components/py.typed +0 -0
- flowtask/components/reviewscrap.py +132 -0
- flowtask/components/tAutoincrement.py +117 -0
- flowtask/components/tConcat.py +109 -0
- flowtask/components/tExplode.py +119 -0
- flowtask/components/tFilter.py +184 -0
- flowtask/components/tGroup.py +236 -0
- flowtask/components/tJoin.py +270 -0
- flowtask/components/tMap/__init__.py +9 -0
- flowtask/components/tMap/functions.py +54 -0
- flowtask/components/tMap/tMap.py +450 -0
- flowtask/components/tMelt.py +112 -0
- flowtask/components/tMerge.py +114 -0
- flowtask/components/tOrder.py +93 -0
- flowtask/components/tPandas.py +94 -0
- flowtask/components/tPivot.py +71 -0
- flowtask/components/tPluckCols.py +76 -0
- flowtask/components/tUnnest.py +82 -0
- flowtask/components/user.py +401 -0
- flowtask/conf.py +457 -0
- flowtask/download.py +102 -0
- flowtask/events/__init__.py +11 -0
- flowtask/events/events/__init__.py +20 -0
- flowtask/events/events/abstract.py +95 -0
- flowtask/events/events/alerts/__init__.py +362 -0
- flowtask/events/events/alerts/colfunctions.py +131 -0
- flowtask/events/events/alerts/functions.py +158 -0
- flowtask/events/events/dummy.py +12 -0
- flowtask/events/events/exec.py +124 -0
- flowtask/events/events/file/__init__.py +7 -0
- flowtask/events/events/file/base.py +51 -0
- flowtask/events/events/file/copy.py +23 -0
- flowtask/events/events/file/delete.py +16 -0
- flowtask/events/events/interfaces/__init__.py +9 -0
- flowtask/events/events/interfaces/client.py +67 -0
- flowtask/events/events/interfaces/credentials.py +28 -0
- flowtask/events/events/interfaces/notifications.py +58 -0
- flowtask/events/events/jira.py +122 -0
- flowtask/events/events/log.py +26 -0
- flowtask/events/events/logerr.py +52 -0
- flowtask/events/events/notify.py +59 -0
- flowtask/events/events/notify_event.py +160 -0
- flowtask/events/events/publish.py +54 -0
- flowtask/events/events/sendfile.py +104 -0
- flowtask/events/events/task.py +97 -0
- flowtask/events/events/teams.py +98 -0
- flowtask/events/events/webhook.py +58 -0
- flowtask/events/manager.py +287 -0
- flowtask/exceptions.c +39393 -0
- flowtask/exceptions.cpython-310-x86_64-linux-gnu.so +0 -0
- flowtask/extensions/__init__.py +3 -0
- flowtask/extensions/abstract.py +82 -0
- flowtask/extensions/logging/__init__.py +65 -0
- flowtask/hooks/__init__.py +9 -0
- flowtask/hooks/actions/__init__.py +22 -0
- flowtask/hooks/actions/abstract.py +66 -0
- flowtask/hooks/actions/dummy.py +23 -0
- flowtask/hooks/actions/jira.py +74 -0
- flowtask/hooks/actions/rest.py +320 -0
- flowtask/hooks/actions/sampledata.py +37 -0
- flowtask/hooks/actions/sensor.py +23 -0
- flowtask/hooks/actions/task.py +9 -0
- flowtask/hooks/actions/ticket.py +37 -0
- flowtask/hooks/actions/zammad.py +55 -0
- flowtask/hooks/hook.py +62 -0
- flowtask/hooks/models.py +17 -0
- flowtask/hooks/service.py +187 -0
- flowtask/hooks/step.py +91 -0
- flowtask/hooks/types/__init__.py +23 -0
- flowtask/hooks/types/base.py +129 -0
- flowtask/hooks/types/brokers/__init__.py +11 -0
- flowtask/hooks/types/brokers/base.py +54 -0
- flowtask/hooks/types/brokers/mqtt.py +35 -0
- flowtask/hooks/types/brokers/rabbitmq.py +82 -0
- flowtask/hooks/types/brokers/redis.py +83 -0
- flowtask/hooks/types/brokers/sqs.py +44 -0
- flowtask/hooks/types/fs.py +232 -0
- flowtask/hooks/types/http.py +49 -0
- flowtask/hooks/types/imap.py +200 -0
- flowtask/hooks/types/jira.py +279 -0
- flowtask/hooks/types/mail.py +205 -0
- flowtask/hooks/types/postgres.py +98 -0
- flowtask/hooks/types/responses/__init__.py +8 -0
- flowtask/hooks/types/responses/base.py +5 -0
- flowtask/hooks/types/sharepoint.py +288 -0
- flowtask/hooks/types/ssh.py +141 -0
- flowtask/hooks/types/tagged.py +59 -0
- flowtask/hooks/types/upload.py +85 -0
- flowtask/hooks/types/watch.py +71 -0
- flowtask/hooks/types/web.py +36 -0
- flowtask/interfaces/AzureClient.py +137 -0
- flowtask/interfaces/AzureGraph.py +839 -0
- flowtask/interfaces/Boto3Client.py +326 -0
- flowtask/interfaces/DropboxClient.py +173 -0
- flowtask/interfaces/ExcelHandler.py +94 -0
- flowtask/interfaces/FTPClient.py +131 -0
- flowtask/interfaces/GoogleCalendar.py +201 -0
- flowtask/interfaces/GoogleClient.py +133 -0
- flowtask/interfaces/GoogleDrive.py +127 -0
- flowtask/interfaces/GoogleGCS.py +89 -0
- flowtask/interfaces/GoogleGeocoding.py +93 -0
- flowtask/interfaces/GoogleLang.py +114 -0
- flowtask/interfaces/GooglePub.py +61 -0
- flowtask/interfaces/GoogleSheet.py +68 -0
- flowtask/interfaces/IMAPClient.py +137 -0
- flowtask/interfaces/O365Calendar.py +113 -0
- flowtask/interfaces/O365Client.py +220 -0
- flowtask/interfaces/OneDrive.py +284 -0
- flowtask/interfaces/Outlook.py +155 -0
- flowtask/interfaces/ParrotBot.py +130 -0
- flowtask/interfaces/SSHClient.py +378 -0
- flowtask/interfaces/Sharepoint.py +496 -0
- flowtask/interfaces/__init__.py +36 -0
- flowtask/interfaces/azureauth.py +119 -0
- flowtask/interfaces/cache.py +201 -0
- flowtask/interfaces/client.py +82 -0
- flowtask/interfaces/compress.py +525 -0
- flowtask/interfaces/credentials.py +124 -0
- flowtask/interfaces/d2l.py +239 -0
- flowtask/interfaces/databases/__init__.py +5 -0
- flowtask/interfaces/databases/db.py +223 -0
- flowtask/interfaces/databases/documentdb.py +55 -0
- flowtask/interfaces/databases/rethink.py +39 -0
- flowtask/interfaces/dataframes/__init__.py +11 -0
- flowtask/interfaces/dataframes/abstract.py +21 -0
- flowtask/interfaces/dataframes/arrow.py +71 -0
- flowtask/interfaces/dataframes/dt.py +69 -0
- flowtask/interfaces/dataframes/pandas.py +167 -0
- flowtask/interfaces/dataframes/polars.py +60 -0
- flowtask/interfaces/db.py +263 -0
- flowtask/interfaces/env.py +46 -0
- flowtask/interfaces/func.py +137 -0
- flowtask/interfaces/http.py +1780 -0
- flowtask/interfaces/locale.py +40 -0
- flowtask/interfaces/log.py +75 -0
- flowtask/interfaces/mask.py +143 -0
- flowtask/interfaces/notification.py +154 -0
- flowtask/interfaces/playwright.py +339 -0
- flowtask/interfaces/powerpoint.py +368 -0
- flowtask/interfaces/py.typed +0 -0
- flowtask/interfaces/qs.py +376 -0
- flowtask/interfaces/result.py +87 -0
- flowtask/interfaces/selenium_service.py +779 -0
- flowtask/interfaces/smartsheet.py +154 -0
- flowtask/interfaces/stat.py +39 -0
- flowtask/interfaces/task.py +96 -0
- flowtask/interfaces/template.py +118 -0
- flowtask/interfaces/vectorstores/__init__.py +1 -0
- flowtask/interfaces/vectorstores/abstract.py +133 -0
- flowtask/interfaces/vectorstores/milvus.py +669 -0
- flowtask/interfaces/zammad.py +107 -0
- flowtask/models.py +193 -0
- flowtask/parsers/__init__.py +15 -0
- flowtask/parsers/_yaml.c +11978 -0
- flowtask/parsers/_yaml.cpython-310-x86_64-linux-gnu.so +0 -0
- flowtask/parsers/argparser.py +235 -0
- flowtask/parsers/base.c +15155 -0
- flowtask/parsers/base.cpython-310-x86_64-linux-gnu.so +0 -0
- flowtask/parsers/json.c +11968 -0
- flowtask/parsers/json.cpython-310-x86_64-linux-gnu.so +0 -0
- flowtask/parsers/maps.py +49 -0
- flowtask/parsers/toml.c +11968 -0
- flowtask/parsers/toml.cpython-310-x86_64-linux-gnu.so +0 -0
- flowtask/plugins/__init__.py +16 -0
- flowtask/plugins/components/__init__.py +0 -0
- flowtask/plugins/handler/__init__.py +45 -0
- flowtask/plugins/importer.py +31 -0
- flowtask/plugins/sources/__init__.py +0 -0
- flowtask/runner.py +283 -0
- flowtask/scheduler/__init__.py +9 -0
- flowtask/scheduler/functions.py +493 -0
- flowtask/scheduler/handlers/__init__.py +8 -0
- flowtask/scheduler/handlers/manager.py +504 -0
- flowtask/scheduler/handlers/models.py +58 -0
- flowtask/scheduler/handlers/service.py +72 -0
- flowtask/scheduler/notifications.py +65 -0
- flowtask/scheduler/scheduler.py +993 -0
- flowtask/services/__init__.py +0 -0
- flowtask/services/bots/__init__.py +0 -0
- flowtask/services/bots/telegram.py +264 -0
- flowtask/services/files/__init__.py +11 -0
- flowtask/services/files/manager.py +522 -0
- flowtask/services/files/model.py +37 -0
- flowtask/services/files/service.py +767 -0
- flowtask/services/jira/__init__.py +3 -0
- flowtask/services/jira/jira_actions.py +191 -0
- flowtask/services/tasks/__init__.py +13 -0
- flowtask/services/tasks/launcher.py +213 -0
- flowtask/services/tasks/manager.py +323 -0
- flowtask/services/tasks/service.py +275 -0
- flowtask/services/tasks/task_manager.py +376 -0
- flowtask/services/tasks/tasks.py +155 -0
- flowtask/storages/__init__.py +16 -0
- flowtask/storages/exceptions.py +12 -0
- flowtask/storages/files/__init__.py +8 -0
- flowtask/storages/files/abstract.py +29 -0
- flowtask/storages/files/filesystem.py +66 -0
- flowtask/storages/tasks/__init__.py +19 -0
- flowtask/storages/tasks/abstract.py +26 -0
- flowtask/storages/tasks/database.py +33 -0
- flowtask/storages/tasks/filesystem.py +108 -0
- flowtask/storages/tasks/github.py +119 -0
- flowtask/storages/tasks/memory.py +45 -0
- flowtask/storages/tasks/row.py +25 -0
- flowtask/tasks/__init__.py +0 -0
- flowtask/tasks/abstract.py +526 -0
- flowtask/tasks/command.py +118 -0
- flowtask/tasks/pile.py +486 -0
- flowtask/tasks/py.typed +0 -0
- flowtask/tasks/task.py +778 -0
- flowtask/template/__init__.py +161 -0
- flowtask/tests.py +257 -0
- flowtask/types/__init__.py +8 -0
- flowtask/types/typedefs.c +11347 -0
- flowtask/types/typedefs.cpython-310-x86_64-linux-gnu.so +0 -0
- flowtask/utils/__init__.py +24 -0
- flowtask/utils/constants.py +117 -0
- flowtask/utils/encoders.py +21 -0
- flowtask/utils/executor.py +112 -0
- flowtask/utils/functions.cpp +14280 -0
- flowtask/utils/functions.cpython-310-x86_64-linux-gnu.so +0 -0
- flowtask/utils/json.cpp +13349 -0
- flowtask/utils/json.cpython-310-x86_64-linux-gnu.so +0 -0
- flowtask/utils/mail.py +63 -0
- flowtask/utils/parseqs.c +13324 -0
- flowtask/utils/parserqs.cpython-310-x86_64-linux-gnu.so +0 -0
- flowtask/utils/stats.py +308 -0
- flowtask/utils/transformations.py +74 -0
- flowtask/utils/uv.py +12 -0
- flowtask/utils/validators.py +97 -0
- flowtask/version.py +11 -0
- flowtask-5.8.4.dist-info/LICENSE +201 -0
- flowtask-5.8.4.dist-info/METADATA +209 -0
- flowtask-5.8.4.dist-info/RECORD +470 -0
- flowtask-5.8.4.dist-info/WHEEL +6 -0
- flowtask-5.8.4.dist-info/entry_points.txt +3 -0
- flowtask-5.8.4.dist-info/top_level.txt +2 -0
- plugins/components/CreateQR.py +39 -0
- plugins/components/TestComponent.py +28 -0
- plugins/components/Use1.py +13 -0
- plugins/components/Workplace.py +117 -0
- plugins/components/__init__.py +3 -0
- plugins/sources/__init__.py +0 -0
- plugins/sources/get_populartimes.py +78 -0
- plugins/sources/google.py +150 -0
- plugins/sources/hubspot.py +679 -0
- plugins/sources/icims.py +679 -0
- plugins/sources/mobileinsight.py +501 -0
- plugins/sources/newrelic.py +262 -0
- plugins/sources/uap.py +268 -0
- plugins/sources/venu.py +244 -0
- 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
|