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