├── .gitignore ├── .DS_Store ├── requirements.in ├── Dockerfile ├── app.json ├── setup ├── postcreate.sh └── prebuild.sh ├── config.yaml ├── CONTRIBUTING.md ├── README.md ├── utils ├── ads_mutator.py ├── entities.py ├── config.py ├── sheets.py ├── auth.py └── ads_searcher.py ├── main.py ├── frontend.py ├── LICENSE └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.log 3 | .venv/* -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/seatera/HEAD/.DS_Store -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | protobuf 2 | google-ads 3 | google-api-python-client 4 | google-auth-httplib2 5 | google-auth-oauthlib 6 | python-dateutil 7 | pyyaml 8 | streamlit 9 | google-cloud 10 | google-cloud-storage -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9.6 2 | 3 | # Copy local code to the container image. 4 | ENV APP_HOME /app 5 | WORKDIR $APP_HOME 6 | COPY . ./ 7 | 8 | RUN pip3 install --require-hashes --no-deps -r requirements.txt 9 | 10 | EXPOSE 8080 11 | 12 | ENTRYPOINT ["streamlit", "run", "frontend.py", "--server.port=8080", "--server.address=0.0.0.0"] -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "seatera", 3 | "options": { 4 | "allow-unauthenticated": true, 5 | "memory": "1024Mi", 6 | "cpu": "2" 7 | }, 8 | "hooks": { 9 | "prebuild": { 10 | "commands": ["chmod 777 ./setup/prebuild.sh", "./setup/prebuild.sh"] 11 | }, 12 | "postcreate": { 13 | "commands": ["chmod 777 ./setup/postcreate.sh", "./setup/postcreate.sh"] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /setup/postcreate.sh: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | echo "Setting bucket env variable.." 16 | gcloud run services update seatera --update-env-vars bucket_name=${GOOGLE_CLOUD_PROJECT}-seatera --region=${GOOGLE_CLOUD_REGION} 17 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | client_id: #OAuth2 client ID 16 | client_secret: #OAuth2 client secret 17 | developer_token: #Your developer token 18 | login_customer_id: #MCC ID 19 | refresh_token: 20 | spreadsheet_url: #URL of your copy of the spreadsheet 21 | use_proto_plus: true 22 | valid_config: false 23 | -------------------------------------------------------------------------------- /setup/prebuild.sh: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | echo "Setting Project ID: ${GOOGLE_CLOUD_PROJECT}" 16 | gcloud config set project ${GOOGLE_CLOUD_PROJECT} 17 | 18 | echo "Enabling Cloud Storage service..." 19 | gcloud services enable storage-component.googleapis.com 20 | 21 | echo "Creating cloud storage bucket..." 22 | gcloud alpha storage buckets create gs://${GOOGLE_CLOUD_PROJECT}-seatera --project=${GOOGLE_CLOUD_PROJECT} 23 | 24 | echo "Uploading config.yaml to cloud storage..." 25 | gcloud alpha storage cp ./config.yaml gs://${GOOGLE_CLOUD_PROJECT}-seatera 26 | 27 | gcloud auth configure-docker -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. 4 | 5 | ## Before you begin 6 | 7 | ### Sign our Contributor License Agreement 8 | 9 | Contributions to this project must be accompanied by a 10 | [Contributor License Agreement](https://cla.developers.google.com/about) (CLA). 11 | You (or your employer) retain the copyright to your contribution; this simply 12 | gives us permission to use and redistribute your contributions as part of the 13 | project. 14 | 15 | If you or your current employer have already signed the Google CLA (even if it 16 | was for a different project), you probably don't need to do it again. 17 | 18 | Visit to see your current agreements or to 19 | sign a new one. 20 | 21 | ### Review our Community Guidelines 22 | 23 | This project follows 24 | [Google's Open Source Community Guidelines](https://opensource.google/conduct/). 25 | 26 | ## Contribution process 27 | 28 | ### Code Reviews 29 | 30 | All submissions, including submissions by project members, require review. We 31 | use GitHub pull requests for this purpose. Consult 32 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 33 | information on using pull requests. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SeaTerA 2 | *** 3 | ##### Get exclusion and keyword recommendations by analyzing your search terms 4 | 5 | SeaTera helps advertisers manage keywords and exclusions across their accounts by analyzing their search terms. 6 | SeaTera collects all search terms above configurable thresholds and checks if these search terms exist as keywords in other ad groups or if they do not exist as keywords at all. 7 | Advertisers can use this data in two ways: 8 | 1. Exclude keywords that drive traffic to the wrong ad group (Exist as keyword in a different ad group than the one that triggered the search term) 9 | 2. Add new keywords (Search terms that drive traffic but don't exist as keywords in any ad group) 10 | 11 | 12 | The tool will create a new spreadsheet and populate two seperate sheets: 13 | 1. Keywords - a list of search terms that drove traffic but do not currently exist as a keyword in any other ad group 14 | 2. Exclusions - a list of search terms that appear in an ad group even though they exist as keywords in different ad groups (one line per ad group) 15 | 16 | 17 | ## Prerequisites 18 | 19 | 1. [A Google Ads Developer token](https://developers.google.com/google-ads/api/docs/first-call/dev-token#:~:text=A%20developer%20token%20from%20Google,SETTINGS%20%3E%20SETUP%20%3E%20API%20Center.) 20 | 21 | 1. A new GCP project with billing attached 22 | 23 | 1. Create OAuth2 Credentials of type **Web** and refresh token with scopes **"Google Ads API"** and **"Google Sheets API" (including drive + drive.file)**. Follow instructions in [this video](https://www.youtube.com/watch?v=KFICa7Ngzng) 24 | 25 | 1. [Enable Google ads API](https://developers.google.com/google-ads/api/docs/first-call/oauth-cloud-project#enable_the_in_your_project) 26 | 27 | 1. Enable Sheets API 28 | 29 | 30 | ## Installation 31 | 32 | 1. Click the big blue button to deploy: 33 | 34 | [![Run on Google Cloud](https://deploy.cloud.run/button.svg)](https://deploy.cloud.run) 35 | 36 | 1. Choose your designated GCP project and desired region. 37 | 38 | 1. Once installation is finished you will recieve your tool's URL. Save it. 39 | 40 | 41 | ## Usage 42 | 43 | 1. If it's your first time using this tool - fill in your credentials in the "Authentication" tab. 44 | 45 | 1. Choose if you want to run the analysis on all accounts under your MCC, or on selected accounts. 46 | 47 | 1. Choose your date range and search term thresholds, and click "RUN" 48 | 49 | 1. A link to a results spreadsheet will be presented once the run is complete 50 | 51 | 52 | ## Disclaimer 53 | This is not an officially supported Google product. -------------------------------------------------------------------------------- /utils/ads_mutator.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import logging 16 | 17 | class Mutator(object): 18 | def __init__(self, client, customer_id): 19 | self._client = client 20 | self._customer_id = customer_id 21 | 22 | 23 | class NegativeKeywordsUploader(Mutator): 24 | def __init__(self, client, customer_id): 25 | super().__init__(client, customer_id) 26 | self._ad_group_service = client.get_service("AdGroupService") 27 | self._ad_group_criterion_service = client.get_service( 28 | "AdGroupCriterionService") 29 | 30 | def upload_from_script(self, keywords): 31 | operations = [] 32 | for kw, adgroups in keywords.items(): 33 | for ag_id in adgroups: 34 | # Create keyword. 35 | ad_group_criterion_operation = self._client.get_type( 36 | "AdGroupCriterionOperation") 37 | ad_group_criterion = ad_group_criterion_operation.create 38 | ad_group_criterion.ad_group = self._ad_group_service.ad_group_path( 39 | self._customer_id, ag_id 40 | ) 41 | ad_group_criterion.status = self._client.enums.AdGroupCriterionStatusEnum.ENABLED 42 | ad_group_criterion.keyword.text = kw 43 | ad_group_criterion.keyword.match_type = ( 44 | self._client.enums.KeywordMatchTypeEnum.EXACT 45 | ) 46 | ad_group_criterion.negative = True 47 | operations.append(ad_group_criterion_operation) 48 | 49 | ad_group_criterion_response = ( 50 | self._ad_group_criterion_service.mutate_ad_group_criteria( 51 | request={'response_content_type': 'MUTABLE_RESOURCE', 52 | 'customer_id': self._customer_id, 'operations': operations} 53 | ) 54 | ) 55 | for result in ad_group_criterion_response.results: 56 | logging.info( 57 | f"Added negative keyword {result.ad_group_criterion.keyword.text} in ad group {result.ad_group_criterion.ad_group} ." 58 | ) 59 | -------------------------------------------------------------------------------- /utils/entities.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from typing import List, Dict, Any 16 | from dateutil.parser import parse 17 | 18 | 19 | class RunSettings: 20 | def __init__(self, thresholds: Dict[str, str], start_date: str, end_date: str, accounts: List[str] = []): 21 | if not start_date or not end_date: 22 | raise ValueError( 23 | "Start and end dates must be provided in settings sheet.") 24 | if parse(start_date) > parse(end_date): 25 | raise ValueError("End Date must be the same or later than Start Date") 26 | 27 | self.thresholds = thresholds 28 | self.start_date = parse(start_date).strftime("%Y-%m-%d") 29 | self.end_date = parse(end_date).strftime("%Y-%m-%d") 30 | self.accounts = accounts 31 | 32 | # Convert cost to cost micros 33 | self.thresholds['cost'] = str(int(self.thresholds['cost']) * 1000000) 34 | 35 | @staticmethod 36 | def from_sheet_read(input: List[List[str]]): 37 | thresholds = {} 38 | start_date = '' 39 | end_date = '' 40 | accounts = '' 41 | 42 | for list in input: 43 | key = list[0] 44 | try: 45 | value = list[1] 46 | except IndexError: 47 | value = 0 48 | 49 | if key == 'start_date': 50 | start_date = value 51 | elif key == 'end_date': 52 | end_date = value 53 | elif key == 'accounts': 54 | if not value: 55 | accounts = [] 56 | else: 57 | accounts = str(value).split(',') 58 | 59 | else: 60 | thresholds[key] = value 61 | 62 | return RunSettings(thresholds, start_date, end_date, accounts) 63 | 64 | @staticmethod 65 | def from_dict(input:Dict[Any, Any]): 66 | thresholds = { 67 | 'clicks': input.get('clicks', 0), 68 | 'conversions': input.get('conversions', 0), 69 | 'impressions': input.get('impressions', 0), 70 | 'cost': input.get('cost', 0), 71 | 'ctr': input.get('ctr', 0) 72 | } 73 | 74 | return RunSettings(thresholds=thresholds, start_date=input['start_date'], end_date=input['end_date'], accounts=input.get('accounts', [])) 75 | 76 | def __repr__(self) -> str: 77 | return f'RunSettings("{self.thresholds}", "{self.start_date}", "{self.end_date}", "{self.accounts}")' 78 | -------------------------------------------------------------------------------- /utils/config.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from yaml.loader import SafeLoader 16 | from copy import deepcopy 17 | from google.cloud import storage 18 | from google.ads.googleads.client import GoogleAdsClient 19 | from typing import Dict 20 | import os 21 | import yaml 22 | 23 | 24 | BUCKET_NAME = os.getenv('bucket_name') 25 | CONFIG_FILE_NAME = 'config.yaml' 26 | CONFIG_FILE_PATH = BUCKET_NAME + '/' + CONFIG_FILE_NAME 27 | 28 | class Config: 29 | def __init__(self) -> None: 30 | self.file_path = CONFIG_FILE_PATH 31 | self.storage_client = storage.Client() 32 | self.bucket = self.storage_client.bucket(BUCKET_NAME) 33 | config = self.load_config_from_file() 34 | if config is None: 35 | config = {} 36 | 37 | self.client_id = config.get('client_id', '') 38 | self.client_secret = config.get('client_secret') 39 | self.refresh_token = config.get('refresh_token', '') 40 | self.developer_token = config.get('developer_token', '') 41 | self.login_customer_id = config.get('login_customer_id', '') 42 | self.spreadsheet_url = config.get('spreadsheet_url', '') 43 | self.check_valid_config() 44 | 45 | def check_valid_config(self): 46 | if self.client_id and self.client_secret and self.refresh_token and self.developer_token and self.login_customer_id: 47 | self.valid_config = True 48 | else: 49 | self.valid_config = False 50 | 51 | def load_config_from_file(self): 52 | try: 53 | blob = self.bucket.blob(CONFIG_FILE_NAME) 54 | with blob.open() as f: 55 | config = yaml.load(f, Loader=SafeLoader) 56 | except Exception as e: 57 | print(str(e)) 58 | return None 59 | return config 60 | 61 | def save_to_file(self): 62 | try: 63 | config = deepcopy(self.to_dict()) 64 | blob = self.bucket.blob(CONFIG_FILE_NAME) 65 | with blob.open('w') as f: 66 | yaml.dump(config, f) 67 | print(f"Configurations updated in {self.file_path}") 68 | except Exception as e: 69 | print(f"Could not write configurations to {self.file_path} file") 70 | print(e) 71 | 72 | def to_dict(self) -> Dict[str, str]: 73 | """ Return the core attributes of the object as dict""" 74 | return { 75 | "client_id": self.client_id, 76 | "client_secret": self.client_secret, 77 | "refresh_token": self.refresh_token, 78 | "developer_token": self.developer_token, 79 | "login_customer_id": self.login_customer_id, 80 | "spreadsheet_url": self.spreadsheet_url 81 | } 82 | 83 | def get_ads_client(self): 84 | return GoogleAdsClient.load_from_dict({ 85 | 'client_id': self.client_id, 86 | 'client_secret': self.client_secret, 87 | 'login_customer_id': self.login_customer_id, 88 | 'developer_token': self.developer_token, 89 | 'refresh_token': self.refresh_token, 90 | 'use_proto_plus': True, 91 | }) -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import sys 16 | import logging 17 | import utils.auth as auth 18 | from pathlib import Path 19 | from pprint import pprint 20 | from utils.auth import CONFIG_FILE, SCOPES 21 | from utils.sheets import SheetsInteractor, get_sheets_service, create_new_spreadsheet, flatten_data 22 | from utils.ads_searcher import AccountsBuilder, SearchTermBuilder, KeywordDedupingBuilder 23 | from utils.ads_mutator import NegativeKeywordsUploader 24 | from utils.entities import RunSettings 25 | from utils.config import Config 26 | from typing import Dict, Any, Optional, Union 27 | from google.ads.googleads.client import GoogleAdsClient 28 | from google.ads.googleads.errors import GoogleAdsException 29 | 30 | _LOGS_PATH = Path('./script.log') 31 | _KEYWORDS_SHEET = 'Keywords' 32 | _EXCLUSIONS_SHEET = 'Exclusions' 33 | 34 | logging.basicConfig(filename=_LOGS_PATH, 35 | level=logging.INFO, 36 | format='%(asctime)s:%(levelname)s:%(message)s') 37 | 38 | 39 | 40 | def _get_search_terms(client: GoogleAdsClient, run_settings: RunSettings, account: str) -> Dict[str, Dict[str, Any]]: 41 | """Uses the SearchTermBuilder class to get all Search Terms from A specific account""" 42 | builder = SearchTermBuilder(client, account) 43 | return builder.build(run_settings.thresholds, run_settings.start_date, run_settings.end_date) 44 | 45 | 46 | def _dedup_and_get_exclusions(client: GoogleAdsClient, run_settings: RunSettings, account: str, search_terms: Dict[str, Any]): 47 | """Removes existing keywords froms search term dict and return an exclusion list""" 48 | kw_builder = KeywordDedupingBuilder(client, account) 49 | return kw_builder.build(search_terms) 50 | 51 | 52 | def _add_negative_keywords(client, account, neg_kw): 53 | builder = NegativeKeywordsUploader(client, account) 54 | builder.upload_from_script(neg_kw) 55 | 56 | 57 | def upload_from_sheets(client, sheet_handler): 58 | pass 59 | 60 | 61 | def run_from_ui(params: Dict[str, str], config: Config): 62 | # Temp function to trigger the run from UI. For when we want to keep both running options 63 | sheets_service = get_sheets_service(config.__dict__) 64 | if not config.spreadsheet_url: 65 | config.spreadsheet_url = create_new_spreadsheet(sheets_service) 66 | config.save_to_file() 67 | sheets_handler = SheetsInteractor(sheets_service, config.spreadsheet_url) 68 | google_ads_client = config.get_ads_client() 69 | 70 | main(google_ads_client, config.login_customer_id, 71 | sheets_handler, params, auto_upload_negatives=False) 72 | 73 | 74 | def get_accounts_for_ui(config: Config): 75 | google_ads_client = config.get_ads_client() 76 | accounts = AccountsBuilder(google_ads_client).get_accounts(with_names=True) 77 | return accounts 78 | 79 | def main(client: GoogleAdsClient, 80 | mcc_id: str, 81 | sheet_handler: SheetsInteractor, 82 | params: Dict[Any, Any] = None, 83 | auto_upload_negatives: bool = False): 84 | 85 | 86 | run_settings = RunSettings.from_dict(params) 87 | 88 | if not run_settings.accounts: 89 | run_settings.accounts = AccountsBuilder(client).get_accounts() 90 | 91 | logging.info(run_settings) 92 | 93 | keyword_recommendations = {} 94 | exclusion_recommendations = {} 95 | for account in run_settings.accounts: 96 | search_terms = _get_search_terms(client, run_settings, account) 97 | exclusions = _dedup_and_get_exclusions( 98 | client, run_settings, account, search_terms) 99 | if search_terms: 100 | keyword_recommendations[account] = search_terms 101 | if exclusions: 102 | exclusion_recommendations[account] = exclusions 103 | 104 | # pprint(keyword_recommendations) 105 | # pprint(exclusion_recommendations) 106 | 107 | # If auto upload, iterate over exclusion dict and for each account add negative kws 108 | if auto_upload_negatives: 109 | for account, neg_kw in exclusion_recommendations.items(): 110 | _add_negative_keywords(client, account, neg_kw) 111 | 112 | flattened_kw_recommendations = flatten_data(keyword_recommendations) 113 | flattened_exclusion_recommendations = flatten_data( 114 | exclusion_recommendations) 115 | 116 | sheet_handler.write_to_spreadsheet( 117 | {_KEYWORDS_SHEET: flattened_kw_recommendations, 118 | _EXCLUSIONS_SHEET: flattened_exclusion_recommendations}) 119 | 120 | 121 | -------------------------------------------------------------------------------- /utils/sheets.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import re 16 | import logging 17 | from typing import List, Any, Dict 18 | from datetime import datetime 19 | from google.oauth2.credentials import Credentials 20 | from utils.auth import CONFIG_FILE, SCOPES 21 | from google.auth.transport.requests import Request 22 | from googleapiclient.discovery import build 23 | from googleapiclient.errors import HttpError 24 | 25 | _SHEETS_SERVICE_VERSION = 'v4' 26 | _SHEETS_SERVICE_NAME = 'sheets' 27 | 28 | _HEADER = ['keyword', 'account name', 'account id', 'campaign name', 29 | 'campaign id', 'adgroup name', 'adgroup id','prominent adgroup', 'clicks', 'impressions', 'conversions', 'cost', 'ctr'] 30 | _RUN_DATETIME = datetime.now() 31 | _RUN_METADATA = f'Last run was completed on {_RUN_DATETIME}' 32 | _KEYWORDS_SHEET = 'Keywords' 33 | _EXCLUSIONS_SHEET = 'Exclusions' 34 | _SS_NAME = 'SeaTerA' 35 | 36 | class SheetsInteractor: 37 | def __init__(self, service, spreadsheet_url): 38 | self.service = service.spreadsheets() 39 | self.spreadsheet_url = spreadsheet_url 40 | self.spreadsheet_id = self._get_spreadsheet_id() 41 | 42 | def _get_spreadsheet_id(self) -> str: 43 | # Returns spreadsheet ID from spreadsheet URL 44 | if not self.spreadsheet_url: 45 | raise Exception( 46 | "No spreadsheet URL found. Follow instructions in README and add spreadsheet URL in config.yaml") 47 | 48 | spreadsheet_regex = '/d/(.*?)/edit' 49 | spreadsheet_match = re.search(spreadsheet_regex, self.spreadsheet_url) 50 | 51 | if spreadsheet_match == None: 52 | raise Exception("Couldn't extract spreadsheet ID from URL.") 53 | 54 | spreadsheet_id = spreadsheet_match.group(1) 55 | return spreadsheet_id 56 | 57 | def write_to_spreadsheet(self, ouput: Dict[str, Dict[Any, Any]]): 58 | data = [] 59 | for sheet, values in ouput.items(): 60 | self._clear_sheet(sheet) 61 | range = sheet + '!A1:' + \ 62 | chr(len(values[0]) + 65) + str(len(values)) 63 | data.append({'range': range, 'values': values}) 64 | 65 | body = {'data': data, 'valueInputOption': "USER_ENTERED"} 66 | try: 67 | result = self.service.values().batchUpdate( 68 | spreadsheetId=self.spreadsheet_id, body=body).execute() 69 | logging.info( 70 | f"{(result.get('totalUpdatedRows') -4)} Rows updated.") 71 | return result 72 | except HttpError as e: 73 | logging.exception(e) 74 | return e 75 | 76 | def read_from_spreadsheet(self, range) -> List[List[Any]]: 77 | results = self.service.values().get( 78 | spreadsheetId=self.spreadsheet_id, range=range).execute() 79 | values = results.get('values', []) 80 | return values 81 | 82 | def _clear_sheet(self, sheet_name): 83 | """Helper function to clear output sheet before writing to it.""" 84 | range_name = sheet_name + '!A:Z' 85 | self.service.values().clear( 86 | spreadsheetId=self.spreadsheet_id, range=range_name, body={}).execute() 87 | 88 | 89 | def get_sheets_service(config: Dict[str, Any]): 90 | creds = None 91 | user_info = { 92 | "client_id": config['client_id'], 93 | "refresh_token": config['refresh_token'], 94 | "client_secret": config['client_secret'] 95 | } 96 | creds = Credentials.from_authorized_user_info(user_info, SCOPES) 97 | 98 | # If credentials are expired, refresh. 99 | if creds.expired: 100 | creds.refresh(Request()) 101 | 102 | service = build(_SHEETS_SERVICE_NAME, 103 | _SHEETS_SERVICE_VERSION, credentials=creds) 104 | return service 105 | 106 | 107 | def create_new_spreadsheet(sheet_service): 108 | spreadsheet_title = _SS_NAME 109 | worksheet_names = [_EXCLUSIONS_SHEET, 110 | _KEYWORDS_SHEET] 111 | sheets = [] 112 | for name in worksheet_names: 113 | worksheet = { 114 | 'properties': { 115 | 'title': name 116 | } 117 | } 118 | sheets.append(worksheet) 119 | 120 | spreadsheet = { 121 | 'properties': { 122 | 'title': spreadsheet_title 123 | }, 124 | 'sheets': sheets 125 | } 126 | ss = sheet_service.spreadsheets().create(body=spreadsheet, 127 | fields='spreadsheetUrl').execute() 128 | return ss.get('spreadsheetUrl') 129 | 130 | 131 | def flatten_data(dict: Dict[str, Any]) -> List[List[Any]]: 132 | row_len = len(_HEADER) 133 | metadata_row = ['' for i in range(row_len)] 134 | metadata_row[0] = _RUN_METADATA 135 | results = [metadata_row, _HEADER] 136 | 137 | for v in dict.values(): 138 | for kw, data in v.items(): 139 | prominent = data.get('prominent', '') 140 | for key, stats in data.items(): 141 | if key != 'prominent': 142 | row = [kw, stats['account'], stats['account_id'], stats['campaign'], stats['campaign_id'], stats['ad_group'], stats['ad_group_id'], 143 | prominent, stats['clicks'], stats['impressions'], stats['conversions'], stats['cost'], stats['ctr']] 144 | results.append(row) 145 | 146 | return results 147 | -------------------------------------------------------------------------------- /utils/auth.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # This file generates a refresh token to use with the Google Ads API, and populates 16 | # it inside config.yaml. 17 | 18 | import yaml 19 | import hashlib 20 | import os 21 | import re 22 | import socket 23 | import sys 24 | import webbrowser 25 | from urllib.parse import unquote 26 | from yaml.loader import SafeLoader 27 | from google_auth_oauthlib.flow import Flow 28 | 29 | SCOPES = ['https://www.googleapis.com/auth/adwords', 'https://www.googleapis.com/auth/spreadsheets', 30 | 'https://www.googleapis.com/auth/drive.file', 'https://www.googleapis.com/auth/drive'] 31 | SERVER = "127.0.0.1" 32 | PORT = 8080 33 | REDIRECT_URI = f"http://{SERVER}:{PORT}" 34 | CONFIG_FILE = './config.yaml' 35 | 36 | 37 | def main(ga_config=None): 38 | if not ga_config: 39 | ga_config = get_config(CONFIG_FILE) 40 | # If YAML values are not filled out, return and display error 41 | if None in (ga_config.get('client_id'), ga_config.get('client_secret'), ga_config.get('login_customer_id'), ga_config.get('developer_token')): 42 | raise Exception( 43 | "Not all required parameters are configured in config.yaml. Refer to README for instructions.") 44 | 45 | flow = Flow.from_client_config({ 46 | "installed": { 47 | "client_id": ga_config['client_id'], 48 | "auth_uri": "https://accounts.google.com/o/oauth2/auth", 49 | "token_uri": "https://oauth2.googleapis.com/token", 50 | "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", 51 | "client_secret": ga_config['client_secret'], 52 | "redirect_uris": [ 53 | "urn:ietf:wg:oauth:2.0:oob", 54 | SERVER 55 | ] 56 | } 57 | }, scopes=SCOPES) 58 | 59 | flow.redirect_uri = REDIRECT_URI 60 | # Create an anti-forgery state token as described here: 61 | # https://developers.google.com/identity/protocols/OpenIDConnect#createxsrftoken 62 | passthrough_val = hashlib.sha256(os.urandom(1024)).hexdigest() 63 | 64 | authorization_url, state = flow.authorization_url( 65 | access_type="offline", 66 | state=passthrough_val, 67 | prompt="consent", 68 | ) 69 | webbrowser.open_new(authorization_url) 70 | 71 | # Retrieves an authorization code by opening a socket to receive the 72 | # redirect request and parsing the query parameters set in the URL. 73 | # Then pass the code back into the OAuth module to get a refresh token. 74 | code = unquote(_get_authorization_code(passthrough_val)) 75 | flow.fetch_token(code=code) 76 | 77 | try: 78 | # Write refresh token to config.yaml 79 | ga_config['refresh_token'] = flow.credentials.refresh_token 80 | with open(CONFIG_FILE, 'w') as f: 81 | yaml.dump(ga_config, f) 82 | print("Refresh token updated and saved") 83 | return flow.credentials.refresh_token 84 | except Exception as e: 85 | print("could not write refresh token to google-ads.yaml file") 86 | print(e) 87 | 88 | 89 | def _get_authorization_code(passthrough_val): 90 | """Opens a socket to handle a single HTTP request containing auth tokens. 91 | Args: 92 | passthrough_val: an anti-forgery token used to verify the request 93 | received by the socket. 94 | Returns: 95 | a str access token from the Google Auth service. 96 | """ 97 | # Open a socket at _SERVER:_PORT and listen for a request 98 | sock = socket.socket() 99 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 100 | sock.bind((SERVER, PORT)) 101 | sock.listen(1) 102 | connection, address = sock.accept() 103 | data = connection.recv(1024) 104 | # Parse the raw request to retrieve the URL query parameters. 105 | params = _parse_raw_query_params(data) 106 | try: 107 | if not params.get("code"): 108 | # If no code is present in the query params then there will be an 109 | # error message with more details. 110 | error = params.get("error") 111 | message = f"Failed to retrieve authorization code. Error: {error}" 112 | raise ValueError(message) 113 | elif params.get("state") != passthrough_val: 114 | message = "State token does not match the expected state." 115 | raise ValueError(message) 116 | else: 117 | message = "Authorization code was successfully retrieved." 118 | except ValueError as error: 119 | print(error) 120 | sys.exit(1) 121 | finally: 122 | response = ("HTTP/1.1 200 OK\n" 123 | "Content-Type: text/html\n\n" 124 | f"{message}" 125 | "

Please go back to your console.

\n") 126 | connection.sendall(response.encode()) 127 | connection.close() 128 | return params.get("code") 129 | 130 | 131 | def _parse_raw_query_params(data): 132 | """Parses a raw HTTP request to extract its query params as a dict. 133 | Note that this logic is likely irrelevant if you're building OAuth logic 134 | into a complete web application, where response parsing is handled by a 135 | framework. 136 | Args: 137 | data: raw request data as bytes. 138 | Returns: 139 | a dict of query parameter key value pairs. 140 | """ 141 | # Decode the request into a utf-8 encoded string 142 | decoded = data.decode("utf-8") 143 | # Use a regular expression to extract the URL query parameters string 144 | match = re.search("GET\s\/\?(.*) ", decoded) 145 | params = match.group(1) 146 | # Split the parameters to isolate the key/value pairs 147 | pairs = [pair.split("=") for pair in params.split("&")] 148 | # Convert pairs to a dict to make it easy to access the values 149 | return {key: val for key, val in pairs} 150 | 151 | 152 | def get_config(yaml_path): 153 | with open(yaml_path, 'r') as f: 154 | ga_config = yaml.load(f, Loader=yaml.SafeLoader) 155 | return ga_config 156 | -------------------------------------------------------------------------------- /frontend.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import streamlit as st 16 | from utils.config import Config 17 | import utils.auth as auth 18 | from time import sleep 19 | from main import run_from_ui, get_accounts_for_ui 20 | from datetime import datetime 21 | 22 | OAUTH_HELP = """Refer to 23 | [Create OAuth2 Credentials](https://developers.google.com/google-ads/api/docs/client-libs/python/oauth-web#create_oauth2_credentials) 24 | for more information""" 25 | 26 | 27 | def validate_config(config): 28 | if config.valid_config: 29 | st.session_state.valid_config = True 30 | else: 31 | st.session_state.valid_config = False 32 | 33 | def initialize_session_state(): 34 | if "valid_config" not in st.session_state: 35 | st.session_state.valid_config = False 36 | if "validating_config_wip" not in st.session_state: 37 | st.session_state.validating_config_wip = False 38 | if "config" not in st.session_state: 39 | st.session_state.config = Config() 40 | if "accounts_for_ui" not in st.session_state: 41 | st.session_state.accounts_for_ui = [] 42 | 43 | def authenticate(config_params): 44 | st.session_state.config.client_id = config_params['client_id'] 45 | st.session_state.config.client_secret = config_params['client_secret'] 46 | st.session_state.config.refresh_token = config_params['refresh_token'] 47 | st.session_state.config.developer_token = config_params['developer_token'] 48 | st.session_state.config.login_customer_id = config_params['login_customer_id'] 49 | 50 | st.session_state.config.check_valid_config() 51 | st.session_state.valid_config = True 52 | st.session_state.config.save_to_file() 53 | 54 | def reset_config(): 55 | st.session_state.valid_config=False 56 | st.session_state.config.valid_config = False 57 | 58 | def update_btn_state(): 59 | # Needed to cloes settings expander before starting to process 60 | st.session_state.run_btn_clicked = True 61 | 62 | def get_accounts_list(): 63 | st.session_state.accounts_for_ui = get_accounts_for_ui(st.session_state.config) 64 | 65 | def value_placeholder(value): 66 | if value: return value 67 | else: return '' 68 | 69 | def run_tool(): 70 | parameters = { 71 | 'start_date': str(st.session_state.start_date), 72 | 'end_date': str(st.session_state.end_date), 73 | 'clicks': st.session_state.clicks, 74 | 'impressions': st.session_state.impressions, 75 | 'ctr': st.session_state.ctr, 76 | 'cost': st.session_state.cost, 77 | 'conversions': st.session_state.conversions, 78 | 'accounts': st.session_state.accounts_selected 79 | } 80 | 81 | run_from_ui(parameters, st.session_state.config) 82 | results_url = config.spreadsheet_url 83 | st.success(f'Search term analysis completed successfully. [Open in Google Sheets]({results_url})', icon="✅") 84 | 85 | # The Page UI starts here 86 | st.set_page_config( 87 | page_title="SeaTerA", 88 | layout="centered" 89 | ) 90 | 91 | customized_button = st.markdown(""" 92 | """, unsafe_allow_html=True) 95 | 96 | st.header("SeaTerA") 97 | 98 | initialize_session_state() 99 | config = st.session_state.config 100 | validate_config(config) 101 | 102 | with st.expander("**Authentication**", expanded=not st.session_state.valid_config): 103 | if not st.session_state.valid_config: 104 | st.info(f"Credentials are not set. {OAUTH_HELP}", icon="⚠️") 105 | client_id = st.text_input("Client ID", value=value_placeholder(config.client_id)) 106 | client_secret = st.text_input("Client Secret", value=value_placeholder(config.client_secret)) 107 | refresh_token = st.text_input("Refresh Token", value=value_placeholder(config.refresh_token)) 108 | developer_token = st.text_input("Developer Token", value=value_placeholder(config.developer_token)) 109 | mcc_id = st.text_input("MCC ID", value=value_placeholder(config.login_customer_id)) 110 | login_btn = st.button("Save", type='primary',on_click=authenticate, args=[{ 111 | 'client_id': client_id, 112 | 'client_secret': client_secret, 113 | 'refresh_token': refresh_token, 114 | 'developer_token': developer_token, 115 | 'login_customer_id': mcc_id 116 | }]) 117 | else: 118 | st.success(f'Credentials succesfully set ', icon="✅") 119 | st.text_input("Client ID", value=config.client_id, disabled= True) 120 | st.text_input("Client Secret", value=config.client_secret, disabled= True) 121 | st.text_input("Refresh Token", value=config.refresh_token, disabled=True) 122 | st.text_input("Developer Token", value=config.developer_token, disabled= True) 123 | st.text_input("MCC ID", value=config.login_customer_id, disabled= True) 124 | edit = st.button("Edit Credentials", on_click=reset_config) 125 | # bal = st.balloons() 126 | 127 | 128 | with st.expander("**Run Settings**", expanded=st.session_state.valid_config and ("run_btn_clicked" not in st.session_state or not st.session_state.run_btn_clicked)): 129 | 130 | # Accounts picker 131 | st.radio("Run on all accounts under MCC or selecet specific accounts",["All Accounts", "Selected Accounts"], index=0, key="all_accounts", label_visibility="visible") 132 | if st.session_state.all_accounts == 'Selected Accounts': 133 | if "accounts_for_ui" not in st.session_state or st.session_state.accounts_for_ui == []: 134 | get_accounts_list() 135 | 136 | accounts_selected_explicit = st.multiselect("Choose Accounts", st.session_state.accounts_for_ui) 137 | 138 | st.session_state.accounts_selected = [x.split(' - ')[0] for x in accounts_selected_explicit] 139 | 140 | elif st.session_state.all_accounts == 'All Accounts': 141 | st.session_state.accounts_selected = [] 142 | # Dates picker 143 | start_date, end_date = st.columns(2) 144 | start_date.date_input("Start Date", key="start_date") 145 | end_date.date_input("End Date", key="end_date") 146 | 147 | 148 | # Thresholds pickers 149 | clicks, impressions, ctr, cost, conversions = st.columns(5) 150 | clicks.number_input("Clicks", min_value=0, key="clicks") 151 | impressions.number_input("Impressions", min_value=0, key="impressions") 152 | ctr.number_input("CTR", min_value=0, key="ctr") 153 | cost.number_input("Cost", min_value=0, key="cost") 154 | conversions.number_input("Conversions", min_value=0, key="conversions") 155 | 156 | st.session_state.run_btn_clicked = st.button("**Run**",type='primary', disabled=not st.session_state.valid_config, on_click=update_btn_state) 157 | 158 | if st.session_state.run_btn_clicked: 159 | with st.spinner(text='Creating search term analysis... This may take a few minutes'): 160 | run_tool() -------------------------------------------------------------------------------- /utils/ads_searcher.py: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | class Builder(object): 16 | def __init__(self, client, customer_id): 17 | self._service = client.get_service('GoogleAdsService') 18 | self._client = client 19 | self._customer_id = customer_id 20 | self._enums = { 21 | 'match_type': client.get_type('KeywordMatchTypeEnum').KeywordMatchType 22 | } 23 | 24 | def _get_rows(self, query): 25 | search_request = self._client.get_type("SearchGoogleAdsStreamRequest") 26 | search_request.customer_id = self._customer_id 27 | search_request.query = query 28 | response = self._service.search_stream(request=search_request) 29 | return response 30 | 31 | 32 | class SearchTermBuilder(Builder): 33 | """Gets Keywords recommednations from a single account.""" 34 | 35 | def build(self, thresholds, start_date, end_date): 36 | query = f""" 37 | SELECT 38 | search_term_view.search_term, 39 | customer.descriptive_name, 40 | customer.id, 41 | customer.currency_code, 42 | campaign.name, 43 | campaign.id, 44 | ad_group.name, 45 | ad_group.id, 46 | metrics.clicks, 47 | metrics.impressions, 48 | metrics.ctr, 49 | metrics.cost_micros, 50 | metrics.conversions 51 | FROM 52 | search_term_view 53 | WHERE 54 | campaign.advertising_channel_type = 'SEARCH' 55 | AND search_term_view.status = 'NONE' 56 | AND metrics.clicks >= {thresholds['clicks']} 57 | AND metrics.impressions >= {thresholds['impressions']} 58 | AND metrics.ctr > {thresholds['ctr']} 59 | AND metrics.cost_micros > {thresholds['cost']} 60 | AND metrics.conversions > {thresholds['conversions']} 61 | AND segments.date BETWEEN '{start_date}' AND '{end_date}' 62 | """ 63 | 64 | rows = self._get_rows(query) 65 | search_terms = {} 66 | for batch in rows: 67 | for row in batch.results: 68 | row = row._pb 69 | try: 70 | search_terms[row.search_term_view.search_term][row.ad_group.id] = { 71 | 'account_id': row.customer.id, 72 | 'account': row.customer.descriptive_name, 73 | 'campaign': row.campaign.name, 74 | 'campaign_id': row.campaign.id, 75 | 'ad_group': row.ad_group.name, 76 | 'ad_group_id': row.ad_group.id, 77 | 'clicks': row.metrics.clicks, 78 | 'impressions': row.metrics.impressions, 79 | 'conversions': row.metrics.conversions, 80 | 'ctr': row.metrics.ctr * 100, 81 | 'cost': row.metrics.cost_micros / 1000000 82 | } 83 | 84 | except KeyError: 85 | search_terms[row.search_term_view.search_term] = { 86 | row.ad_group.id: { 87 | 'account_id': row.customer.id, 88 | 'account': row.customer.descriptive_name, 89 | 'campaign': row.campaign.name, 90 | 'campaign_id': row.campaign.id, 91 | 'ad_group': row.ad_group.name, 92 | 'ad_group_id': row.ad_group.id, 93 | 'clicks': row.metrics.clicks, 94 | 'impressions': row.metrics.impressions, 95 | 'conversions': row.metrics.conversions, 96 | 'ctr': row.metrics.ctr * 100, 97 | 'cost': row.metrics.cost_micros / 1000000 98 | } 99 | } 100 | 101 | return search_terms 102 | 103 | 104 | class KeywordDedupingBuilder(Builder): 105 | """Gets Keywords from a single account, removes if from search term dict if 106 | KW exist in the same ad group. If exist in a different ad group, adds to exclusion list with 107 | to be add as negative kw in the st's original ad group.""" 108 | 109 | def build(self, search_terms): 110 | rows = self._get_rows(''' 111 | SELECT 112 | ad_group_criterion.keyword.text, 113 | ad_group.id, 114 | ad_group.name, 115 | campaign.name, 116 | ad_group_criterion.negative 117 | FROM 118 | ad_group_criterion 119 | WHERE 120 | ad_group_criterion.type = KEYWORD 121 | AND 122 | ad_group_criterion.status IN ('ENABLED', 'PAUSED') 123 | AND 124 | campaign.advertising_channel_type = 'SEARCH' 125 | ''') 126 | 127 | # Create a dict of keywords that appear in the search term list 128 | # and all the ad groups they exist in 129 | keywords = {} 130 | for batch in rows: 131 | for row in batch.results: 132 | row = row._pb 133 | # if keyword is not in search term dict, move on to the next one 134 | if not search_terms.get(row.ad_group_criterion.keyword.text): 135 | continue 136 | try: 137 | keywords[row.ad_group_criterion.keyword.text].append( 138 | row.ad_group.id) 139 | except KeyError: 140 | keywords[row.ad_group_criterion.keyword.text] = [ 141 | row.ad_group.id] 142 | 143 | # Create exclusion dict of negative keywords. Will have search terms 144 | # that appear in other ad groups as keywords. 145 | exclusion_list = {} 146 | for kw, kw_ags in keywords.items(): 147 | st_stats = search_terms[kw] 148 | for ag in kw_ags: 149 | if st_stats.get(ag): 150 | st_stats.pop(ag) 151 | # If st ad groups remain, add their stats and kw to exclusion list. 152 | if st_stats: 153 | exclusion_list[kw] = st_stats 154 | exclusion_list[kw]['prominent'] = self._get_prominent_existing_location(kw) 155 | search_terms.pop(kw) 156 | 157 | return exclusion_list 158 | 159 | def _get_prominent_existing_location(self, kw): 160 | """For given KW, get the ad group and campaign names where this KW has the largest cost""" 161 | rows = self._get_rows(f''' 162 | SELECT 163 | campaign.name, 164 | ad_group.name, 165 | metrics.cost_micros 166 | FROM keyword_view 167 | WHERE 168 | ad_group_criterion.keyword.text = "{kw}" 169 | ORDER BY 170 | metrics.cost_micros DESC 171 | LIMIT 1 172 | ''') 173 | 174 | for batch in rows: 175 | for row in batch.results: 176 | row = row._pb 177 | return row.campaign.name + '~' + row.ad_group.name 178 | 179 | 180 | class AccountsBuilder(Builder): 181 | """Gets all client accounts' IDs under the MCC.""" 182 | 183 | def __init__(self, client): 184 | super().__init__(client, client.login_customer_id) 185 | self._client = client 186 | 187 | def get_accounts(self, with_names=False): 188 | """Used to get all client accounts using API""" 189 | accounts = [] 190 | query = ''' 191 | SELECT 192 | customer_client.descriptive_name, 193 | customer_client.id 194 | FROM 195 | customer_client 196 | WHERE 197 | customer_client.manager = False 198 | AND customer_client.status = 'ENABLED' 199 | ''' 200 | 201 | rows = self._get_rows(query) 202 | for batch in rows: 203 | for row in batch.results: 204 | row = row._pb 205 | account = str(row.customer_client.id) 206 | if with_names: 207 | account += ' - ' + str(row.customer_client.descriptive_name) 208 | accounts.append(account) 209 | 210 | return accounts 211 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.9 3 | # by the following command: 4 | # 5 | # pip-compile --generate-hashes requirements.in 6 | # 7 | altair==5.0.1 \ 8 | --hash=sha256:087d7033cb2d6c228493a053e12613058a5d47faf6a36aea3ff60305fd8b4cb0 \ 9 | --hash=sha256:9f3552ed5497d4dfc14cf48a76141d8c29ee56eae2873481b4b28134268c9bbe 10 | # via streamlit 11 | attrs==23.1.0 \ 12 | --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \ 13 | --hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015 14 | # via jsonschema 15 | blinker==1.6.2 \ 16 | --hash=sha256:4afd3de66ef3a9f8067559fb7a1cbe555c17dcbe15971b05d1b625c3e7abe213 \ 17 | --hash=sha256:c3d739772abb7bc2860abf5f2ec284223d9ad5c76da018234f6f50d6f31ab1f0 18 | # via streamlit 19 | cachetools==5.3.1 \ 20 | --hash=sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590 \ 21 | --hash=sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b 22 | # via 23 | # google-auth 24 | # streamlit 25 | certifi==2023.5.7 \ 26 | --hash=sha256:0f0d56dc5a6ad56fd4ba36484d6cc34451e1c6548c61daad8c320169f91eddc7 \ 27 | --hash=sha256:c6c2e98f5c7869efca1f8916fed228dd91539f9f1b444c314c06eef02980c716 28 | # via requests 29 | charset-normalizer==3.1.0 \ 30 | --hash=sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6 \ 31 | --hash=sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1 \ 32 | --hash=sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e \ 33 | --hash=sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373 \ 34 | --hash=sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62 \ 35 | --hash=sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230 \ 36 | --hash=sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be \ 37 | --hash=sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c \ 38 | --hash=sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0 \ 39 | --hash=sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448 \ 40 | --hash=sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f \ 41 | --hash=sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649 \ 42 | --hash=sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d \ 43 | --hash=sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0 \ 44 | --hash=sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706 \ 45 | --hash=sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a \ 46 | --hash=sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59 \ 47 | --hash=sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23 \ 48 | --hash=sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5 \ 49 | --hash=sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb \ 50 | --hash=sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e \ 51 | --hash=sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e \ 52 | --hash=sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c \ 53 | --hash=sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28 \ 54 | --hash=sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d \ 55 | --hash=sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41 \ 56 | --hash=sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974 \ 57 | --hash=sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce \ 58 | --hash=sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f \ 59 | --hash=sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1 \ 60 | --hash=sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d \ 61 | --hash=sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8 \ 62 | --hash=sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017 \ 63 | --hash=sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31 \ 64 | --hash=sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7 \ 65 | --hash=sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8 \ 66 | --hash=sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e \ 67 | --hash=sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14 \ 68 | --hash=sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd \ 69 | --hash=sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d \ 70 | --hash=sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795 \ 71 | --hash=sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b \ 72 | --hash=sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b \ 73 | --hash=sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b \ 74 | --hash=sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203 \ 75 | --hash=sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f \ 76 | --hash=sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19 \ 77 | --hash=sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1 \ 78 | --hash=sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a \ 79 | --hash=sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac \ 80 | --hash=sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9 \ 81 | --hash=sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0 \ 82 | --hash=sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137 \ 83 | --hash=sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f \ 84 | --hash=sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6 \ 85 | --hash=sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5 \ 86 | --hash=sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909 \ 87 | --hash=sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f \ 88 | --hash=sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0 \ 89 | --hash=sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324 \ 90 | --hash=sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755 \ 91 | --hash=sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb \ 92 | --hash=sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854 \ 93 | --hash=sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c \ 94 | --hash=sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60 \ 95 | --hash=sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84 \ 96 | --hash=sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0 \ 97 | --hash=sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b \ 98 | --hash=sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1 \ 99 | --hash=sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531 \ 100 | --hash=sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1 \ 101 | --hash=sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11 \ 102 | --hash=sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326 \ 103 | --hash=sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df \ 104 | --hash=sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab 105 | # via requests 106 | click==8.1.3 \ 107 | --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \ 108 | --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48 109 | # via streamlit 110 | decorator==5.1.1 \ 111 | --hash=sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330 \ 112 | --hash=sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186 113 | # via validators 114 | gitdb==4.0.10 \ 115 | --hash=sha256:6eb990b69df4e15bad899ea868dc46572c3f75339735663b81de79b06f17eb9a \ 116 | --hash=sha256:c286cf298426064079ed96a9e4a9d39e7f3e9bf15ba60701e95f5492f28415c7 117 | # via gitpython 118 | gitpython==3.1.31 \ 119 | --hash=sha256:8ce3bcf69adfdf7c7d503e78fd3b1c492af782d58893b650adb2ac8912ddd573 \ 120 | --hash=sha256:f04893614f6aa713a60cbbe1e6a97403ef633103cdd0ef5eb6efe0deb98dbe8d 121 | # via streamlit 122 | google-ads==22.1.0 \ 123 | --hash=sha256:6fdd3fb635678fbb3c8f87271afc81f0e139882b83b48505160fc4daacf33ad0 \ 124 | --hash=sha256:cfab38b40eb8424a4a514823bd8b911a57ef55dd64e2112cfa46a70d8090de98 125 | # via -r requirements.in 126 | google-api-core==2.11.0 \ 127 | --hash=sha256:4b9bb5d5a380a0befa0573b302651b8a9a89262c1730e37bf423cec511804c22 \ 128 | --hash=sha256:ce222e27b0de0d7bc63eb043b956996d6dccab14cc3b690aaea91c9cc99dc16e 129 | # via 130 | # google-ads 131 | # google-api-python-client 132 | # google-cloud-core 133 | # google-cloud-storage 134 | google-api-python-client==2.89.0 \ 135 | --hash=sha256:0b0c9503df2da92692ffceee88423ca593cbf0b939d879e2c46fbdc1a39cf091 \ 136 | --hash=sha256:272ff339928ac35b1e117d30e5db444fb701803bb748bb29e7bb520be29dea36 137 | # via -r requirements.in 138 | google-auth==2.20.0 \ 139 | --hash=sha256:030af34138909ccde0fbce611afc178f1d65d32fbff281f25738b1fe1c6f3eaa \ 140 | --hash=sha256:23b7b0950fcda519bfb6692bf0d5289d2ea49fc143717cc7188458ec620e63fa 141 | # via 142 | # google-api-core 143 | # google-api-python-client 144 | # google-auth-httplib2 145 | # google-auth-oauthlib 146 | # google-cloud-core 147 | # google-cloud-storage 148 | google-auth-httplib2==0.1.0 \ 149 | --hash=sha256:31e49c36c6b5643b57e82617cb3e021e3e1d2df9da63af67252c02fa9c1f4a10 \ 150 | --hash=sha256:a07c39fd632becacd3f07718dfd6021bf396978f03ad3ce4321d060015cc30ac 151 | # via 152 | # -r requirements.in 153 | # google-api-python-client 154 | google-auth-oauthlib==1.0.0 \ 155 | --hash=sha256:95880ca704928c300f48194d1770cf5b1462835b6e49db61445a520f793fd5fb \ 156 | --hash=sha256:e375064964820b47221a7e1b7ee1fd77051b6323c3f9e3e19785f78ab67ecfc5 157 | # via 158 | # -r requirements.in 159 | # google-ads 160 | google-cloud==0.34.0 \ 161 | --hash=sha256:01430187cf56df10a9ba775dd547393185d4b40741db0ea5889301f8e7a9d5d3 \ 162 | --hash=sha256:fb1ab7b0548fe44b3d538041f0a374505b7f990d448a935ea36649c5ccab5acf 163 | # via -r requirements.in 164 | google-cloud-core==2.3.2 \ 165 | --hash=sha256:8417acf6466be2fa85123441696c4badda48db314c607cf1e5d543fa8bdc22fe \ 166 | --hash=sha256:b9529ee7047fd8d4bf4a2182de619154240df17fbe60ead399078c1ae152af9a 167 | # via google-cloud-storage 168 | google-cloud-storage==2.9.0 \ 169 | --hash=sha256:83a90447f23d5edd045e0037982c270302e3aeb45fc1288d2c2ca713d27bad94 \ 170 | --hash=sha256:9b6ae7b509fc294bdacb84d0f3ea8e20e2c54a8b4bbe39c5707635fec214eff3 171 | # via -r requirements.in 172 | google-crc32c==1.5.0 \ 173 | --hash=sha256:024894d9d3cfbc5943f8f230e23950cd4906b2fe004c72e29b209420a1e6b05a \ 174 | --hash=sha256:02c65b9817512edc6a4ae7c7e987fea799d2e0ee40c53ec573a692bee24de876 \ 175 | --hash=sha256:02ebb8bf46c13e36998aeaad1de9b48f4caf545e91d14041270d9dca767b780c \ 176 | --hash=sha256:07eb3c611ce363c51a933bf6bd7f8e3878a51d124acfc89452a75120bc436289 \ 177 | --hash=sha256:1034d91442ead5a95b5aaef90dbfaca8633b0247d1e41621d1e9f9db88c36298 \ 178 | --hash=sha256:116a7c3c616dd14a3de8c64a965828b197e5f2d121fedd2f8c5585c547e87b02 \ 179 | --hash=sha256:19e0a019d2c4dcc5e598cd4a4bc7b008546b0358bd322537c74ad47a5386884f \ 180 | --hash=sha256:1c7abdac90433b09bad6c43a43af253e688c9cfc1c86d332aed13f9a7c7f65e2 \ 181 | --hash=sha256:1e986b206dae4476f41bcec1faa057851f3889503a70e1bdb2378d406223994a \ 182 | --hash=sha256:272d3892a1e1a2dbc39cc5cde96834c236d5327e2122d3aaa19f6614531bb6eb \ 183 | --hash=sha256:278d2ed7c16cfc075c91378c4f47924c0625f5fc84b2d50d921b18b7975bd210 \ 184 | --hash=sha256:2ad40e31093a4af319dadf503b2467ccdc8f67c72e4bcba97f8c10cb078207b5 \ 185 | --hash=sha256:2e920d506ec85eb4ba50cd4228c2bec05642894d4c73c59b3a2fe20346bd00ee \ 186 | --hash=sha256:3359fc442a743e870f4588fcf5dcbc1bf929df1fad8fb9905cd94e5edb02e84c \ 187 | --hash=sha256:37933ec6e693e51a5b07505bd05de57eee12f3e8c32b07da7e73669398e6630a \ 188 | --hash=sha256:398af5e3ba9cf768787eef45c803ff9614cc3e22a5b2f7d7ae116df8b11e3314 \ 189 | --hash=sha256:3b747a674c20a67343cb61d43fdd9207ce5da6a99f629c6e2541aa0e89215bcd \ 190 | --hash=sha256:461665ff58895f508e2866824a47bdee72497b091c730071f2b7575d5762ab65 \ 191 | --hash=sha256:4c6fdd4fccbec90cc8a01fc00773fcd5fa28db683c116ee3cb35cd5da9ef6c37 \ 192 | --hash=sha256:5829b792bf5822fd0a6f6eb34c5f81dd074f01d570ed7f36aa101d6fc7a0a6e4 \ 193 | --hash=sha256:596d1f98fc70232fcb6590c439f43b350cb762fb5d61ce7b0e9db4539654cc13 \ 194 | --hash=sha256:5ae44e10a8e3407dbe138984f21e536583f2bba1be9491239f942c2464ac0894 \ 195 | --hash=sha256:635f5d4dd18758a1fbd1049a8e8d2fee4ffed124462d837d1a02a0e009c3ab31 \ 196 | --hash=sha256:64e52e2b3970bd891309c113b54cf0e4384762c934d5ae56e283f9a0afcd953e \ 197 | --hash=sha256:66741ef4ee08ea0b2cc3c86916ab66b6aef03768525627fd6a1b34968b4e3709 \ 198 | --hash=sha256:67b741654b851abafb7bc625b6d1cdd520a379074e64b6a128e3b688c3c04740 \ 199 | --hash=sha256:6ac08d24c1f16bd2bf5eca8eaf8304812f44af5cfe5062006ec676e7e1d50afc \ 200 | --hash=sha256:6f998db4e71b645350b9ac28a2167e6632c239963ca9da411523bb439c5c514d \ 201 | --hash=sha256:72218785ce41b9cfd2fc1d6a017dc1ff7acfc4c17d01053265c41a2c0cc39b8c \ 202 | --hash=sha256:74dea7751d98034887dbd821b7aae3e1d36eda111d6ca36c206c44478035709c \ 203 | --hash=sha256:759ce4851a4bb15ecabae28f4d2e18983c244eddd767f560165563bf9aefbc8d \ 204 | --hash=sha256:77e2fd3057c9d78e225fa0a2160f96b64a824de17840351b26825b0848022906 \ 205 | --hash=sha256:7c074fece789b5034b9b1404a1f8208fc2d4c6ce9decdd16e8220c5a793e6f61 \ 206 | --hash=sha256:7c42c70cd1d362284289c6273adda4c6af8039a8ae12dc451dcd61cdabb8ab57 \ 207 | --hash=sha256:7f57f14606cd1dd0f0de396e1e53824c371e9544a822648cd76c034d209b559c \ 208 | --hash=sha256:83c681c526a3439b5cf94f7420471705bbf96262f49a6fe546a6db5f687a3d4a \ 209 | --hash=sha256:8485b340a6a9e76c62a7dce3c98e5f102c9219f4cfbf896a00cf48caf078d438 \ 210 | --hash=sha256:84e6e8cd997930fc66d5bb4fde61e2b62ba19d62b7abd7a69920406f9ecca946 \ 211 | --hash=sha256:89284716bc6a5a415d4eaa11b1726d2d60a0cd12aadf5439828353662ede9dd7 \ 212 | --hash=sha256:8b87e1a59c38f275c0e3676fc2ab6d59eccecfd460be267ac360cc31f7bcde96 \ 213 | --hash=sha256:8f24ed114432de109aa9fd317278518a5af2d31ac2ea6b952b2f7782b43da091 \ 214 | --hash=sha256:98cb4d057f285bd80d8778ebc4fde6b4d509ac3f331758fb1528b733215443ae \ 215 | --hash=sha256:998679bf62b7fb599d2878aa3ed06b9ce688b8974893e7223c60db155f26bd8d \ 216 | --hash=sha256:9ba053c5f50430a3fcfd36f75aff9caeba0440b2d076afdb79a318d6ca245f88 \ 217 | --hash=sha256:9c99616c853bb585301df6de07ca2cadad344fd1ada6d62bb30aec05219c45d2 \ 218 | --hash=sha256:a1fd716e7a01f8e717490fbe2e431d2905ab8aa598b9b12f8d10abebb36b04dd \ 219 | --hash=sha256:a2355cba1f4ad8b6988a4ca3feed5bff33f6af2d7f134852cf279c2aebfde541 \ 220 | --hash=sha256:b1f8133c9a275df5613a451e73f36c2aea4fe13c5c8997e22cf355ebd7bd0728 \ 221 | --hash=sha256:b8667b48e7a7ef66afba2c81e1094ef526388d35b873966d8a9a447974ed9178 \ 222 | --hash=sha256:ba1eb1843304b1e5537e1fca632fa894d6f6deca8d6389636ee5b4797affb968 \ 223 | --hash=sha256:be82c3c8cfb15b30f36768797a640e800513793d6ae1724aaaafe5bf86f8f346 \ 224 | --hash=sha256:c02ec1c5856179f171e032a31d6f8bf84e5a75c45c33b2e20a3de353b266ebd8 \ 225 | --hash=sha256:c672d99a345849301784604bfeaeba4db0c7aae50b95be04dd651fd2a7310b93 \ 226 | --hash=sha256:c6c777a480337ac14f38564ac88ae82d4cd238bf293f0a22295b66eb89ffced7 \ 227 | --hash=sha256:cae0274952c079886567f3f4f685bcaf5708f0a23a5f5216fdab71f81a6c0273 \ 228 | --hash=sha256:cd67cf24a553339d5062eff51013780a00d6f97a39ca062781d06b3a73b15462 \ 229 | --hash=sha256:d3515f198eaa2f0ed49f8819d5732d70698c3fa37384146079b3799b97667a94 \ 230 | --hash=sha256:d5280312b9af0976231f9e317c20e4a61cd2f9629b7bfea6a693d1878a264ebd \ 231 | --hash=sha256:de06adc872bcd8c2a4e0dc51250e9e65ef2ca91be023b9d13ebd67c2ba552e1e \ 232 | --hash=sha256:e1674e4307fa3024fc897ca774e9c7562c957af85df55efe2988ed9056dc4e57 \ 233 | --hash=sha256:e2096eddb4e7c7bdae4bd69ad364e55e07b8316653234a56552d9c988bd2d61b \ 234 | --hash=sha256:e560628513ed34759456a416bf86b54b2476c59144a9138165c9a1575801d0d9 \ 235 | --hash=sha256:edfedb64740750e1a3b16152620220f51d58ff1b4abceb339ca92e934775c27a \ 236 | --hash=sha256:f13cae8cc389a440def0c8c52057f37359014ccbc9dc1f0827936bcd367c6100 \ 237 | --hash=sha256:f314013e7dcd5cf45ab1945d92e713eec788166262ae8deb2cfacd53def27325 \ 238 | --hash=sha256:f583edb943cf2e09c60441b910d6a20b4d9d626c75a36c8fcac01a6c96c01183 \ 239 | --hash=sha256:fd8536e902db7e365f49e7d9029283403974ccf29b13fc7028b97e2295b33556 \ 240 | --hash=sha256:fe70e325aa68fa4b5edf7d1a4b6f691eb04bbccac0ace68e34820d283b5f80d4 241 | # via google-resumable-media 242 | google-resumable-media==2.5.0 \ 243 | --hash=sha256:218931e8e2b2a73a58eb354a288e03a0fd5fb1c4583261ac6e4c078666468c93 \ 244 | --hash=sha256:da1bd943e2e114a56d85d6848497ebf9be6a14d3db23e9fc57581e7c3e8170ec 245 | # via google-cloud-storage 246 | googleapis-common-protos==1.59.1 \ 247 | --hash=sha256:0cbedb6fb68f1c07e18eb4c48256320777707e7d0c55063ae56c15db3224a61e \ 248 | --hash=sha256:b35d530fe825fb4227857bc47ad84c33c809ac96f312e13182bdeaa2abe1178a 249 | # via 250 | # google-ads 251 | # google-api-core 252 | # grpcio-status 253 | grpcio==1.54.2 \ 254 | --hash=sha256:0212e2f7fdf7592e4b9d365087da30cb4d71e16a6f213120c89b4f8fb35a3ab3 \ 255 | --hash=sha256:09d4bfd84686cd36fd11fd45a0732c7628308d094b14d28ea74a81db0bce2ed3 \ 256 | --hash=sha256:1e623e0cf99a0ac114f091b3083a1848dbc64b0b99e181473b5a4a68d4f6f821 \ 257 | --hash=sha256:2288d76e4d4aa7ef3fe7a73c1c470b66ea68e7969930e746a8cd8eca6ef2a2ea \ 258 | --hash=sha256:2296356b5c9605b73ed6a52660b538787094dae13786ba53080595d52df13a98 \ 259 | --hash=sha256:2a1e601ee31ef30a9e2c601d0867e236ac54c922d32ed9f727b70dd5d82600d5 \ 260 | --hash=sha256:2be88c081e33f20630ac3343d8ad9f1125f32987968e9c8c75c051c9800896e8 \ 261 | --hash=sha256:33d40954199bddbb6a78f8f6f2b2082660f381cd2583ec860a6c2fa7c8400c08 \ 262 | --hash=sha256:40e1cbf69d6741b40f750f3cccc64326f927ac6145a9914d33879e586002350c \ 263 | --hash=sha256:46a057329938b08e5f0e12ea3d7aed3ecb20a0c34c4a324ef34e00cecdb88a12 \ 264 | --hash=sha256:4864f99aac207e3e45c5e26c6cbb0ad82917869abc2f156283be86c05286485c \ 265 | --hash=sha256:4c44e1a765b31e175c391f22e8fc73b2a2ece0e5e6ff042743d8109b5d2eff9f \ 266 | --hash=sha256:4cb283f630624ebb16c834e5ac3d7880831b07cbe76cb08ab7a271eeaeb8943e \ 267 | --hash=sha256:5008964885e8d23313c8e5ea0d44433be9bfd7e24482574e8cc43c02c02fc796 \ 268 | --hash=sha256:50a9f075eeda5097aa9a182bb3877fe1272875e45370368ac0ee16ab9e22d019 \ 269 | --hash=sha256:51630c92591d6d3fe488a7c706bd30a61594d144bac7dee20c8e1ce78294f474 \ 270 | --hash=sha256:5cc928cfe6c360c1df636cf7991ab96f059666ac7b40b75a769410cc6217df9c \ 271 | --hash=sha256:61f7203e2767800edee7a1e1040aaaf124a35ce0c7fe0883965c6b762defe598 \ 272 | --hash=sha256:66233ccd2a9371158d96e05d082043d47dadb18cbb294dc5accfdafc2e6b02a7 \ 273 | --hash=sha256:70fcac7b94f4c904152809a050164650ac81c08e62c27aa9f156ac518029ebbe \ 274 | --hash=sha256:714242ad0afa63a2e6dabd522ae22e1d76e07060b5af2ddda5474ba4f14c2c94 \ 275 | --hash=sha256:782f4f8662a2157c4190d0f99eaaebc602899e84fb1e562a944e5025929e351c \ 276 | --hash=sha256:7fc2b4edb938c8faa4b3c3ea90ca0dd89b7565a049e8e4e11b77e60e4ed2cc05 \ 277 | --hash=sha256:881d058c5ccbea7cc2c92085a11947b572498a27ef37d3eef4887f499054dca8 \ 278 | --hash=sha256:89dde0ac72a858a44a2feb8e43dc68c0c66f7857a23f806e81e1b7cc7044c9cf \ 279 | --hash=sha256:8cdbcbd687e576d48f7886157c95052825ca9948c0ed2afdc0134305067be88b \ 280 | --hash=sha256:8d6192c37a30a115f4663592861f50e130caed33efc4eec24d92ec881c92d771 \ 281 | --hash=sha256:96a41817d2c763b1d0b32675abeb9179aa2371c72aefdf74b2d2b99a1b92417b \ 282 | --hash=sha256:9bdbb7624d65dc0ed2ed8e954e79ab1724526f09b1efa88dcd9a1815bf28be5f \ 283 | --hash=sha256:9bf88004fe086c786dc56ef8dd6cb49c026833fdd6f42cb853008bce3f907148 \ 284 | --hash=sha256:a08920fa1a97d4b8ee5db2f31195de4a9def1a91bc003544eb3c9e6b8977960a \ 285 | --hash=sha256:a2f5a1f1080ccdc7cbaf1171b2cf384d852496fe81ddedeb882d42b85727f610 \ 286 | --hash=sha256:b04202453941a63b36876a7172b45366dc0cde10d5fd7855c0f4a4e673c0357a \ 287 | --hash=sha256:b38b3de8cff5bc70f8f9c615f51b48eff7313fc9aca354f09f81b73036e7ddfa \ 288 | --hash=sha256:b52d00d1793d290c81ad6a27058f5224a7d5f527867e5b580742e1bd211afeee \ 289 | --hash=sha256:b74ae837368cfffeb3f6b498688a123e6b960951be4dec0e869de77e7fa0439e \ 290 | --hash=sha256:be48496b0e00460717225e7680de57c38be1d8629dc09dadcd1b3389d70d942b \ 291 | --hash=sha256:c0e3155fc5335ec7b3b70f15230234e529ca3607b20a562b6c75fb1b1218874c \ 292 | --hash=sha256:c2392f5b5d84b71d853918687d806c1aa4308109e5ca158a16e16a6be71041eb \ 293 | --hash=sha256:c72956972e4b508dd39fdc7646637a791a9665b478e768ffa5f4fe42123d5de1 \ 294 | --hash=sha256:dc80c9c6b608bf98066a038e0172013a49cfa9a08d53335aefefda2c64fc68f4 \ 295 | --hash=sha256:e416c8baf925b5a1aff31f7f5aecc0060b25d50cce3a5a7255dc5cf2f1d4e5eb \ 296 | --hash=sha256:f8da84bbc61a4e92af54dc96344f328e5822d574f767e9b08e1602bb5ddc254a \ 297 | --hash=sha256:f900ed4ad7a0f1f05d35f955e0943944d5a75f607a836958c6b8ab2a81730ef2 \ 298 | --hash=sha256:fd6c6c29717724acf9fc1847c4515d57e4dc12762452457b9cb37461f30a81bb 299 | # via 300 | # google-ads 301 | # grpcio-status 302 | grpcio-status==1.54.2 \ 303 | --hash=sha256:2a7cb4838225f1b53bd0448a3008c5b5837941e1f3a0b13fa38768f08a7b68c2 \ 304 | --hash=sha256:3255cbec5b7c706caa3d4dd584606c080e6415e15631bb2f6215e2b70055836d 305 | # via google-ads 306 | httplib2==0.22.0 \ 307 | --hash=sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc \ 308 | --hash=sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81 309 | # via 310 | # google-api-python-client 311 | # google-auth-httplib2 312 | idna==3.4 \ 313 | --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \ 314 | --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2 315 | # via requests 316 | importlib-metadata==6.7.0 \ 317 | --hash=sha256:1aaf550d4f73e5d6783e7acb77aec43d49da8017410afae93822cc9cca98c4d4 \ 318 | --hash=sha256:cb52082e659e97afc5dac71e79de97d8681de3aa07ff18578330904a9d18e5b5 319 | # via streamlit 320 | jinja2==3.1.2 \ 321 | --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \ 322 | --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61 323 | # via 324 | # altair 325 | # pydeck 326 | jsonschema==4.17.3 \ 327 | --hash=sha256:0f864437ab8b6076ba6707453ef8f98a6a0d512a80e93f8abdb676f737ecb60d \ 328 | --hash=sha256:a870ad254da1a8ca84b6a2905cac29d265f805acc57af304784962a2aa6508f6 329 | # via altair 330 | markdown-it-py==3.0.0 \ 331 | --hash=sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1 \ 332 | --hash=sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb 333 | # via rich 334 | markupsafe==2.1.3 \ 335 | --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \ 336 | --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \ 337 | --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \ 338 | --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \ 339 | --hash=sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c \ 340 | --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \ 341 | --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \ 342 | --hash=sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb \ 343 | --hash=sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939 \ 344 | --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \ 345 | --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \ 346 | --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \ 347 | --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \ 348 | --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \ 349 | --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \ 350 | --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \ 351 | --hash=sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd \ 352 | --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \ 353 | --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \ 354 | --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \ 355 | --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \ 356 | --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \ 357 | --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \ 358 | --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \ 359 | --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \ 360 | --hash=sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007 \ 361 | --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \ 362 | --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \ 363 | --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \ 364 | --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \ 365 | --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \ 366 | --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \ 367 | --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \ 368 | --hash=sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1 \ 369 | --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \ 370 | --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \ 371 | --hash=sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c \ 372 | --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \ 373 | --hash=sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823 \ 374 | --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \ 375 | --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \ 376 | --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \ 377 | --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \ 378 | --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \ 379 | --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \ 380 | --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \ 381 | --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \ 382 | --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \ 383 | --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \ 384 | --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \ 385 | --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \ 386 | --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \ 387 | --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \ 388 | --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \ 389 | --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \ 390 | --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \ 391 | --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \ 392 | --hash=sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc \ 393 | --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 \ 394 | --hash=sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11 395 | # via jinja2 396 | mdurl==0.1.2 \ 397 | --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ 398 | --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba 399 | # via markdown-it-py 400 | numpy==1.25.0 \ 401 | --hash=sha256:0ac6edfb35d2a99aaf102b509c8e9319c499ebd4978df4971b94419a116d0790 \ 402 | --hash=sha256:26815c6c8498dc49d81faa76d61078c4f9f0859ce7817919021b9eba72b425e3 \ 403 | --hash=sha256:4aedd08f15d3045a4e9c648f1e04daca2ab1044256959f1f95aafeeb3d794c16 \ 404 | --hash=sha256:4c69fe5f05eea336b7a740e114dec995e2f927003c30702d896892403df6dbf0 \ 405 | --hash=sha256:5177310ac2e63d6603f659fadc1e7bab33dd5a8db4e0596df34214eeab0fee3b \ 406 | --hash=sha256:5aa48bebfb41f93043a796128854b84407d4df730d3fb6e5dc36402f5cd594c0 \ 407 | --hash=sha256:5b1b90860bf7d8a8c313b372d4f27343a54f415b20fb69dd601b7efe1029c91e \ 408 | --hash=sha256:6c284907e37f5e04d2412950960894b143a648dea3f79290757eb878b91acbd1 \ 409 | --hash=sha256:6d183b5c58513f74225c376643234c369468e02947b47942eacbb23c1671f25d \ 410 | --hash=sha256:7412125b4f18aeddca2ecd7219ea2d2708f697943e6f624be41aa5f8a9852cc4 \ 411 | --hash=sha256:7cd981ccc0afe49b9883f14761bb57c964df71124dcd155b0cba2b591f0d64b9 \ 412 | --hash=sha256:85cdae87d8c136fd4da4dad1e48064d700f63e923d5af6c8c782ac0df8044542 \ 413 | --hash=sha256:8aa130c3042052d656751df5e81f6d61edff3e289b5994edcf77f54118a8d9f4 \ 414 | --hash=sha256:95367ccd88c07af21b379be1725b5322362bb83679d36691f124a16357390153 \ 415 | --hash=sha256:9c7211d7920b97aeca7b3773a6783492b5b93baba39e7c36054f6e749fc7490c \ 416 | --hash=sha256:9e3f2b96e3b63c978bc29daaa3700c028fe3f049ea3031b58aa33fe2a5809d24 \ 417 | --hash=sha256:b76aa836a952059d70a2788a2d98cb2a533ccd46222558b6970348939e55fc24 \ 418 | --hash=sha256:b792164e539d99d93e4e5e09ae10f8cbe5466de7d759fc155e075237e0c274e4 \ 419 | --hash=sha256:c0dc071017bc00abb7d7201bac06fa80333c6314477b3d10b52b58fa6a6e38f6 \ 420 | --hash=sha256:cc3fda2b36482891db1060f00f881c77f9423eead4c3579629940a3e12095fe8 \ 421 | --hash=sha256:d6b267f349a99d3908b56645eebf340cb58f01bd1e773b4eea1a905b3f0e4208 \ 422 | --hash=sha256:d76a84998c51b8b68b40448ddd02bd1081bb33abcdc28beee6cd284fe11036c6 \ 423 | --hash=sha256:e559c6afbca484072a98a51b6fa466aae785cfe89b69e8b856c3191bc8872a82 \ 424 | --hash=sha256:ecc68f11404930e9c7ecfc937aa423e1e50158317bf67ca91736a9864eae0232 \ 425 | --hash=sha256:f1accae9a28dc3cda46a91de86acf69de0d1b5f4edd44a9b0c3ceb8036dfff19 426 | # via 427 | # altair 428 | # pandas 429 | # pyarrow 430 | # pydeck 431 | # streamlit 432 | oauthlib==3.2.2 \ 433 | --hash=sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca \ 434 | --hash=sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918 435 | # via requests-oauthlib 436 | packaging==23.1 \ 437 | --hash=sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61 \ 438 | --hash=sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f 439 | # via streamlit 440 | pandas==2.0.2 \ 441 | --hash=sha256:02755de164da6827764ceb3bbc5f64b35cb12394b1024fdf88704d0fa06e0e2f \ 442 | --hash=sha256:0a1e0576611641acde15c2322228d138258f236d14b749ad9af498ab69089e2d \ 443 | --hash=sha256:1eb09a242184092f424b2edd06eb2b99d06dc07eeddff9929e8667d4ed44e181 \ 444 | --hash=sha256:30a89d0fec4263ccbf96f68592fd668939481854d2ff9da709d32a047689393b \ 445 | --hash=sha256:50e451932b3011b61d2961b4185382c92cc8c6ee4658dcd4f320687bb2d000ee \ 446 | --hash=sha256:51a93d422fbb1bd04b67639ba4b5368dffc26923f3ea32a275d2cc450f1d1c86 \ 447 | --hash=sha256:598e9020d85a8cdbaa1815eb325a91cfff2bb2b23c1442549b8a3668e36f0f77 \ 448 | --hash=sha256:66d00300f188fa5de73f92d5725ced162488f6dc6ad4cecfe4144ca29debe3b8 \ 449 | --hash=sha256:69167693cb8f9b3fc060956a5d0a0a8dbfed5f980d9fd2c306fb5b9c855c814c \ 450 | --hash=sha256:6d6d10c2142d11d40d6e6c0a190b1f89f525bcf85564707e31b0a39e3b398e08 \ 451 | --hash=sha256:713f2f70abcdade1ddd68fc91577cb090b3544b07ceba78a12f799355a13ee44 \ 452 | --hash=sha256:7376e13d28eb16752c398ca1d36ccfe52bf7e887067af9a0474de6331dd948d2 \ 453 | --hash=sha256:77550c8909ebc23e56a89f91b40ad01b50c42cfbfab49b3393694a50549295ea \ 454 | --hash=sha256:7b21cb72958fc49ad757685db1919021d99650d7aaba676576c9e88d3889d456 \ 455 | --hash=sha256:9ebb9f1c22ddb828e7fd017ea265a59d80461d5a79154b49a4207bd17514d122 \ 456 | --hash=sha256:a18e5c72b989ff0f7197707ceddc99828320d0ca22ab50dd1b9e37db45b010c0 \ 457 | --hash=sha256:a6b5f14cd24a2ed06e14255ff40fe2ea0cfaef79a8dd68069b7ace74bd6acbba \ 458 | --hash=sha256:b42b120458636a981077cfcfa8568c031b3e8709701315e2bfa866324a83efa8 \ 459 | --hash=sha256:c4af689352c4fe3d75b2834933ee9d0ccdbf5d7a8a7264f0ce9524e877820c08 \ 460 | --hash=sha256:c7319b6e68de14e6209460f72a8d1ef13c09fb3d3ef6c37c1e65b35d50b5c145 \ 461 | --hash=sha256:cf3f0c361a4270185baa89ec7ab92ecaa355fe783791457077473f974f654df5 \ 462 | --hash=sha256:dd46bde7309088481b1cf9c58e3f0e204b9ff9e3244f441accd220dd3365ce7c \ 463 | --hash=sha256:dd5476b6c3fe410ee95926873f377b856dbc4e81a9c605a0dc05aaccc6a7c6c6 \ 464 | --hash=sha256:e69140bc2d29a8556f55445c15f5794490852af3de0f609a24003ef174528b79 \ 465 | --hash=sha256:f908a77cbeef9bbd646bd4b81214cbef9ac3dda4181d5092a4aa9797d1bc7774 466 | # via 467 | # altair 468 | # streamlit 469 | pillow==9.5.0 \ 470 | --hash=sha256:07999f5834bdc404c442146942a2ecadd1cb6292f5229f4ed3b31e0a108746b1 \ 471 | --hash=sha256:0852ddb76d85f127c135b6dd1f0bb88dbb9ee990d2cd9aa9e28526c93e794fba \ 472 | --hash=sha256:1781a624c229cb35a2ac31cc4a77e28cafc8900733a864870c49bfeedacd106a \ 473 | --hash=sha256:1e7723bd90ef94eda669a3c2c19d549874dd5badaeefabefd26053304abe5799 \ 474 | --hash=sha256:229e2c79c00e85989a34b5981a2b67aa079fd08c903f0aaead522a1d68d79e51 \ 475 | --hash=sha256:22baf0c3cf0c7f26e82d6e1adf118027afb325e703922c8dfc1d5d0156bb2eeb \ 476 | --hash=sha256:252a03f1bdddce077eff2354c3861bf437c892fb1832f75ce813ee94347aa9b5 \ 477 | --hash=sha256:2dfaaf10b6172697b9bceb9a3bd7b951819d1ca339a5ef294d1f1ac6d7f63270 \ 478 | --hash=sha256:322724c0032af6692456cd6ed554bb85f8149214d97398bb80613b04e33769f6 \ 479 | --hash=sha256:35f6e77122a0c0762268216315bf239cf52b88865bba522999dc38f1c52b9b47 \ 480 | --hash=sha256:375f6e5ee9620a271acb6820b3d1e94ffa8e741c0601db4c0c4d3cb0a9c224bf \ 481 | --hash=sha256:3ded42b9ad70e5f1754fb7c2e2d6465a9c842e41d178f262e08b8c85ed8a1d8e \ 482 | --hash=sha256:432b975c009cf649420615388561c0ce7cc31ce9b2e374db659ee4f7d57a1f8b \ 483 | --hash=sha256:482877592e927fd263028c105b36272398e3e1be3269efda09f6ba21fd83ec66 \ 484 | --hash=sha256:489f8389261e5ed43ac8ff7b453162af39c3e8abd730af8363587ba64bb2e865 \ 485 | --hash=sha256:54f7102ad31a3de5666827526e248c3530b3a33539dbda27c6843d19d72644ec \ 486 | --hash=sha256:560737e70cb9c6255d6dcba3de6578a9e2ec4b573659943a5e7e4af13f298f5c \ 487 | --hash=sha256:5671583eab84af046a397d6d0ba25343c00cd50bce03787948e0fff01d4fd9b1 \ 488 | --hash=sha256:5ba1b81ee69573fe7124881762bb4cd2e4b6ed9dd28c9c60a632902fe8db8b38 \ 489 | --hash=sha256:5d4ebf8e1db4441a55c509c4baa7a0587a0210f7cd25fcfe74dbbce7a4bd1906 \ 490 | --hash=sha256:60037a8db8750e474af7ffc9faa9b5859e6c6d0a50e55c45576bf28be7419705 \ 491 | --hash=sha256:608488bdcbdb4ba7837461442b90ea6f3079397ddc968c31265c1e056964f1ef \ 492 | --hash=sha256:6608ff3bf781eee0cd14d0901a2b9cc3d3834516532e3bd673a0a204dc8615fc \ 493 | --hash=sha256:662da1f3f89a302cc22faa9f14a262c2e3951f9dbc9617609a47521c69dd9f8f \ 494 | --hash=sha256:7002d0797a3e4193c7cdee3198d7c14f92c0836d6b4a3f3046a64bd1ce8df2bf \ 495 | --hash=sha256:763782b2e03e45e2c77d7779875f4432e25121ef002a41829d8868700d119392 \ 496 | --hash=sha256:77165c4a5e7d5a284f10a6efaa39a0ae8ba839da344f20b111d62cc932fa4e5d \ 497 | --hash=sha256:7c9af5a3b406a50e313467e3565fc99929717f780164fe6fbb7704edba0cebbe \ 498 | --hash=sha256:7ec6f6ce99dab90b52da21cf0dc519e21095e332ff3b399a357c187b1a5eee32 \ 499 | --hash=sha256:833b86a98e0ede388fa29363159c9b1a294b0905b5128baf01db683672f230f5 \ 500 | --hash=sha256:84a6f19ce086c1bf894644b43cd129702f781ba5751ca8572f08aa40ef0ab7b7 \ 501 | --hash=sha256:8507eda3cd0608a1f94f58c64817e83ec12fa93a9436938b191b80d9e4c0fc44 \ 502 | --hash=sha256:85ec677246533e27770b0de5cf0f9d6e4ec0c212a1f89dfc941b64b21226009d \ 503 | --hash=sha256:8aca1152d93dcc27dc55395604dcfc55bed5f25ef4c98716a928bacba90d33a3 \ 504 | --hash=sha256:8d935f924bbab8f0a9a28404422da8af4904e36d5c33fc6f677e4c4485515625 \ 505 | --hash=sha256:8f36397bf3f7d7c6a3abdea815ecf6fd14e7fcd4418ab24bae01008d8d8ca15e \ 506 | --hash=sha256:91ec6fe47b5eb5a9968c79ad9ed78c342b1f97a091677ba0e012701add857829 \ 507 | --hash=sha256:965e4a05ef364e7b973dd17fc765f42233415974d773e82144c9bbaaaea5d089 \ 508 | --hash=sha256:96e88745a55b88a7c64fa49bceff363a1a27d9a64e04019c2281049444a571e3 \ 509 | --hash=sha256:99eb6cafb6ba90e436684e08dad8be1637efb71c4f2180ee6b8f940739406e78 \ 510 | --hash=sha256:9adf58f5d64e474bed00d69bcd86ec4bcaa4123bfa70a65ce72e424bfb88ed96 \ 511 | --hash=sha256:9b1af95c3a967bf1da94f253e56b6286b50af23392a886720f563c547e48e964 \ 512 | --hash=sha256:a0aa9417994d91301056f3d0038af1199eb7adc86e646a36b9e050b06f526597 \ 513 | --hash=sha256:a0f9bb6c80e6efcde93ffc51256d5cfb2155ff8f78292f074f60f9e70b942d99 \ 514 | --hash=sha256:a127ae76092974abfbfa38ca2d12cbeddcdeac0fb71f9627cc1135bedaf9d51a \ 515 | --hash=sha256:aaf305d6d40bd9632198c766fb64f0c1a83ca5b667f16c1e79e1661ab5060140 \ 516 | --hash=sha256:aca1c196f407ec7cf04dcbb15d19a43c507a81f7ffc45b690899d6a76ac9fda7 \ 517 | --hash=sha256:ace6ca218308447b9077c14ea4ef381ba0b67ee78d64046b3f19cf4e1139ad16 \ 518 | --hash=sha256:b416f03d37d27290cb93597335a2f85ed446731200705b22bb927405320de903 \ 519 | --hash=sha256:bf548479d336726d7a0eceb6e767e179fbde37833ae42794602631a070d630f1 \ 520 | --hash=sha256:c1170d6b195555644f0616fd6ed929dfcf6333b8675fcca044ae5ab110ded296 \ 521 | --hash=sha256:c380b27d041209b849ed246b111b7c166ba36d7933ec6e41175fd15ab9eb1572 \ 522 | --hash=sha256:c446d2245ba29820d405315083d55299a796695d747efceb5717a8b450324115 \ 523 | --hash=sha256:c830a02caeb789633863b466b9de10c015bded434deb3ec87c768e53752ad22a \ 524 | --hash=sha256:cb841572862f629b99725ebaec3287fc6d275be9b14443ea746c1dd325053cbd \ 525 | --hash=sha256:cfa4561277f677ecf651e2b22dc43e8f5368b74a25a8f7d1d4a3a243e573f2d4 \ 526 | --hash=sha256:cfcc2c53c06f2ccb8976fb5c71d448bdd0a07d26d8e07e321c103416444c7ad1 \ 527 | --hash=sha256:d3c6b54e304c60c4181da1c9dadf83e4a54fd266a99c70ba646a9baa626819eb \ 528 | --hash=sha256:d3d403753c9d5adc04d4694d35cf0391f0f3d57c8e0030aac09d7678fa8030aa \ 529 | --hash=sha256:d9c206c29b46cfd343ea7cdfe1232443072bbb270d6a46f59c259460db76779a \ 530 | --hash=sha256:e49eb4e95ff6fd7c0c402508894b1ef0e01b99a44320ba7d8ecbabefddcc5569 \ 531 | --hash=sha256:f8286396b351785801a976b1e85ea88e937712ee2c3ac653710a4a57a8da5d9c \ 532 | --hash=sha256:f8fc330c3370a81bbf3f88557097d1ea26cd8b019d6433aa59f71195f5ddebbf \ 533 | --hash=sha256:fbd359831c1657d69bb81f0db962905ee05e5e9451913b18b831febfe0519082 \ 534 | --hash=sha256:fe7e1c262d3392afcf5071df9afa574544f28eac825284596ac6db56e6d11062 \ 535 | --hash=sha256:fed1e1cf6a42577953abbe8e6cf2fe2f566daebde7c34724ec8803c4c0cda579 536 | # via streamlit 537 | proto-plus==1.22.2 \ 538 | --hash=sha256:0e8cda3d5a634d9895b75c573c9352c16486cb75deb0e078b5fda34db4243165 \ 539 | --hash=sha256:de34e52d6c9c6fcd704192f09767cb561bb4ee64e70eede20b0834d841f0be4d 540 | # via google-ads 541 | protobuf==4.23.3 \ 542 | --hash=sha256:0149053336a466e3e0b040e54d0b615fc71de86da66791c592cc3c8d18150bf8 \ 543 | --hash=sha256:08fe19d267608d438aa37019236db02b306e33f6b9902c3163838b8e75970223 \ 544 | --hash=sha256:29660574cd769f2324a57fb78127cda59327eb6664381ecfe1c69731b83e8288 \ 545 | --hash=sha256:2991f5e7690dab569f8f81702e6700e7364cc3b5e572725098215d3da5ccc6ac \ 546 | --hash=sha256:3b01a5274ac920feb75d0b372d901524f7e3ad39c63b1a2d55043f3887afe0c1 \ 547 | --hash=sha256:3bcbeb2bf4bb61fe960dd6e005801a23a43578200ea8ceb726d1f6bd0e562ba1 \ 548 | --hash=sha256:447b9786ac8e50ae72cae7a2eec5c5df6a9dbf9aa6f908f1b8bda6032644ea62 \ 549 | --hash=sha256:514b6bbd54a41ca50c86dd5ad6488afe9505901b3557c5e0f7823a0cf67106fb \ 550 | --hash=sha256:5cb9e41188737f321f4fce9a4337bf40a5414b8d03227e1d9fbc59bc3a216e35 \ 551 | --hash=sha256:7a92beb30600332a52cdadbedb40d33fd7c8a0d7f549c440347bc606fb3fe34b \ 552 | --hash=sha256:84ea0bd90c2fdd70ddd9f3d3fc0197cc24ecec1345856c2b5ba70e4d99815359 \ 553 | --hash=sha256:aca6e86a08c5c5962f55eac9b5bd6fce6ed98645d77e8bfc2b952ecd4a8e4f6a \ 554 | --hash=sha256:cc14358a8742c4e06b1bfe4be1afbdf5c9f6bd094dff3e14edb78a1513893ff5 555 | # via 556 | # -r requirements.in 557 | # google-ads 558 | # google-api-core 559 | # googleapis-common-protos 560 | # grpcio-status 561 | # proto-plus 562 | # streamlit 563 | pyarrow==12.0.1 \ 564 | --hash=sha256:051f9f5ccf585f12d7de836e50965b3c235542cc896959320d9776ab93f3b33d \ 565 | --hash=sha256:1887bdae17ec3b4c046fcf19951e71b6a619f39fa674f9881216173566c8f718 \ 566 | --hash=sha256:2d3c4cbbf81e6dd23fe921bc91dc4619ea3b79bc58ef10bce0f49bdafb103daf \ 567 | --hash=sha256:345e1828efdbd9aa4d4de7d5676778aba384a2c3add896d995b23d368e60e5af \ 568 | --hash=sha256:3de26da901216149ce086920547dfff5cd22818c9eab67ebc41e863a5883bac7 \ 569 | --hash=sha256:43364daec02f69fec89d2315f7fbfbeec956e0d991cbbef471681bd77875c40f \ 570 | --hash=sha256:459a1c0ed2d68671188b2118c63bac91eaef6fc150c77ddd8a583e3c795737bf \ 571 | --hash=sha256:6251e38470da97a5b2e00de5c6a049149f7b2bd62f12fa5dbb9ac674119ba71a \ 572 | --hash=sha256:6895b5fb74289d055c43db3af0de6e16b07586c45763cb5e558d38b86a91e3a7 \ 573 | --hash=sha256:6d288029a94a9bb5407ceebdd7110ba398a00412c5b0155ee9813a40d246c5df \ 574 | --hash=sha256:749be7fd2ff260683f9cc739cb862fb11be376de965a2a8ccbf2693b098db6c7 \ 575 | --hash=sha256:85e705e33eaf666bbe508a16fd5ba27ca061e177916b7a317ba5a51bee43384c \ 576 | --hash=sha256:8d6009fdf8986332b2169314da482baed47ac053311c8934ac6651e614deacd6 \ 577 | --hash=sha256:9120c3eb2b1f6f516a3b7a9714ed860882d9ef98c4b17edcdc91d95b7528db60 \ 578 | --hash=sha256:a3c63124fc26bf5f95f508f5d04e1ece8cc23a8b0af2a1e6ab2b1ec3fdc91b24 \ 579 | --hash=sha256:b13329f79fa4472324f8d32dc1b1216616d09bd1e77cfb13104dec5463632c36 \ 580 | --hash=sha256:bb656150d3d12ec1396f6dde542db1675a95c0cc8366d507347b0beed96e87ca \ 581 | --hash=sha256:be2757e9275875d2a9c6e6052ac7957fbbfc7bc7370e4a036a9b893e96fedaba \ 582 | --hash=sha256:c780f4dc40460015d80fcd6a6140de80b615349ed68ef9adb653fe351778c9b3 \ 583 | --hash=sha256:cce317fc96e5b71107bf1f9f184d5e54e2bd14bbf3f9a3d62819961f0af86fec \ 584 | --hash=sha256:cdacf515ec276709ac8042c7d9bd5be83b4f5f39c6c037a17a60d7ebfd92c890 \ 585 | --hash=sha256:ce4aebdf412bd0eeb800d8e47db854f9f9f7e2f5a0220440acf219ddfddd4f63 \ 586 | --hash=sha256:cf812306d66f40f69e684300f7af5111c11f6e0d89d6b733e05a3de44961529d \ 587 | --hash=sha256:e0d8730c7f6e893f6db5d5b86eda42c0a130842d101992b581e2138e4d5663d3 \ 588 | --hash=sha256:e2c9cb8eeabbadf5fcfc3d1ddea616c7ce893db2ce4dcef0ac13b099ad7ca082 589 | # via streamlit 590 | pyasn1==0.5.0 \ 591 | --hash=sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57 \ 592 | --hash=sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde 593 | # via 594 | # pyasn1-modules 595 | # rsa 596 | pyasn1-modules==0.3.0 \ 597 | --hash=sha256:5bd01446b736eb9d31512a30d46c1ac3395d676c6f3cafa4c03eb54b9925631c \ 598 | --hash=sha256:d3ccd6ed470d9ffbc716be08bd90efbd44d0734bc9303818f7336070984a162d 599 | # via google-auth 600 | pydeck==0.8.0 \ 601 | --hash=sha256:07edde833f7cfcef6749124351195aa7dcd24663d4909fd7898dbd0b6fbc01ec \ 602 | --hash=sha256:a8fa7757c6f24bba033af39db3147cb020eef44012ba7e60d954de187f9ed4d5 603 | # via streamlit 604 | pygments==2.15.1 \ 605 | --hash=sha256:8ace4d3c1dd481894b2005f560ead0f9f19ee64fe983366be1a21e171d12775c \ 606 | --hash=sha256:db2db3deb4b4179f399a09054b023b6a586b76499d36965813c71aa8ed7b5fd1 607 | # via rich 608 | pympler==1.0.1 \ 609 | --hash=sha256:993f1a3599ca3f4fcd7160c7545ad06310c9e12f70174ae7ae8d4e25f6c5d3fa \ 610 | --hash=sha256:d260dda9ae781e1eab6ea15bacb84015849833ba5555f141d2d9b7b7473b307d 611 | # via streamlit 612 | pyparsing==3.1.0 \ 613 | --hash=sha256:d554a96d1a7d3ddaf7183104485bc19fd80543ad6ac5bdb6426719d766fb06c1 \ 614 | --hash=sha256:edb662d6fe322d6e990b1594b5feaeadf806803359e3d4d42f11e295e588f0ea 615 | # via httplib2 616 | pyrsistent==0.19.3 \ 617 | --hash=sha256:016ad1afadf318eb7911baa24b049909f7f3bb2c5b1ed7b6a8f21db21ea3faa8 \ 618 | --hash=sha256:1a2994773706bbb4995c31a97bc94f1418314923bd1048c6d964837040376440 \ 619 | --hash=sha256:20460ac0ea439a3e79caa1dbd560344b64ed75e85d8703943e0b66c2a6150e4a \ 620 | --hash=sha256:3311cb4237a341aa52ab8448c27e3a9931e2ee09561ad150ba94e4cfd3fc888c \ 621 | --hash=sha256:3a8cb235fa6d3fd7aae6a4f1429bbb1fec1577d978098da1252f0489937786f3 \ 622 | --hash=sha256:3ab2204234c0ecd8b9368dbd6a53e83c3d4f3cab10ecaf6d0e772f456c442393 \ 623 | --hash=sha256:42ac0b2f44607eb92ae88609eda931a4f0dfa03038c44c772e07f43e738bcac9 \ 624 | --hash=sha256:49c32f216c17148695ca0e02a5c521e28a4ee6c5089f97e34fe24163113722da \ 625 | --hash=sha256:4b774f9288dda8d425adb6544e5903f1fb6c273ab3128a355c6b972b7df39dcf \ 626 | --hash=sha256:4c18264cb84b5e68e7085a43723f9e4c1fd1d935ab240ce02c0324a8e01ccb64 \ 627 | --hash=sha256:5a474fb80f5e0d6c9394d8db0fc19e90fa540b82ee52dba7d246a7791712f74a \ 628 | --hash=sha256:64220c429e42a7150f4bfd280f6f4bb2850f95956bde93c6fda1b70507af6ef3 \ 629 | --hash=sha256:878433581fc23e906d947a6814336eee031a00e6defba224234169ae3d3d6a98 \ 630 | --hash=sha256:99abb85579e2165bd8522f0c0138864da97847875ecbd45f3e7e2af569bfc6f2 \ 631 | --hash=sha256:a2471f3f8693101975b1ff85ffd19bb7ca7dd7c38f8a81701f67d6b4f97b87d8 \ 632 | --hash=sha256:aeda827381f5e5d65cced3024126529ddc4289d944f75e090572c77ceb19adbf \ 633 | --hash=sha256:b735e538f74ec31378f5a1e3886a26d2ca6351106b4dfde376a26fc32a044edc \ 634 | --hash=sha256:c147257a92374fde8498491f53ffa8f4822cd70c0d85037e09028e478cababb7 \ 635 | --hash=sha256:c4db1bd596fefd66b296a3d5d943c94f4fac5bcd13e99bffe2ba6a759d959a28 \ 636 | --hash=sha256:c74bed51f9b41c48366a286395c67f4e894374306b197e62810e0fdaf2364da2 \ 637 | --hash=sha256:c9bb60a40a0ab9aba40a59f68214eed5a29c6274c83b2cc206a359c4a89fa41b \ 638 | --hash=sha256:cc5d149f31706762c1f8bda2e8c4f8fead6e80312e3692619a75301d3dbb819a \ 639 | --hash=sha256:ccf0d6bd208f8111179f0c26fdf84ed7c3891982f2edaeae7422575f47e66b64 \ 640 | --hash=sha256:e42296a09e83028b3476f7073fcb69ffebac0e66dbbfd1bd847d61f74db30f19 \ 641 | --hash=sha256:e8f2b814a3dc6225964fa03d8582c6e0b6650d68a232df41e3cc1b66a5d2f8d1 \ 642 | --hash=sha256:f0774bf48631f3a20471dd7c5989657b639fd2d285b861237ea9e82c36a415a9 \ 643 | --hash=sha256:f0e7c4b2f77593871e918be000b96c8107da48444d57005b6a6bc61fb4331b2c 644 | # via jsonschema 645 | python-dateutil==2.8.2 \ 646 | --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ 647 | --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9 648 | # via 649 | # -r requirements.in 650 | # pandas 651 | # streamlit 652 | pytz==2023.3 \ 653 | --hash=sha256:1d8ce29db189191fb55338ee6d0387d82ab59f3d00eac103412d64e0ebd0c588 \ 654 | --hash=sha256:a151b3abb88eda1d4e34a9814df37de2a80e301e68ba0fd856fb9b46bfbbbffb 655 | # via pandas 656 | pytz-deprecation-shim==0.1.0.post0 \ 657 | --hash=sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6 \ 658 | --hash=sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d 659 | # via tzlocal 660 | pyyaml==6.0 \ 661 | --hash=sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf \ 662 | --hash=sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293 \ 663 | --hash=sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b \ 664 | --hash=sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57 \ 665 | --hash=sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b \ 666 | --hash=sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4 \ 667 | --hash=sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07 \ 668 | --hash=sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba \ 669 | --hash=sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9 \ 670 | --hash=sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287 \ 671 | --hash=sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513 \ 672 | --hash=sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0 \ 673 | --hash=sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782 \ 674 | --hash=sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0 \ 675 | --hash=sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92 \ 676 | --hash=sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f \ 677 | --hash=sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2 \ 678 | --hash=sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc \ 679 | --hash=sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1 \ 680 | --hash=sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c \ 681 | --hash=sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86 \ 682 | --hash=sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4 \ 683 | --hash=sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c \ 684 | --hash=sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34 \ 685 | --hash=sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b \ 686 | --hash=sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d \ 687 | --hash=sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c \ 688 | --hash=sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb \ 689 | --hash=sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7 \ 690 | --hash=sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737 \ 691 | --hash=sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3 \ 692 | --hash=sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d \ 693 | --hash=sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358 \ 694 | --hash=sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53 \ 695 | --hash=sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78 \ 696 | --hash=sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803 \ 697 | --hash=sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a \ 698 | --hash=sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f \ 699 | --hash=sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174 \ 700 | --hash=sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5 701 | # via 702 | # -r requirements.in 703 | # google-ads 704 | requests==2.31.0 \ 705 | --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \ 706 | --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1 707 | # via 708 | # google-api-core 709 | # google-cloud-storage 710 | # requests-oauthlib 711 | # streamlit 712 | requests-oauthlib==1.3.1 \ 713 | --hash=sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5 \ 714 | --hash=sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a 715 | # via google-auth-oauthlib 716 | rich==13.4.2 \ 717 | --hash=sha256:8f87bc7ee54675732fa66a05ebfe489e27264caeeff3728c945d25971b6485ec \ 718 | --hash=sha256:d653d6bccede5844304c605d5aac802c7cf9621efd700b46c7ec2b51ea914898 719 | # via streamlit 720 | rsa==4.9 \ 721 | --hash=sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7 \ 722 | --hash=sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21 723 | # via google-auth 724 | six==1.16.0 \ 725 | --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ 726 | --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 727 | # via 728 | # google-auth 729 | # google-auth-httplib2 730 | # python-dateutil 731 | smmap==5.0.0 \ 732 | --hash=sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94 \ 733 | --hash=sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936 734 | # via gitdb 735 | streamlit==1.23.1 \ 736 | --hash=sha256:02cd55a95acd20d73ff03866956ba6e98d71041ee4728dbfef05cf17069d1bb9 \ 737 | --hash=sha256:af95a78f9d291f12f779cf66a2bdadfafd8a46fe4fcc6ed281492640ed37bf1e 738 | # via -r requirements.in 739 | tenacity==8.2.2 \ 740 | --hash=sha256:2f277afb21b851637e8f52e6a613ff08734c347dc19ade928e519d7d2d8569b0 \ 741 | --hash=sha256:43af037822bd0029025877f3b2d97cc4d7bb0c2991000a3d59d71517c5c969e0 742 | # via streamlit 743 | toml==0.10.2 \ 744 | --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ 745 | --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f 746 | # via streamlit 747 | toolz==0.12.0 \ 748 | --hash=sha256:2059bd4148deb1884bb0eb770a3cde70e7f954cfbbdc2285f1f2de01fd21eb6f \ 749 | --hash=sha256:88c570861c440ee3f2f6037c4654613228ff40c93a6c25e0eba70d17282c6194 750 | # via altair 751 | tornado==6.3.2 \ 752 | --hash=sha256:05615096845cf50a895026f749195bf0b10b8909f9be672f50b0fe69cba368e4 \ 753 | --hash=sha256:0c325e66c8123c606eea33084976c832aa4e766b7dff8aedd7587ea44a604cdf \ 754 | --hash=sha256:29e71c847a35f6e10ca3b5c2990a52ce38b233019d8e858b755ea6ce4dcdd19d \ 755 | --hash=sha256:4b927c4f19b71e627b13f3db2324e4ae660527143f9e1f2e2fb404f3a187e2ba \ 756 | --hash=sha256:5b17b1cf5f8354efa3d37c6e28fdfd9c1c1e5122f2cb56dac121ac61baa47cbe \ 757 | --hash=sha256:6a0848f1aea0d196a7c4f6772197cbe2abc4266f836b0aac76947872cd29b411 \ 758 | --hash=sha256:7efcbcc30b7c654eb6a8c9c9da787a851c18f8ccd4a5a3a95b05c7accfa068d2 \ 759 | --hash=sha256:834ae7540ad3a83199a8da8f9f2d383e3c3d5130a328889e4cc991acc81e87a0 \ 760 | --hash=sha256:b46a6ab20f5c7c1cb949c72c1994a4585d2eaa0be4853f50a03b5031e964fc7c \ 761 | --hash=sha256:c2de14066c4a38b4ecbbcd55c5cc4b5340eb04f1c5e81da7451ef555859c833f \ 762 | --hash=sha256:c367ab6c0393d71171123ca5515c61ff62fe09024fa6bf299cd1339dc9456829 763 | # via streamlit 764 | typing-extensions==4.6.3 \ 765 | --hash=sha256:88a4153d8505aabbb4e13aacb7c486c2b4a33ca3b3f807914a9b4c844c471c26 \ 766 | --hash=sha256:d91d5919357fe7f681a9f2b5b4cb2a5f1ef0a1e9f59c4d8ff0d3491e05c0ffd5 767 | # via 768 | # altair 769 | # streamlit 770 | tzdata==2023.3 \ 771 | --hash=sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a \ 772 | --hash=sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda 773 | # via 774 | # pandas 775 | # pytz-deprecation-shim 776 | tzlocal==4.3 \ 777 | --hash=sha256:3f21d09e1b2aa9f2dacca12da240ca37de3ba5237a93addfd6d593afe9073355 \ 778 | --hash=sha256:b44c4388f3d34f25862cfbb387578a4d70fec417649da694a132f628a23367e2 779 | # via streamlit 780 | uritemplate==4.1.1 \ 781 | --hash=sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0 \ 782 | --hash=sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e 783 | # via google-api-python-client 784 | urllib3==1.26.16 \ 785 | --hash=sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f \ 786 | --hash=sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14 787 | # via 788 | # google-auth 789 | # requests 790 | validators==0.20.0 \ 791 | --hash=sha256:24148ce4e64100a2d5e267233e23e7afeb55316b47d30faae7eb6e7292bc226a 792 | # via streamlit 793 | zipp==3.15.0 \ 794 | --hash=sha256:112929ad649da941c23de50f356a2b5570c954b65150642bccdd66bf194d224b \ 795 | --hash=sha256:48904fc76a60e542af151aded95726c1a5c34ed43ab4134b597665c86d7ad556 796 | # via importlib-metadata 797 | 798 | # WARNING: The following packages were not pinned, but pip requires them to be 799 | # pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag. 800 | # setuptools 801 | --------------------------------------------------------------------------------