Spaces:
Sleeping
Sleeping
Update worker_script.py
Browse files- worker_script.py +170 -128
worker_script.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
| 1 |
import os
|
| 2 |
import sys
|
| 3 |
-
import time
|
| 4 |
|
| 5 |
-
#
|
|
|
|
|
|
|
| 6 |
try:
|
| 7 |
import sentence_transformers
|
| 8 |
import pyarrow
|
|
@@ -13,36 +14,57 @@ except ImportError:
|
|
| 13 |
|
| 14 |
import requests
|
| 15 |
import pandas as pd
|
| 16 |
-
import numpy as np
|
| 17 |
-
import pyarrow.parquet as pq
|
| 18 |
-
import pyarrow as pa
|
| 19 |
-
import gc
|
| 20 |
import hashlib
|
| 21 |
import re
|
| 22 |
-
import json
|
| 23 |
from datetime import datetime
|
| 24 |
from typing import Set
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
from sentence_transformers import SentenceTransformer
|
| 26 |
from huggingface_hub import hf_hub_download, HfApi
|
| 27 |
|
| 28 |
-
#
|
| 29 |
-
|
| 30 |
-
|
| 31 |
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
|
| 36 |
class GrantsDataUpdater:
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
self.temp_new_csv = "temp_new_data.csv"
|
| 39 |
-
|
| 40 |
-
|
| 41 |
def normalize_and_hash(self, text):
|
| 42 |
if not isinstance(text, str): text = ""
|
| 43 |
clean_text = re.sub(r'\s+', '', text).lower()
|
| 44 |
return hashlib.md5(clean_text.encode('utf-8')).hexdigest()
|
| 45 |
-
|
| 46 |
def format_record(self, record):
|
| 47 |
def val(k): return str(record.get(k, "") or "").strip()
|
| 48 |
mapping = [
|
|
@@ -61,133 +83,153 @@ class GrantsDataUpdater:
|
|
| 61 |
("owner organization", "owner_org"), ("owner organization title", "owner_org_title"),
|
| 62 |
]
|
| 63 |
return ", ".join([f"{d} : {val(k)}" for d, k in mapping])
|
| 64 |
-
|
| 65 |
-
def
|
| 66 |
-
|
| 67 |
try:
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
return False
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
| 80 |
try:
|
| 81 |
-
response = requests.get(
|
| 82 |
-
downloaded = 0
|
| 83 |
-
|
| 84 |
with open(self.temp_new_csv, 'wb') as f:
|
| 85 |
-
for chunk in response.iter_content(chunk_size=
|
| 86 |
-
if chunk:
|
| 87 |
-
f.write(chunk)
|
| 88 |
-
downloaded += len(chunk)
|
| 89 |
-
if downloaded % (25 * 1024 * 1024) < 1024*1024:
|
| 90 |
-
print(f" ...downloaded {downloaded // (1024*1024)} MB", flush=True)
|
| 91 |
-
|
| 92 |
-
print(f"[{datetime.now().strftime('%H:%M:%S')}] Download Complete.", flush=True)
|
| 93 |
return True
|
| 94 |
-
except
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
df_batch = batch.to_pandas()
|
| 109 |
-
for text in df_batch['data']:
|
| 110 |
-
existing_hashes.add(self.normalize_and_hash(text))
|
| 111 |
-
del df_batch
|
| 112 |
-
gc.collect()
|
| 113 |
-
except Exception as e:
|
| 114 |
-
print(f"Index Error: {e}", flush=True)
|
| 115 |
-
return existing_hashes
|
| 116 |
-
|
| 117 |
-
def process(self):
|
| 118 |
-
first_run = not self.download_existing_files()
|
| 119 |
-
if not self.download_new_csv(): return False
|
| 120 |
-
|
| 121 |
-
existing_hashes = self.get_existing_hashes()
|
| 122 |
-
|
| 123 |
-
print(f"[{datetime.now().strftime('%H:%M:%S')}] Scanning for new grants...", flush=True)
|
| 124 |
new_rows = []
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
for df_chunk in pd.read_csv(self.temp_new_csv, chunksize=self.chunk_size, low_memory=False):
|
| 130 |
-
|
| 131 |
-
chunk_new_count = 0
|
| 132 |
-
|
| 133 |
for _, row in df_chunk.iterrows():
|
| 134 |
-
|
| 135 |
-
|
| 136 |
|
| 137 |
-
if
|
| 138 |
-
|
| 139 |
-
existing_hashes.add(
|
| 140 |
-
chunk_new_count += 1
|
| 141 |
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
print(f"
|
| 148 |
-
if os.path.exists(self.temp_new_csv): os.remove(self.temp_new_csv)
|
| 149 |
-
return True
|
| 150 |
-
|
| 151 |
-
print(f"[{datetime.now().strftime('%H:%M:%S')}] Scan complete. TOTAL NEW GRANTS: {total_new}", flush=True)
|
| 152 |
-
|
| 153 |
-
print(f"[{datetime.now().strftime('%H:%M:%S')}] Generating vectors for {len(new_rows)} new items...", flush=True)
|
| 154 |
-
|
| 155 |
-
# CORRECTED DEVICE LOGIC
|
| 156 |
-
device = 'cuda' if hasattr(sys, 'getandroidapilevel') else ('cuda' if os.path.exists('/kaggle') else 'cpu')
|
| 157 |
-
model = SentenceTransformer('intfloat/multilingual-e5-large', device=device)
|
| 158 |
-
|
| 159 |
-
new_texts = ["passage: " + r['data'] for r in new_rows]
|
| 160 |
-
batch_size = 1000
|
| 161 |
-
new_embeddings_list = []
|
| 162 |
-
|
| 163 |
-
for i in range(0, len(new_texts), batch_size):
|
| 164 |
-
batch_texts = new_texts[i : i+batch_size]
|
| 165 |
-
print(f" Embedding batch {i//batch_size + 1}: Items {i} to {min(i+batch_size, len(new_texts))}...", flush=True)
|
| 166 |
-
emb_batch = model.encode(batch_texts, convert_to_numpy=True, normalize_embeddings=True)
|
| 167 |
-
new_embeddings_list.append(emb_batch)
|
| 168 |
-
|
| 169 |
-
new_embeddings = np.vstack(new_embeddings_list)
|
| 170 |
|
| 171 |
-
|
| 172 |
-
schema = pa.schema([('data', pa.string())])
|
| 173 |
-
new_table = pa.Table.from_pylist(new_rows, schema=schema)
|
| 174 |
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
else:
|
| 179 |
-
old_table = pq.read_table(PARQUET_FILE)
|
| 180 |
-
combined_table = pa.concat_tables([old_table, new_table])
|
| 181 |
-
pq.write_table(combined_table, PARQUET_FILE)
|
| 182 |
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
if os.path.exists(self.temp_new_csv): os.remove(self.temp_new_csv)
|
| 188 |
-
print(f"[{datetime.now().strftime('%H:%M:%S')}] Job Complete.", flush=True)
|
| 189 |
-
return True
|
| 190 |
|
|
|
|
|
|
|
|
|
|
| 191 |
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
updater = GrantsDataUpdater()
|
| 193 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
import sys
|
|
|
|
| 3 |
|
| 4 |
+
# ==========================================
|
| 5 |
+
# 1. SETUP ENV (Kaggle Needs This)
|
| 6 |
+
# ==========================================
|
| 7 |
try:
|
| 8 |
import sentence_transformers
|
| 9 |
import pyarrow
|
|
|
|
| 14 |
|
| 15 |
import requests
|
| 16 |
import pandas as pd
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
import hashlib
|
| 18 |
import re
|
|
|
|
| 19 |
from datetime import datetime
|
| 20 |
from typing import Set
|
| 21 |
+
import logging
|
| 22 |
+
import numpy as np
|
| 23 |
+
import pyarrow.parquet as pq
|
| 24 |
+
import pyarrow as pa
|
| 25 |
+
import gc
|
| 26 |
from sentence_transformers import SentenceTransformer
|
| 27 |
from huggingface_hub import hf_hub_download, HfApi
|
| 28 |
|
| 29 |
+
# Configure Logging to print to Stdout for the UI to catch
|
| 30 |
+
logging.basicConfig(level=logging.INFO, stream=sys.stdout, format='%(message)s')
|
| 31 |
+
logger = logging.getLogger(__name__)
|
| 32 |
|
| 33 |
+
# ==========================================
|
| 34 |
+
# 2. YOUR PRECISE LOGIC
|
| 35 |
+
# ==========================================
|
| 36 |
|
| 37 |
class GrantsDataUpdater:
|
| 38 |
+
"""
|
| 39 |
+
Incremental update system for Canadian Grants data.
|
| 40 |
+
Uses your preprocessing functions to detect new rows.
|
| 41 |
+
"""
|
| 42 |
+
|
| 43 |
+
def __init__(self,
|
| 44 |
+
data_url: str = "https://open.canada.ca/data/dataset/432527ab-7aac-45b5-81d6-7597107a7013/resource/1d15a62f-5656-49ad-8c88-f40ce689d831/download/grants.csv",
|
| 45 |
+
api_url: str = "https://open.canada.ca/data/api/3/action/package_show?id=432527ab-7aac-45b5-81d6-7597107a7013",
|
| 46 |
+
local_parquet: str = "grant data.parquet",
|
| 47 |
+
metadata_file: str = "last_metadata.json",
|
| 48 |
+
new_rows_csv: str = "new_rows.csv",
|
| 49 |
+
full_updated_parquet: str = "grant data.parquet", # Overwrite same file
|
| 50 |
+
chunk_size: int = 50000):
|
| 51 |
+
|
| 52 |
+
self.data_url = data_url
|
| 53 |
+
self.api_url = api_url
|
| 54 |
+
self.local_parquet = local_parquet
|
| 55 |
+
self.metadata_file = metadata_file
|
| 56 |
+
self.new_rows_csv = new_rows_csv
|
| 57 |
+
self.full_updated_parquet = full_updated_parquet
|
| 58 |
+
self.chunk_size = chunk_size
|
| 59 |
+
|
| 60 |
+
# Temporary files
|
| 61 |
self.temp_new_csv = "temp_new_data.csv"
|
| 62 |
+
|
|
|
|
| 63 |
def normalize_and_hash(self, text):
|
| 64 |
if not isinstance(text, str): text = ""
|
| 65 |
clean_text = re.sub(r'\s+', '', text).lower()
|
| 66 |
return hashlib.md5(clean_text.encode('utf-8')).hexdigest()
|
| 67 |
+
|
| 68 |
def format_record(self, record):
|
| 69 |
def val(k): return str(record.get(k, "") or "").strip()
|
| 70 |
mapping = [
|
|
|
|
| 83 |
("owner organization", "owner_org"), ("owner organization title", "owner_org_title"),
|
| 84 |
]
|
| 85 |
return ", ".join([f"{d} : {val(k)}" for d, k in mapping])
|
| 86 |
+
|
| 87 |
+
def check_for_updates(self) -> bool:
|
| 88 |
+
# Simplified for Kaggle run (Always run if forced, or check API)
|
| 89 |
try:
|
| 90 |
+
response = requests.get(self.api_url, timeout=30)
|
| 91 |
+
data = response.json()
|
| 92 |
+
if not data.get('success'): return False
|
| 93 |
+
result = data['result']
|
| 94 |
+
current_modified = result.get('metadata_modified')
|
| 95 |
+
|
| 96 |
+
# If we don't have a local metadata file (first run on Kaggle), we must assume update needed
|
| 97 |
+
if not os.path.exists(self.metadata_file):
|
| 98 |
+
logger.info("No local metadata found. Forcing update check.")
|
| 99 |
+
return True
|
| 100 |
+
|
| 101 |
+
with open(self.metadata_file, 'r') as f:
|
| 102 |
+
import json
|
| 103 |
+
last_meta = json.load(f)
|
| 104 |
+
|
| 105 |
+
if current_modified != last_meta.get('metadata_modified'):
|
| 106 |
+
logger.info("New data detected via API.")
|
| 107 |
+
return True
|
| 108 |
+
|
| 109 |
+
logger.info("Metadata matches. No update needed.")
|
| 110 |
return False
|
| 111 |
+
except:
|
| 112 |
+
return True
|
| 113 |
+
|
| 114 |
+
def download_new_data(self) -> bool:
|
| 115 |
+
logger.info("Downloading new dataset...")
|
| 116 |
try:
|
| 117 |
+
response = requests.get(self.data_url, stream=True, timeout=300)
|
|
|
|
|
|
|
| 118 |
with open(self.temp_new_csv, 'wb') as f:
|
| 119 |
+
for chunk in response.iter_content(chunk_size=8192): f.write(chunk)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
return True
|
| 121 |
+
except: return False
|
| 122 |
+
|
| 123 |
+
def get_existing_hashes(self) -> Set[str]:
|
| 124 |
+
try:
|
| 125 |
+
logger.info("Loading existing data hashes...")
|
| 126 |
+
if not os.path.exists(self.local_parquet): return set()
|
| 127 |
+
|
| 128 |
+
# Optimized: Read only 'data' column
|
| 129 |
+
df_old = pd.read_parquet(self.local_parquet, columns=['data'])
|
| 130 |
+
return set(self.normalize_and_hash(x) for x in df_old['data'])
|
| 131 |
+
except: return set()
|
| 132 |
+
|
| 133 |
+
def process_and_find_new_rows(self, existing_hashes: Set[str]):
|
| 134 |
+
logger.info("Processing new data...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
new_rows = []
|
| 136 |
+
all_rows = [] # We need to rebuild the file to keep order safe for embeddings
|
| 137 |
+
|
| 138 |
+
# Load existing data first to keep it at the top (Append mode simulation)
|
| 139 |
+
if os.path.exists(self.local_parquet):
|
| 140 |
+
# We assume existing data is good.
|
| 141 |
+
pass
|
| 142 |
+
|
| 143 |
+
# Scan New CSV
|
| 144 |
+
new_cnt = 0
|
| 145 |
+
scanned = 0
|
| 146 |
+
|
| 147 |
+
# We will append new rows to a list, then append that list to the parquet file
|
| 148 |
+
# This avoids re-writing the whole 1GB file if we can help it.
|
| 149 |
|
| 150 |
for df_chunk in pd.read_csv(self.temp_new_csv, chunksize=self.chunk_size, low_memory=False):
|
| 151 |
+
chunk_new = []
|
|
|
|
|
|
|
| 152 |
for _, row in df_chunk.iterrows():
|
| 153 |
+
formatted = self.format_record(row.to_dict())
|
| 154 |
+
h = self.normalize_and_hash(formatted)
|
| 155 |
|
| 156 |
+
if h not in existing_hashes:
|
| 157 |
+
chunk_new.append({'data': formatted})
|
| 158 |
+
existing_hashes.add(h)
|
|
|
|
| 159 |
|
| 160 |
+
if chunk_new:
|
| 161 |
+
new_rows.extend(chunk_new)
|
| 162 |
+
new_cnt += len(chunk_new)
|
| 163 |
+
|
| 164 |
+
scanned += len(df_chunk)
|
| 165 |
+
print(f"Scanned {scanned} rows... Found {new_cnt} new.", flush=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
|
| 167 |
+
logger.info(f"Total New Rows: {new_cnt}")
|
|
|
|
|
|
|
| 168 |
|
| 169 |
+
# Save New Rows CSV
|
| 170 |
+
if new_rows:
|
| 171 |
+
pd.DataFrame(new_rows).to_csv(self.new_rows_csv, index=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
|
| 173 |
+
# Append to Parquet
|
| 174 |
+
schema = pa.schema([('data', pa.string())])
|
| 175 |
+
new_table = pa.Table.from_pylist(new_rows, schema=schema)
|
| 176 |
|
| 177 |
+
if os.path.exists(self.local_parquet):
|
| 178 |
+
old_table = pq.read_table(self.local_parquet)
|
| 179 |
+
combined_table = pa.concat_tables([old_table, new_table])
|
| 180 |
+
pq.write_table(combined_table, self.full_updated_parquet)
|
| 181 |
+
else:
|
| 182 |
+
pq.write_table(new_table, self.full_updated_parquet)
|
| 183 |
+
|
| 184 |
+
return new_cnt
|
| 185 |
+
|
| 186 |
+
def cleanup_temp_files(self):
|
| 187 |
if os.path.exists(self.temp_new_csv): os.remove(self.temp_new_csv)
|
|
|
|
|
|
|
| 188 |
|
| 189 |
+
# ==========================================
|
| 190 |
+
# 3. EXECUTION BLOCK (Runs on Kaggle)
|
| 191 |
+
# ==========================================
|
| 192 |
if __name__ == "__main__":
|
| 193 |
+
|
| 194 |
+
# A. Download Existing Data from HF (So your logic has something to compare)
|
| 195 |
+
REPO_ID = "ravi2814/grant-data-storage"
|
| 196 |
+
print("Downloading existing parquet from Hugging Face...", flush=True)
|
| 197 |
+
try:
|
| 198 |
+
hf_hub_download(repo_id=REPO_ID, filename="grant data.parquet", repo_type="dataset", local_dir=".", force_download=True)
|
| 199 |
+
hf_hub_download(repo_id=REPO_ID, filename="embeddings.npy", repo_type="dataset", local_dir=".", force_download=True)
|
| 200 |
+
except:
|
| 201 |
+
print("No existing data found. Starting fresh.", flush=True)
|
| 202 |
+
|
| 203 |
+
# B. Run Your Logic
|
| 204 |
updater = GrantsDataUpdater()
|
| 205 |
+
|
| 206 |
+
if updater.download_new_data():
|
| 207 |
+
hashes = updater.get_existing_hashes()
|
| 208 |
+
new_count = updater.process_and_find_new_rows(hashes)
|
| 209 |
+
|
| 210 |
+
# C. Generate Embeddings for NEW rows (If any)
|
| 211 |
+
if new_count > 0:
|
| 212 |
+
print(f"Generating embeddings for {new_count} new rows...", flush=True)
|
| 213 |
+
device = 'cuda' if hasattr(sys, 'getandroidapilevel') else ('cuda' if os.path.exists('/kaggle') else 'cpu')
|
| 214 |
+
model = SentenceTransformer('intfloat/multilingual-e5-large', device=device)
|
| 215 |
+
|
| 216 |
+
# Load new rows
|
| 217 |
+
df_new = pd.read_csv(updater.new_rows_csv)
|
| 218 |
+
texts = ["passage: " + str(t) for t in df_new['data']]
|
| 219 |
+
|
| 220 |
+
new_embeddings = model.encode(texts, convert_to_numpy=True, normalize_embeddings=True, show_progress_bar=True)
|
| 221 |
+
|
| 222 |
+
# Append Embeddings
|
| 223 |
+
if os.path.exists("embeddings.npy"):
|
| 224 |
+
old_embeddings = np.load("embeddings.npy")
|
| 225 |
+
combined = np.vstack([old_embeddings, new_embeddings])
|
| 226 |
+
np.save("embeddings.npy", combined)
|
| 227 |
+
else:
|
| 228 |
+
np.save("embeddings.npy", new_embeddings)
|
| 229 |
+
|
| 230 |
+
print("Embeddings updated.", flush=True)
|
| 231 |
+
else:
|
| 232 |
+
print("No new rows to embed.", flush=True)
|
| 233 |
+
|
| 234 |
+
updater.cleanup_temp_files()
|
| 235 |
+
print("Job Complete.", flush=True)
|