ravi2814 commited on
Commit
59d7584
·
verified ·
1 Parent(s): ede8e0d

Update worker_script.py

Browse files
Files changed (1) hide show
  1. worker_script.py +170 -128
worker_script.py CHANGED
@@ -1,8 +1,9 @@
1
  import os
2
  import sys
3
- import time
4
 
5
- # 1. AUTO-INSTALL DEPENDENCIES
 
 
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
- # Config
29
- DATA_URL = "https://open.canada.ca/data/dataset/432527ab-7aac-45b5-81d6-7597107a7013/resource/1d15a62f-5656-49ad-8c88-f40ce689d831/download/grants.csv"
30
- REPO_ID = "ravi2814/grant-data-storage"
31
 
32
- PARQUET_FILE = "grant data.parquet"
33
- EMBEDDINGS_FILE = "embeddings.npy"
34
- METADATA_FILE = "last_metadata.json"
35
 
36
  class GrantsDataUpdater:
37
- def __init__(self):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  self.temp_new_csv = "temp_new_data.csv"
39
- self.chunk_size = 50000
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 download_existing_files(self):
66
- print(f"[{datetime.now().strftime('%H:%M:%S')}] Connecting to HuggingFace...", flush=True)
67
  try:
68
- hf_hub_download(repo_id=REPO_ID, filename=PARQUET_FILE, repo_type="dataset", local_dir=".", force_download=True)
69
- hf_hub_download(repo_id=REPO_ID, filename=EMBEDDINGS_FILE, repo_type="dataset", local_dir=".", force_download=True)
70
- try: hf_hub_download(repo_id=REPO_ID, filename=METADATA_FILE, repo_type="dataset", local_dir=".", force_download=True)
71
- except: pass
72
- print(f"[{datetime.now().strftime('%H:%M:%S')}] Success: Existing database retrieved.", flush=True)
73
- return True
74
- except:
75
- print(f"[{datetime.now().strftime('%H:%M:%S')}] Notice: No existing database found. Starting fresh.", flush=True)
 
 
 
 
 
 
 
 
 
 
 
 
76
  return False
77
-
78
- def download_new_csv(self):
79
- print(f"[{datetime.now().strftime('%H:%M:%S')}] Downloading latest CSV from Canada.ca...", flush=True)
 
 
80
  try:
81
- response = requests.get(DATA_URL, stream=True, timeout=300)
82
- downloaded = 0
83
-
84
  with open(self.temp_new_csv, 'wb') as f:
85
- for chunk in response.iter_content(chunk_size=1024*1024):
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 Exception as e:
95
- print(f"Download Error: {e}", flush=True)
96
- return False
97
-
98
- def get_existing_hashes(self):
99
- existing_hashes = set()
100
- if os.path.exists(PARQUET_FILE):
101
- print(f"[{datetime.now().strftime('%H:%M:%S')}] Indexing existing records...", flush=True)
102
- try:
103
- pf = pq.ParquetFile(PARQUET_FILE)
104
- rows = pf.metadata.num_rows
105
- print(f" Current Database Size: {rows:,} rows", flush=True)
106
-
107
- for batch in pf.iter_batches(batch_size=50000, columns=['data']):
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
- scanned_count = 0
126
- total_new = 0
127
- chunk_idx = 0
 
 
 
 
 
 
 
 
 
 
128
 
129
  for df_chunk in pd.read_csv(self.temp_new_csv, chunksize=self.chunk_size, low_memory=False):
130
- chunk_idx += 1
131
- chunk_new_count = 0
132
-
133
  for _, row in df_chunk.iterrows():
134
- formatted_text = self.format_record(row.to_dict())
135
- row_hash = self.normalize_and_hash(formatted_text)
136
 
137
- if row_hash not in existing_hashes:
138
- new_rows.append({'data': formatted_text})
139
- existing_hashes.add(row_hash)
140
- chunk_new_count += 1
141
 
142
- scanned_count += len(df_chunk)
143
- total_new += chunk_new_count
144
- print(f" Chunk {chunk_idx}: Scanned {scanned_count:,} rows. Found {chunk_new_count} NEW grants.", flush=True)
145
-
146
- if not new_rows:
147
- print(f"[{datetime.now().strftime('%H:%M:%S')}] Scan complete. No new data found.", flush=True)
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
- print(f"[{datetime.now().strftime('%H:%M:%S')}] Appending to database...", flush=True)
172
- schema = pa.schema([('data', pa.string())])
173
- new_table = pa.Table.from_pylist(new_rows, schema=schema)
174
 
175
- if first_run or not os.path.exists(PARQUET_FILE):
176
- pq.write_table(new_table, PARQUET_FILE)
177
- np.save(EMBEDDINGS_FILE, new_embeddings)
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
- old_embeddings = np.load(EMBEDDINGS_FILE)
184
- combined_embeddings = np.vstack([old_embeddings, new_embeddings])
185
- np.save(EMBEDDINGS_FILE, combined_embeddings)
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
- updater.process()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)