Bohaska commited on
Commit
7392937
·
1 Parent(s): a8087d6

update GA resolution scripts to use API

Browse files
ns_ga_resolutions_loose_bge-m3.npy CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:55e9bf59aa6262ef5d81918d14cefca4667dfc5d41847670c7118622e832c275
3
- size 3418831
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1df930cf80b890d10c3f95f9a4fd520d16a7d3a76726bf871e394345cf6d6e11
3
+ size 3555265
ns_ga_resolutions_semantic_bge-m3.npy CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:7a3e1e4def2ec2cd87a3c11fabb8af6e2457251c120619ffc29940c938c100e4
3
- size 1595520
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:76872864f28f8812bad14c5e1bcf48bf513dbd760bec937385bf8cfa4cc45de4
3
+ size 1642624
parsed_ga_resolutions.json CHANGED
The diff for this file is too large to render. See raw diff
 
small_scripts/ga_resolutions.json DELETED
The diff for this file is too large to render. See raw diff
 
small_scripts/make_embedding/embedding_ga_resolutions.py CHANGED
@@ -13,85 +13,135 @@ MODEL_PATH = '../../../../Downloads/bge-m3'
13
  # Path to the input JSON file for GA resolutions.
14
  GA_RESOLUTIONS_JSON_PATH = os.path.join(script_dir, '..', '..', 'parsed_ga_resolutions.json')
15
 
16
- # Output directory for the generated embedding files.
17
- # Assuming output files should go to the parent directory of this script.
18
  OUTPUT_DIR = os.path.join(script_dir, '..', '..')
19
 
 
 
 
 
 
 
20
  # --- Main Embedding Function ---
21
- def encode_ga_resolutions():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  print("Initializing BGEM3FlagModel...")
23
  try:
24
  model = BGEM3FlagModel(MODEL_PATH, use_fp16=True)
25
  print("Model loaded.")
26
  except Exception as e:
27
  print(f"Error loading model from {MODEL_PATH}: {e}")
28
- print("Please ensure the model is downloaded to the specified path.")
29
  return
30
 
31
- print(f"Loading GA resolutions from: {GA_RESOLUTIONS_JSON_PATH}")
32
  try:
33
- with open(GA_RESOLUTIONS_JSON_PATH, 'r', encoding='utf-8') as file:
34
- resolutions_data = json.load(file)
35
- except FileNotFoundError:
36
- print(f"Error: GA resolutions JSON file not found at {GA_RESOLUTIONS_JSON_PATH}")
37
- return
38
- except json.JSONDecodeError as e:
39
- print(f"Error decoding JSON from {GA_RESOLUTIONS_JSON_PATH}: {e}")
40
- return
41
  except Exception as e:
42
- print(f"An unexpected error occurred while loading {GA_RESOLUTIONS_JSON_PATH}: {e}")
43
- return
44
-
45
- # Extract the 'body' of each resolution to be encoded
46
- resolutions_text = [r['body'] for r in resolutions_data if 'body' in r and r['body'].strip()]
47
-
48
- if not resolutions_text:
49
- print("No valid resolution bodies found to encode. Exiting.")
50
  return
51
 
52
- print(f"Found {len(resolutions_text)} GA resolutions to encode.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
- print("Encoding resolutions (dense, sparse)...") # <--- Updated print statement
55
  try:
56
- embeddings = model.encode(resolutions_text,
57
- batch_size=8, # Adjust batch_size based on your GPU/CPU memory
58
- max_length=8192, # Max length of input sequence
59
- return_dense=True,
60
- return_sparse=True, # This will return 'lexical_weights' for BGE-M3
61
- return_colbert_vecs=False) # <--- REMOVED COLBERT GENERATION
62
-
63
  # Ensure output directory exists
64
  os.makedirs(OUTPUT_DIR, exist_ok=True)
65
 
66
- # --- Save Semantic (Dense) Embeddings ---
67
- dense_embeddings = embeddings['dense_vecs']
68
- dense_output_path = os.path.join(OUTPUT_DIR, 'ns_ga_resolutions_semantic_bge-m3.npy') # Renamed file
69
- np.save(dense_output_path, dense_embeddings)
70
- print(f"Saved semantic embeddings to {dense_output_path} (Shape: {dense_embeddings.shape})") # Renamed type and file
71
-
72
- # --- Save Loose (Sparse) Embeddings ---
73
- # 'lexical_weights' is a list of dictionaries, one for each item in the batch
74
- sparse_list_of_dicts = embeddings['lexical_weights']
75
-
76
- # Save this list of sparse dictionaries as a NumPy object array
77
- sparse_output_path = os.path.join(OUTPUT_DIR, 'ns_ga_resolutions_loose_bge-m3.npy') # Renamed file
78
- np.save(sparse_output_path, np.array(sparse_list_of_dicts, dtype=object), allow_pickle=True) # allow_pickle is essential for storing Python objects
79
- print(f"Saved loose embeddings to {sparse_output_path} (Total objects: {len(sparse_list_of_dicts)})") # Renamed type and file
80
 
 
 
 
81
 
82
- # --- Removed ColBERT Embeddings Saving ---
83
- # colbert_list_of_arrays = embeddings['colbert_vecs']
84
- # colbert_output_path = os.path.join(OUTPUT_DIR, 'ns_ga_resolutions_colbert_bge-m3.npy')
85
- # np.save(colbert_output_path, np.array(colbert_list_of_arrays, dtype=object), allow_pickle=True)
86
- # print(f"Saved ColBERT embeddings to {colbert_output_path} (Total objects: {len(colbert_list_of_arrays)})")
87
 
88
- print("\nGA Resolution embedding generation complete!")
89
 
90
  except Exception as e:
91
- print(f"An error occurred during embedding generation: {e}")
92
- import traceback
93
- traceback.print_exc() # Print full traceback for debugging
94
 
95
  # Call the function to start the embedding process
96
  if __name__ == "__main__":
97
- encode_ga_resolutions()
 
13
  # Path to the input JSON file for GA resolutions.
14
  GA_RESOLUTIONS_JSON_PATH = os.path.join(script_dir, '..', '..', 'parsed_ga_resolutions.json')
15
 
16
+ # Output directory for the generated files.
 
17
  OUTPUT_DIR = os.path.join(script_dir, '..', '..')
18
 
19
+ # --- Output and Cache File Paths ---
20
+ DENSE_OUTPUT_PATH = os.path.join(OUTPUT_DIR, 'ns_ga_resolutions_semantic_bge-m3.npy')
21
+ SPARSE_OUTPUT_PATH = os.path.join(OUTPUT_DIR, 'ns_ga_resolutions_loose_bge-m3.npy')
22
+ MANIFEST_PATH = os.path.join(script_dir, 'embeddings_manifest.json') # New manifest file
23
+
24
+
25
  # --- Main Embedding Function ---
26
+ def encode_ga_resolutions_with_caching():
27
+ # 1. --- Load the source of truth: all resolutions ---
28
+ print(f"Loading all GA resolutions from: {GA_RESOLUTIONS_JSON_PATH}")
29
+ try:
30
+ with open(GA_RESOLUTIONS_JSON_PATH, 'r', encoding='utf-8') as file:
31
+ all_resolutions_data = json.load(file)
32
+ # Filter out resolutions without a valid body
33
+ all_resolutions_data = [
34
+ r for r in all_resolutions_data if 'id' in r and 'body' in r and r['body'].strip()
35
+ ]
36
+ except (FileNotFoundError, json.JSONDecodeError, Exception) as e:
37
+ print(f"Fatal Error: Could not load or parse the source resolutions file. Cannot proceed. Error: {e}")
38
+ return
39
+
40
+ # 2. --- Load existing cache (manifest and embeddings) ---
41
+ cached_manifest = {}
42
+ old_dense_embeddings = None
43
+ old_sparse_embeddings = None
44
+
45
+ if os.path.exists(MANIFEST_PATH) and os.path.exists(DENSE_OUTPUT_PATH) and os.path.exists(SPARSE_OUTPUT_PATH):
46
+ print("Found existing cache. Loading manifest and embeddings.")
47
+ try:
48
+ with open(MANIFEST_PATH, 'r', encoding='utf-8') as f:
49
+ cached_manifest = json.load(f)
50
+ # Convert string keys from JSON back to integers if necessary
51
+ cached_manifest = {int(k): v for k, v in cached_manifest.items()}
52
+
53
+ old_dense_embeddings = np.load(DENSE_OUTPUT_PATH)
54
+ old_sparse_embeddings = np.load(SPARSE_OUTPUT_PATH, allow_pickle=True)
55
+ print(f"Successfully loaded cache for {len(cached_manifest)} resolutions.")
56
+ except Exception as e:
57
+ print(f"Warning: Could not load cache files correctly: {e}. Re-embedding all resolutions.")
58
+ cached_manifest = {} # Reset if cache is corrupt
59
+ else:
60
+ print("No existing cache found. Will generate embeddings for all resolutions.")
61
+
62
+ # 3. --- Identify new resolutions to be encoded ---
63
+ all_res_ids = {r['id'] for r in all_resolutions_data}
64
+ cached_res_ids = set(cached_manifest.keys())
65
+ new_res_ids = all_res_ids - cached_res_ids
66
+
67
+ if not new_res_ids:
68
+ print("All resolutions are already embedded. Nothing to do. Exiting.")
69
+ return
70
+
71
+ print(f"Found {len(new_res_ids)} new resolutions to embed.")
72
+ resolutions_to_encode = [r for r in all_resolutions_data if r['id'] in new_res_ids]
73
+ # Sort by ID to ensure a consistent order
74
+ resolutions_to_encode.sort(key=lambda x: x['id'])
75
+
76
+ new_texts = [r['body'] for r in resolutions_to_encode]
77
+
78
+ # 4. --- Initialize model and encode ONLY the new data ---
79
  print("Initializing BGEM3FlagModel...")
80
  try:
81
  model = BGEM3FlagModel(MODEL_PATH, use_fp16=True)
82
  print("Model loaded.")
83
  except Exception as e:
84
  print(f"Error loading model from {MODEL_PATH}: {e}")
 
85
  return
86
 
87
+ print(f"Encoding {len(new_texts)} new resolutions (dense, sparse)...")
88
  try:
89
+ new_embeddings = model.encode(new_texts,
90
+ batch_size=8,
91
+ max_length=8192,
92
+ return_dense=True,
93
+ return_sparse=True,
94
+ return_colbert_vecs=False)
 
 
95
  except Exception as e:
96
+ print(f"An error occurred during embedding generation: {e}")
97
+ import traceback
98
+ traceback.print_exc()
 
 
 
 
 
99
  return
100
 
101
+ # 5. --- Combine old and new embeddings ---
102
+ new_dense_vecs = new_embeddings['dense_vecs']
103
+ new_sparse_list = new_embeddings['lexical_weights']
104
+ new_sparse_vecs = np.array(new_sparse_list, dtype=object)
105
+
106
+ if old_dense_embeddings is not None and old_sparse_embeddings is not None:
107
+ print("Combining new embeddings with cached ones...")
108
+ combined_dense_embeddings = np.vstack([old_dense_embeddings, new_dense_vecs])
109
+ combined_sparse_embeddings = np.concatenate([old_sparse_embeddings, new_sparse_vecs])
110
+ else:
111
+ # This branch is for the first run when no cache exists
112
+ combined_dense_embeddings = new_dense_vecs
113
+ combined_sparse_embeddings = new_sparse_vecs
114
+
115
+ # 6. --- Update manifest and save everything ---
116
+ print("Updating manifest file...")
117
+ start_index = len(cached_manifest)
118
+ updated_manifest = cached_manifest.copy()
119
+ for i, res in enumerate(resolutions_to_encode):
120
+ updated_manifest[res['id']] = start_index + i
121
 
 
122
  try:
 
 
 
 
 
 
 
123
  # Ensure output directory exists
124
  os.makedirs(OUTPUT_DIR, exist_ok=True)
125
 
126
+ # Save combined embeddings
127
+ np.save(DENSE_OUTPUT_PATH, combined_dense_embeddings)
128
+ print(f"Saved combined semantic embeddings to {DENSE_OUTPUT_PATH} (Shape: {combined_dense_embeddings.shape})")
 
 
 
 
 
 
 
 
 
 
 
129
 
130
+ np.save(SPARSE_OUTPUT_PATH, combined_sparse_embeddings, allow_pickle=True)
131
+ print(
132
+ f"Saved combined loose embeddings to {SPARSE_OUTPUT_PATH} (Total objects: {len(combined_sparse_embeddings)})")
133
 
134
+ # Save the updated manifest
135
+ with open(MANIFEST_PATH, 'w', encoding='utf-8') as f:
136
+ json.dump(updated_manifest, f, indent=2)
137
+ print(f"Saved updated manifest to {MANIFEST_PATH}")
 
138
 
139
+ print("\nGA Resolution embedding process complete!")
140
 
141
  except Exception as e:
142
+ print(f"An error occurred while saving the files: {e}")
143
+
 
144
 
145
  # Call the function to start the embedding process
146
  if __name__ == "__main__":
147
+ encode_ga_resolutions_with_caching()
small_scripts/make_embedding/embeddings_manifest.json ADDED
@@ -0,0 +1,804 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "1": 0,
3
+ "2": 1,
4
+ "3": 2,
5
+ "4": 3,
6
+ "5": 4,
7
+ "6": 5,
8
+ "7": 6,
9
+ "8": 7,
10
+ "9": 8,
11
+ "10": 9,
12
+ "11": 10,
13
+ "12": 11,
14
+ "13": 12,
15
+ "14": 13,
16
+ "15": 14,
17
+ "16": 15,
18
+ "17": 16,
19
+ "18": 17,
20
+ "19": 18,
21
+ "20": 19,
22
+ "21": 20,
23
+ "22": 21,
24
+ "23": 22,
25
+ "24": 23,
26
+ "25": 24,
27
+ "26": 25,
28
+ "27": 26,
29
+ "28": 27,
30
+ "29": 28,
31
+ "30": 29,
32
+ "31": 30,
33
+ "32": 31,
34
+ "33": 32,
35
+ "34": 33,
36
+ "35": 34,
37
+ "36": 35,
38
+ "37": 36,
39
+ "38": 37,
40
+ "39": 38,
41
+ "40": 39,
42
+ "41": 40,
43
+ "42": 41,
44
+ "43": 42,
45
+ "44": 43,
46
+ "45": 44,
47
+ "46": 45,
48
+ "47": 46,
49
+ "48": 47,
50
+ "49": 48,
51
+ "50": 49,
52
+ "53": 50,
53
+ "54": 51,
54
+ "55": 52,
55
+ "57": 53,
56
+ "59": 54,
57
+ "61": 55,
58
+ "62": 56,
59
+ "63": 57,
60
+ "65": 58,
61
+ "66": 59,
62
+ "67": 60,
63
+ "68": 61,
64
+ "69": 62,
65
+ "70": 63,
66
+ "71": 64,
67
+ "72": 65,
68
+ "74": 66,
69
+ "75": 67,
70
+ "77": 68,
71
+ "78": 69,
72
+ "79": 70,
73
+ "81": 71,
74
+ "84": 72,
75
+ "87": 73,
76
+ "90": 74,
77
+ "92": 75,
78
+ "93": 76,
79
+ "94": 77,
80
+ "95": 78,
81
+ "96": 79,
82
+ "97": 80,
83
+ "99": 81,
84
+ "100": 82,
85
+ "102": 83,
86
+ "104": 84,
87
+ "106": 85,
88
+ "108": 86,
89
+ "110": 87,
90
+ "111": 88,
91
+ "113": 89,
92
+ "114": 90,
93
+ "115": 91,
94
+ "116": 92,
95
+ "118": 93,
96
+ "121": 94,
97
+ "122": 95,
98
+ "123": 96,
99
+ "124": 97,
100
+ "125": 98,
101
+ "126": 99,
102
+ "127": 100,
103
+ "128": 101,
104
+ "129": 102,
105
+ "130": 103,
106
+ "132": 104,
107
+ "133": 105,
108
+ "134": 106,
109
+ "135": 107,
110
+ "137": 108,
111
+ "139": 109,
112
+ "141": 110,
113
+ "142": 111,
114
+ "143": 112,
115
+ "145": 113,
116
+ "146": 114,
117
+ "148": 115,
118
+ "149": 116,
119
+ "151": 117,
120
+ "154": 118,
121
+ "156": 119,
122
+ "157": 120,
123
+ "159": 121,
124
+ "161": 122,
125
+ "163": 123,
126
+ "165": 124,
127
+ "166": 125,
128
+ "168": 126,
129
+ "170": 127,
130
+ "172": 128,
131
+ "173": 129,
132
+ "175": 130,
133
+ "177": 131,
134
+ "178": 132,
135
+ "179": 133,
136
+ "180": 134,
137
+ "181": 135,
138
+ "183": 136,
139
+ "184": 137,
140
+ "185": 138,
141
+ "186": 139,
142
+ "188": 140,
143
+ "190": 141,
144
+ "193": 142,
145
+ "197": 143,
146
+ "199": 144,
147
+ "200": 145,
148
+ "202": 146,
149
+ "203": 147,
150
+ "204": 148,
151
+ "206": 149,
152
+ "207": 150,
153
+ "208": 151,
154
+ "210": 152,
155
+ "211": 153,
156
+ "213": 154,
157
+ "216": 155,
158
+ "219": 156,
159
+ "220": 157,
160
+ "221": 158,
161
+ "224": 159,
162
+ "225": 160,
163
+ "228": 161,
164
+ "229": 162,
165
+ "230": 163,
166
+ "233": 164,
167
+ "235": 165,
168
+ "237": 166,
169
+ "238": 167,
170
+ "239": 168,
171
+ "242": 169,
172
+ "244": 170,
173
+ "246": 171,
174
+ "248": 172,
175
+ "249": 173,
176
+ "250": 174,
177
+ "252": 175,
178
+ "254": 176,
179
+ "255": 177,
180
+ "256": 178,
181
+ "257": 179,
182
+ "259": 180,
183
+ "260": 181,
184
+ "261": 182,
185
+ "263": 183,
186
+ "266": 184,
187
+ "267": 185,
188
+ "269": 186,
189
+ "270": 187,
190
+ "272": 188,
191
+ "275": 189,
192
+ "277": 190,
193
+ "278": 191,
194
+ "279": 192,
195
+ "280": 193,
196
+ "281": 194,
197
+ "282": 195,
198
+ "283": 196,
199
+ "284": 197,
200
+ "287": 198,
201
+ "289": 199,
202
+ "291": 200,
203
+ "292": 201,
204
+ "293": 202,
205
+ "295": 203,
206
+ "296": 204,
207
+ "297": 205,
208
+ "299": 206,
209
+ "301": 207,
210
+ "304": 208,
211
+ "306": 209,
212
+ "308": 210,
213
+ "309": 211,
214
+ "310": 212,
215
+ "311": 213,
216
+ "312": 214,
217
+ "314": 215,
218
+ "315": 216,
219
+ "316": 217,
220
+ "317": 218,
221
+ "319": 219,
222
+ "320": 220,
223
+ "321": 221,
224
+ "324": 222,
225
+ "326": 223,
226
+ "328": 224,
227
+ "329": 225,
228
+ "330": 226,
229
+ "332": 227,
230
+ "333": 228,
231
+ "335": 229,
232
+ "336": 230,
233
+ "338": 231,
234
+ "339": 232,
235
+ "340": 233,
236
+ "341": 234,
237
+ "343": 235,
238
+ "344": 236,
239
+ "346": 237,
240
+ "347": 238,
241
+ "348": 239,
242
+ "349": 240,
243
+ "350": 241,
244
+ "351": 242,
245
+ "352": 243,
246
+ "354": 244,
247
+ "356": 245,
248
+ "357": 246,
249
+ "358": 247,
250
+ "360": 248,
251
+ "362": 249,
252
+ "364": 250,
253
+ "367": 251,
254
+ "370": 252,
255
+ "371": 253,
256
+ "372": 254,
257
+ "373": 255,
258
+ "375": 256,
259
+ "377": 257,
260
+ "378": 258,
261
+ "379": 259,
262
+ "380": 260,
263
+ "381": 261,
264
+ "382": 262,
265
+ "384": 263,
266
+ "385": 264,
267
+ "386": 265,
268
+ "389": 266,
269
+ "390": 267,
270
+ "391": 268,
271
+ "392": 269,
272
+ "395": 270,
273
+ "396": 271,
274
+ "397": 272,
275
+ "398": 273,
276
+ "400": 274,
277
+ "402": 275,
278
+ "403": 276,
279
+ "407": 277,
280
+ "408": 278,
281
+ "409": 279,
282
+ "411": 280,
283
+ "413": 281,
284
+ "415": 282,
285
+ "416": 283,
286
+ "418": 284,
287
+ "419": 285,
288
+ "420": 286,
289
+ "423": 287,
290
+ "431": 288,
291
+ "432": 289,
292
+ "435": 290,
293
+ "438": 291,
294
+ "439": 292,
295
+ "441": 293,
296
+ "442": 294,
297
+ "443": 295,
298
+ "445": 296,
299
+ "447": 297,
300
+ "449": 298,
301
+ "450": 299,
302
+ "455": 300,
303
+ "458": 301,
304
+ "460": 302,
305
+ "461": 303,
306
+ "464": 304,
307
+ "466": 305,
308
+ "473": 306,
309
+ "474": 307,
310
+ "476": 308,
311
+ "477": 309,
312
+ "479": 310,
313
+ "480": 311,
314
+ "481": 312,
315
+ "483": 313,
316
+ "484": 314,
317
+ "485": 315,
318
+ "487": 316,
319
+ "488": 317,
320
+ "490": 318,
321
+ "491": 319,
322
+ "492": 320,
323
+ "493": 321,
324
+ "495": 322,
325
+ "497": 323,
326
+ "503": 324,
327
+ "504": 325,
328
+ "505": 326,
329
+ "506": 327,
330
+ "507": 328,
331
+ "509": 329,
332
+ "510": 330,
333
+ "512": 331,
334
+ "514": 332,
335
+ "515": 333,
336
+ "517": 334,
337
+ "518": 335,
338
+ "519": 336,
339
+ "520": 337,
340
+ "521": 338,
341
+ "522": 339,
342
+ "523": 340,
343
+ "524": 341,
344
+ "525": 342,
345
+ "526": 343,
346
+ "527": 344,
347
+ "528": 345,
348
+ "529": 346,
349
+ "531": 347,
350
+ "534": 348,
351
+ "536": 349,
352
+ "537": 350,
353
+ "538": 351,
354
+ "541": 352,
355
+ "543": 353,
356
+ "544": 354,
357
+ "546": 355,
358
+ "548": 356,
359
+ "549": 357,
360
+ "550": 358,
361
+ "551": 359,
362
+ "553": 360,
363
+ "555": 361,
364
+ "557": 362,
365
+ "559": 363,
366
+ "560": 364,
367
+ "562": 365,
368
+ "563": 366,
369
+ "564": 367,
370
+ "565": 368,
371
+ "566": 369,
372
+ "567": 370,
373
+ "568": 371,
374
+ "570": 372,
375
+ "572": 373,
376
+ "573": 374,
377
+ "576": 375,
378
+ "577": 376,
379
+ "578": 377,
380
+ "579": 378,
381
+ "581": 379,
382
+ "583": 380,
383
+ "585": 381,
384
+ "587": 382,
385
+ "588": 383,
386
+ "589": 384,
387
+ "593": 385,
388
+ "596": 386,
389
+ "597": 387,
390
+ "599": 388,
391
+ "601": 389,
392
+ "602": 390,
393
+ "603": 391,
394
+ "609": 392,
395
+ "612": 393,
396
+ "613": 394,
397
+ "614": 395,
398
+ "615": 396,
399
+ "616": 397,
400
+ "618": 398,
401
+ "621": 399,
402
+ "623": 400,
403
+ "625": 401,
404
+ "627": 402,
405
+ "628": 403,
406
+ "629": 404,
407
+ "631": 405,
408
+ "637": 406,
409
+ "639": 407,
410
+ "641": 408,
411
+ "642": 409,
412
+ "644": 410,
413
+ "646": 411,
414
+ "648": 412,
415
+ "649": 413,
416
+ "651": 414,
417
+ "653": 415,
418
+ "654": 416,
419
+ "655": 417,
420
+ "658": 418,
421
+ "660": 419,
422
+ "661": 420,
423
+ "665": 421,
424
+ "666": 422,
425
+ "667": 423,
426
+ "668": 424,
427
+ "670": 425,
428
+ "676": 426,
429
+ "677": 427,
430
+ "683": 428,
431
+ "685": 429,
432
+ "686": 430,
433
+ "687": 431,
434
+ "690": 432,
435
+ "692": 433,
436
+ "694": 434,
437
+ "696": 435,
438
+ "697": 436,
439
+ "698": 437,
440
+ "699": 438,
441
+ "700": 439,
442
+ "701": 440,
443
+ "704": 441,
444
+ "705": 442,
445
+ "707": 443,
446
+ "708": 444,
447
+ "709": 445,
448
+ "711": 446,
449
+ "712": 447,
450
+ "714": 448,
451
+ "715": 449,
452
+ "717": 450,
453
+ "719": 451,
454
+ "727": 452,
455
+ "730": 453,
456
+ "731": 454,
457
+ "733": 455,
458
+ "734": 456,
459
+ "735": 457,
460
+ "736": 458,
461
+ "738": 459,
462
+ "739": 460,
463
+ "741": 461,
464
+ "742": 462,
465
+ "743": 463,
466
+ "744": 464,
467
+ "748": 465,
468
+ "751": 466,
469
+ "752": 467,
470
+ "753": 468,
471
+ "757": 469,
472
+ "758": 470,
473
+ "759": 471,
474
+ "760": 472,
475
+ "761": 473,
476
+ "762": 474,
477
+ "763": 475,
478
+ "766": 476,
479
+ "770": 477,
480
+ "772": 478,
481
+ "773": 479,
482
+ "775": 480,
483
+ "777": 481,
484
+ "780": 482,
485
+ "782": 483,
486
+ "783": 484,
487
+ "787": 485,
488
+ "790": 486,
489
+ "795": 487,
490
+ "797": 488,
491
+ "798": 489,
492
+ "801": 490,
493
+ "803": 491,
494
+ "805": 492,
495
+ "806": 493,
496
+ "807": 494,
497
+ "810": 495,
498
+ "811": 496,
499
+ "812": 497,
500
+ "813": 498,
501
+ "814": 499,
502
+ "815": 500,
503
+ "818": 501,
504
+ "820": 502,
505
+ "821": 503,
506
+ "823": 504,
507
+ "825": 505,
508
+ "827": 506,
509
+ "828": 507,
510
+ "830": 508,
511
+ "832": 509,
512
+ "834": 510,
513
+ "836": 511,
514
+ "839": 512,
515
+ "840": 513,
516
+ "841": 514,
517
+ "843": 515,
518
+ "844": 516,
519
+ "846": 517,
520
+ "847": 518,
521
+ "848": 519,
522
+ "850": 520,
523
+ "852": 521,
524
+ "854": 522,
525
+ "855": 523,
526
+ "856": 524,
527
+ "858": 525,
528
+ "860": 526,
529
+ "862": 527,
530
+ "863": 528,
531
+ "867": 529,
532
+ "869": 530,
533
+ "870": 531,
534
+ "873": 532,
535
+ "874": 533,
536
+ "875": 534,
537
+ "876": 535,
538
+ "879": 536,
539
+ "881": 537,
540
+ "883": 538,
541
+ "885": 539,
542
+ "886": 540,
543
+ "888": 541,
544
+ "890": 542,
545
+ "891": 543,
546
+ "893": 544,
547
+ "894": 545,
548
+ "895": 546,
549
+ "897": 547,
550
+ "899": 548,
551
+ "900": 549,
552
+ "902": 550,
553
+ "903": 551,
554
+ "905": 552,
555
+ "907": 553,
556
+ "909": 554,
557
+ "911": 555,
558
+ "913": 556,
559
+ "914": 557,
560
+ "915": 558,
561
+ "916": 559,
562
+ "917": 560,
563
+ "918": 561,
564
+ "920": 562,
565
+ "923": 563,
566
+ "924": 564,
567
+ "926": 565,
568
+ "929": 566,
569
+ "930": 567,
570
+ "932": 568,
571
+ "934": 569,
572
+ "936": 570,
573
+ "937": 571,
574
+ "939": 572,
575
+ "942": 573,
576
+ "943": 574,
577
+ "944": 575,
578
+ "946": 576,
579
+ "947": 577,
580
+ "949": 578,
581
+ "952": 579,
582
+ "954": 580,
583
+ "956": 581,
584
+ "958": 582,
585
+ "959": 583,
586
+ "960": 584,
587
+ "961": 585,
588
+ "964": 586,
589
+ "966": 587,
590
+ "967": 588,
591
+ "970": 589,
592
+ "972": 590,
593
+ "974": 591,
594
+ "976": 592,
595
+ "978": 593,
596
+ "979": 594,
597
+ "981": 595,
598
+ "982": 596,
599
+ "984": 597,
600
+ "985": 598,
601
+ "988": 599,
602
+ "990": 600,
603
+ "992": 601,
604
+ "993": 602,
605
+ "998": 603,
606
+ "999": 604,
607
+ "1000": 605,
608
+ "1001": 606,
609
+ "1004": 607,
610
+ "1006": 608,
611
+ "1008": 609,
612
+ "1009": 610,
613
+ "1012": 611,
614
+ "1014": 612,
615
+ "1016": 613,
616
+ "1021": 614,
617
+ "1024": 615,
618
+ "1025": 616,
619
+ "1026": 617,
620
+ "1027": 618,
621
+ "1030": 619,
622
+ "1033": 620,
623
+ "1036": 621,
624
+ "1039": 622,
625
+ "1040": 623,
626
+ "1042": 624,
627
+ "1044": 625,
628
+ "1045": 626,
629
+ "1048": 627,
630
+ "1049": 628,
631
+ "1052": 629,
632
+ "1054": 630,
633
+ "1056": 631,
634
+ "1058": 632,
635
+ "1059": 633,
636
+ "1060": 634,
637
+ "1061": 635,
638
+ "1062": 636,
639
+ "1063": 637,
640
+ "1065": 638,
641
+ "1067": 639,
642
+ "1068": 640,
643
+ "1070": 641,
644
+ "1072": 642,
645
+ "1074": 643,
646
+ "1077": 644,
647
+ "1078": 645,
648
+ "1079": 646,
649
+ "1081": 647,
650
+ "1083": 648,
651
+ "1086": 649,
652
+ "1088": 650,
653
+ "1091": 651,
654
+ "1092": 652,
655
+ "1093": 653,
656
+ "1095": 654,
657
+ "1096": 655,
658
+ "1097": 656,
659
+ "1100": 657,
660
+ "1102": 658,
661
+ "1104": 659,
662
+ "1106": 660,
663
+ "1108": 661,
664
+ "1109": 662,
665
+ "1111": 663,
666
+ "1113": 664,
667
+ "1115": 665,
668
+ "1118": 666,
669
+ "1120": 667,
670
+ "1122": 668,
671
+ "1124": 669,
672
+ "1126": 670,
673
+ "1130": 671,
674
+ "1132": 672,
675
+ "1134": 673,
676
+ "1136": 674,
677
+ "1138": 675,
678
+ "1139": 676,
679
+ "1141": 677,
680
+ "1143": 678,
681
+ "1144": 679,
682
+ "1146": 680,
683
+ "1148": 681,
684
+ "1150": 682,
685
+ "1151": 683,
686
+ "1152": 684,
687
+ "1154": 685,
688
+ "1156": 686,
689
+ "1158": 687,
690
+ "1160": 688,
691
+ "1161": 689,
692
+ "1163": 690,
693
+ "1166": 691,
694
+ "1168": 692,
695
+ "1171": 693,
696
+ "1173": 694,
697
+ "1176": 695,
698
+ "1178": 696,
699
+ "1180": 697,
700
+ "1182": 698,
701
+ "1184": 699,
702
+ "1186": 700,
703
+ "1188": 701,
704
+ "1190": 702,
705
+ "1191": 703,
706
+ "1193": 704,
707
+ "1195": 705,
708
+ "1197": 706,
709
+ "1198": 707,
710
+ "1199": 708,
711
+ "1201": 709,
712
+ "1203": 710,
713
+ "1205": 711,
714
+ "1207": 712,
715
+ "1208": 713,
716
+ "1209": 714,
717
+ "1211": 715,
718
+ "1212": 716,
719
+ "1214": 717,
720
+ "1216": 718,
721
+ "1217": 719,
722
+ "1218": 720,
723
+ "1220": 721,
724
+ "1223": 722,
725
+ "1224": 723,
726
+ "1225": 724,
727
+ "1228": 725,
728
+ "1229": 726,
729
+ "1230": 727,
730
+ "1231": 728,
731
+ "1232": 729,
732
+ "1233": 730,
733
+ "1234": 731,
734
+ "1235": 732,
735
+ "1236": 733,
736
+ "1237": 734,
737
+ "1240": 735,
738
+ "1242": 736,
739
+ "1244": 737,
740
+ "1245": 738,
741
+ "1246": 739,
742
+ "1247": 740,
743
+ "1248": 741,
744
+ "1250": 742,
745
+ "1252": 743,
746
+ "1254": 744,
747
+ "1257": 745,
748
+ "1258": 746,
749
+ "1259": 747,
750
+ "1260": 748,
751
+ "1263": 749,
752
+ "1266": 750,
753
+ "1270": 751,
754
+ "1272": 752,
755
+ "1273": 753,
756
+ "1275": 754,
757
+ "1277": 755,
758
+ "1278": 756,
759
+ "1280": 757,
760
+ "1281": 758,
761
+ "1283": 759,
762
+ "1285": 760,
763
+ "1287": 761,
764
+ "1291": 762,
765
+ "1293": 763,
766
+ "1296": 764,
767
+ "1297": 765,
768
+ "1298": 766,
769
+ "1299": 767,
770
+ "1300": 768,
771
+ "1303": 769,
772
+ "1304": 770,
773
+ "1305": 771,
774
+ "1307": 772,
775
+ "1310": 773,
776
+ "1312": 774,
777
+ "1315": 775,
778
+ "1316": 776,
779
+ "1317": 777,
780
+ "1318": 778,
781
+ "1324": 779,
782
+ "1325": 780,
783
+ "1327": 781,
784
+ "1329": 782,
785
+ "1330": 783,
786
+ "1332": 784,
787
+ "1333": 785,
788
+ "1336": 786,
789
+ "1338": 787,
790
+ "1339": 788,
791
+ "1340": 789,
792
+ "1341": 790,
793
+ "1343": 791,
794
+ "1345": 792,
795
+ "1347": 793,
796
+ "1350": 794,
797
+ "1351": 795,
798
+ "1352": 796,
799
+ "1354": 797,
800
+ "1355": 798,
801
+ "1357": 799,
802
+ "1359": 800,
803
+ "1361": 801
804
+ }
small_scripts/parse_ga_resolutions.py CHANGED
@@ -1,228 +1,212 @@
 
 
1
  import json
2
- import re
3
- from bs4 import BeautifulSoup
4
- from markdownify import markdownify as md
5
-
6
-
7
- def parse_resolution_html(html_string):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  """
9
- Parses a single HTML string representing a World Assembly resolution
10
- into a structured Python dictionary.
11
 
12
  Args:
13
- html_string: The HTML string of a single resolution block.
14
 
15
  Returns:
16
- A dictionary representing the resolution data, or None if parsing fails.
17
  """
18
- soup = BeautifulSoup(html_string, 'html.parser')
19
- data = {}
20
-
21
- # Find the main resolution container div
22
- thing_div = soup.find('div', class_='WA_thing')
23
- if not thing_div:
24
- # If the main container isn't found, it's not a valid resolution block
25
- print("Warning: Could not find the main 'WA_thing' div.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  return None
27
 
28
- # --- Status (Repealed) ---
29
- # Check if the resolution has been repealed
30
- repealed_div = thing_div.find('div', class_='WA_thing_repealed')
31
- if repealed_div:
32
- data['status'] = 'Repealed'
33
- # Extract info about the repealing resolution if available
34
- repeal_line = repealed_div.find('p', class_='WA_thing_repealline')
35
- if repeal_line:
36
- repealer_link = repeal_line.find('a')
37
- if repealer_link:
38
- data['repealed_by'] = {
39
- 'id': repealer_link.text.strip().replace('Repealed by GA#', '').strip(), # Extract just the number
40
- 'link': repealer_link.get('href')
41
- }
42
  else:
43
- data['status'] = 'Active'
44
-
45
- # --- Header ---
46
- header_div = thing_div.find('div', class_='WA_thing_header')
47
- if header_div:
48
- # Raw title (e.g., "General Assembly Resolution # 769")
49
- rtitle_tag = header_div.find('p', class_='WA_rtitle')
50
- if rtitle_tag:
51
- data['raw_title'] = rtitle_tag.text.strip()
52
-
53
- # Main title and link (contains ID and Council)
54
- title_link_tag = header_div.find('h2').find('a') if header_div.find('h2') else None
55
- if title_link_tag:
56
- data['title'] = title_link_tag.text.strip()
57
- href = title_link_tag.get('href')
58
- data['link'] = href
59
- # Extract ID and Council from the link
60
- match = re.search(r'id=(\d+)/council=(\d+)', href)
61
- if match:
62
- data['id'] = int(match.group(1))
63
- data['council'] = int(match.group(2))
64
-
65
- # Description (the paragraph without a specific class in the header)
66
- # Find all p tags in header and take the last one that isn't rtitle or repealline
67
- all_ps_in_header = header_div.find_all('p')
68
- description_tag = None
69
- for p_tag in reversed(all_ps_in_header):
70
- if 'WA_rtitle' not in p_tag.get('class', []) and 'WA_thing_repealline' not in p_tag.get('class', []):
71
- description_tag = p_tag
72
- break
73
- if description_tag:
74
- data['description'] = description_tag.text.strip()
75
-
76
-
77
- # --- Info Box (Category, Area, Strength, Proposed by, Repeals info) ---
78
- rbox_div = thing_div.find('div', class_='WA_thing_rbox')
79
- if rbox_div:
80
- # Iterate through paragraphs in the info box
81
- for p_tag in rbox_div.find_all('p'):
82
- leader_span = p_tag.find('span', class_='WA_leader')
83
- if leader_span:
84
- label = leader_span.text.strip().replace(':', '')
85
- # Get the text content after the leader span
86
- content_text = leader_span.next_sibling
87
- if content_text:
88
- content_text = content_text.strip()
89
-
90
- if label == 'Category':
91
- data['category'] = content_text
92
- elif label == 'Area of Effect':
93
- data['area_of_effect'] = content_text
94
- elif label == 'Strength':
95
- data['strength'] = content_text
96
- elif label == 'Resolution': # This tag appears specifically for Repeal resolutions
97
- link_tag = p_tag.find('a')
98
- if link_tag:
99
- data['repeals'] = {
100
- 'ga_id': link_tag.text.strip().replace('GA#', ''),
101
- 'link': link_tag.get('href')
102
- }
103
- elif label == 'Proposed by':
104
- nation_link_tag = p_tag.find('a', class_='nlink')
105
- if nation_link_tag:
106
- nation_name_span = nation_link_tag.find('span', class_='nnameblock')
107
- if nation_name_span:
108
- data['proposed_by'] = {
109
- 'name': nation_name_span.text.strip(),
110
- 'link': nation_link_tag.get('href')
111
- }
112
-
113
-
114
- # --- Body HTML ---
115
- body_div = thing_div.find('div', class_='WA_thing_body')
116
- if body_div:
117
- # Get the inner HTML content while preserving tags
118
- data['body'] = md(body_div.decode_contents().strip())
119
-
120
- # --- Co-authors ---
121
- # Co-authors paragraph is a sibling immediately after the body div
122
- coauthors_p = body_div.find_next_sibling('p') if body_div else None
123
- if coauthors_p and coauthors_p.find('span', class_='WA_leader', string='Co-authors:'):
124
- coauthors_list = []
125
- # Find all nation links within this paragraph
126
- for a_tag in coauthors_p.find_all('a', class_='nlink'):
127
- nname_span = a_tag.find('span', class_='nnameblock')
128
- if nname_span:
129
- coauthors_list.append({
130
- 'name': nname_span.text.strip(),
131
- 'link': a_tag.get('href')
132
- })
133
- if coauthors_list:
134
- data['co_authors'] = coauthors_list
135
-
136
- # --- Vote Counts and Dates ---
137
- presbottom_div = thing_div.find('div', class_='WApresbottom')
138
- if presbottom_div:
139
- # Passed/Repealed Dates (in floatrightbox)
140
- floatrightbox = presbottom_div.find('div', class_='floatrightbox')
141
- if floatrightbox:
142
- # Passed date
143
- passed_leader_p = floatrightbox.find('p', class_='WA_leader', string='Passed:')
144
- if passed_leader_p:
145
- # Navigate up to the <td>, then find the next sibling <td>, then find the <p> inside it, then the <time>
146
- passed_leader_td = passed_leader_p.find_parent('td')
147
- if passed_leader_td: # Add a check here too just in case the structure is unexpected
148
- date_td = passed_leader_td.find_next_sibling('td')
149
- if date_td: # Check if the next <td> exists
150
- date_p = date_td.find('p') # Find the paragraph inside that <td>
151
- if date_p: # Check if the paragraph exists
152
- passed_time_tag = date_p.find('time') # Find the time tag inside that paragraph
153
-
154
- if passed_time_tag:
155
- data['passed_date'] = {
156
- 'datetime': passed_time_tag.get('datetime'),
157
- 'text': passed_time_tag.text.strip()
158
- }
159
-
160
- # Repealed date (only present if repealed)
161
- # Apply similar robust navigation here
162
- repealed_leader_p = floatrightbox.find('p', class_='WA_leader')
163
- if repealed_leader_p and repealed_leader_p.find('a', string='Repealed:'):
164
- repealed_leader_td = repealed_leader_p.find_parent('td')
165
- if repealed_leader_td:
166
- date_td = repealed_leader_td.find_next_sibling('td')
167
- if date_td:
168
- date_p = date_td.find('p')
169
- if date_p:
170
- repealed_time_tag = date_p.find('time')
171
-
172
- if repealed_time_tag:
173
- # Ensure status is marked repealed even if WA_thing_repealed div was missed
174
- if 'status' not in data or data['status'] != 'Repealed':
175
- data['status'] = 'Repealed'
176
- data['repealed_date'] = {
177
- 'datetime': repealed_time_tag.get('datetime'),
178
- 'text': repealed_time_tag.text.strip()
179
- }
180
- # Vote Counts (in WA_votecount table)
181
- # This part of the logic seems mostly correct because you're navigating cell by cell within the row
182
- votecount_table = presbottom_div.find('table', class_='WA_votecount')
183
- if votecount_table:
184
- for row in votecount_table.find_all('tr'):
185
- leader_cell = row.find('p', class_='WA_leader')
186
- if leader_cell:
187
- label = leader_cell.text.strip().replace(':', '')
188
- if label in ['For', 'Against']:
189
- # Find the cells for count and percentage relative to the leader cell
190
- # These navigations (find_parent('td').find_next_sibling('td')) are correct
191
- count_cell = leader_cell.find_parent('td').find_next_sibling('td')
192
- percentage_cell = count_cell.find_next_sibling('td') if count_cell else None
193
-
194
- count_text = count_cell.find('span', class_='bigtext').text.strip().replace(',', '') if count_cell and count_cell.find('span', class_='bigtext') else '0'
195
- percentage_text = percentage_cell.find('span', class_='smalltext').text.strip().replace('%', '') if percentage_cell and percentage_cell.find('span', class_='smalltext') else '0'
196
-
197
-
198
- try:
199
- data[label.lower() + '_votes'] = int(count_text)
200
- except ValueError:
201
- data[label.lower() + '_votes'] = 0 # Handle potential parsing errors
202
-
203
- try:
204
- data[label.lower() + '_percentage'] = float(percentage_text)
205
- except ValueError:
206
- data[label.lower() + '_percentage'] = 0.0
207
-
208
- return data
209
-
210
-
211
- def __main__():
212
- resolutions = open("ga_resolutions.json", "r")
213
- html_resolutions = json.load(resolutions)
214
- resolutions.close()
215
- json_resolutions = []
216
- for resolution in html_resolutions:
217
- json_resolutions.append(parse_resolution_html(resolution))
218
- output = open("../parsed_ga_resolutions.json", "w")
219
- json.dump(json_resolutions, output)
220
-
221
- __main__()
222
-
223
- def format_resoluton(resolution):
224
- title = "<resolution>\n"
225
- if resolution['status'] == "REPEALED":
226
- title = f"[Repealed by GA#{resolution['repealed_by']} "
227
- title += f"GA#{resolution['id']} {resolution['title']}"
228
- return title + "\n\n" + resolution['body'] + "\n</resolution>"
 
1
+ import requests
2
+ import xml.etree.ElementTree as ET
3
  import json
4
+ import time
5
+ import os
6
+
7
+ # --- Configuration ---
8
+ # Replace with your own nation name or contact info.
9
+ USER_AGENT = "NS Issue Search dev update script (Jiangbei)"
10
+ CACHE_FILE = "../parsed_ga_resolutions.json"
11
+ API_BASE_URL = "https://www.nationstates.net/cgi-bin/api.cgi"
12
+ COUNCIL_ID = 1 # 1 for General Assembly, 2 for Security Council
13
+
14
+
15
+ def load_cache(filename):
16
+ """Loads existing resolutions from the JSON cache file."""
17
+ if not os.path.exists(filename):
18
+ print(f"Cache file '{filename}' not found. Will start from scratch.")
19
+ return {}
20
+
21
+ try:
22
+ with open(filename, 'r', encoding='utf-8') as f:
23
+ resolutions_list = json.load(f)
24
+ # Convert list to a dictionary keyed by resolution ID for fast lookups
25
+ return {res['id']: res for res in resolutions_list}
26
+ except (json.JSONDecodeError, IOError) as e:
27
+ print(f"Error reading cache file '{filename}': {e}. Starting from scratch.")
28
+ return {}
29
+
30
+
31
+ def save_cache(filename, resolutions_dict):
32
+ """Saves the resolutions dictionary to the JSON cache file."""
33
+ try:
34
+ # Convert the dictionary values back to a list and sort by ID
35
+ sorted_resolutions = sorted(resolutions_dict.values(), key=lambda r: r['id'])
36
+ with open(filename, 'w', encoding='utf-8') as f:
37
+ json.dump(sorted_resolutions, f, indent=2)
38
+ print(f"Successfully saved {len(sorted_resolutions)} resolutions to '{filename}'.")
39
+ except IOError as e:
40
+ print(f"Error writing to cache file '{filename}': {e}")
41
+
42
+
43
+ def parse_resolution_xml(xml_string):
44
  """
45
+ Parses a single XML string from the NationStates API into a structured dictionary.
 
46
 
47
  Args:
48
+ xml_string: The XML content from the API response.
49
 
50
  Returns:
51
+ A dictionary representing the resolution data, or None if parsing fails or resolution is empty.
52
  """
53
+ try:
54
+ root = ET.fromstring(xml_string)
55
+ res_node = root.find('RESOLUTION')
56
+
57
+ # If the RESOLUTION tag is empty, it means the resolution doesn't exist.
58
+ if res_node is None or not list(res_node):
59
+ return None
60
+
61
+ data = {}
62
+ # Iterate through all direct child tags of <RESOLUTION>
63
+ for child in res_node:
64
+ # Special case for COAUTHOR, which has multiple <N> children
65
+ if child.tag == 'COAUTHOR':
66
+ co_authors = [n.text for n in child.findall('N')]
67
+ if co_authors:
68
+ data['co_authors'] = co_authors
69
+ continue # Skip to the next tag
70
+
71
+ key = child.tag.lower()
72
+ value = child.text
73
+
74
+ # Try to convert numeric values to integers
75
+ try:
76
+ data[key] = int(value)
77
+ except (ValueError, TypeError):
78
+ data[key] = value
79
+
80
+ # --- Map API fields to desired dictionary structure ---
81
+ # Keep required fields with consistent naming
82
+ if 'name' in data: data['title'] = data.pop('name')
83
+ if 'resid' in data: data['id'] = data.pop('resid')
84
+ if 'desc' in data: data['body'] = data.pop('desc') # Keep BBCode as text
85
+ if 'councilid' in data: data['council'] = data.pop('councilid')
86
+
87
+ # Determine status and structure repeal information
88
+ if 'repealed_by' in data:
89
+ data['status'] = 'Repealed'
90
+ data['repealed_by'] = {
91
+ 'id': data.pop('repealed_by'),
92
+ 'timestamp': data.pop('repealed', None)
93
+ }
94
+ else:
95
+ data['status'] = 'Active'
96
+
97
+ # Structure info for resolutions that ARE repeals
98
+ if 'repeals_resid' in data:
99
+ data['repeals'] = {
100
+ 'id': data.pop('repeals_resid'),
101
+ 'council': data.pop('repeals_councilid')
102
+ }
103
+
104
+ return data
105
+
106
+ except ET.ParseError as e:
107
+ print(f"Error parsing XML: {e}")
108
  return None
109
 
110
+
111
+ def main():
112
+ """Main function to fetch, parse, and cache resolutions."""
113
+ print("--- World Assembly Resolution Fetcher ---")
114
+
115
+ # Load existing resolutions from cache
116
+ cached_resolutions = load_cache(CACHE_FILE)
117
+ if cached_resolutions:
118
+ # Find the latest resolution ID we already have and start from the next one
119
+ start_id = max(cached_resolutions.keys()) + 1
120
+ print(f"Loaded {len(cached_resolutions)} resolutions from cache. Starting fetch from GA#{start_id}.")
 
 
 
121
  else:
122
+ start_id = 1
123
+
124
+ # --- API Request Loop ---
125
+ session = requests.Session()
126
+ session.headers.update({'User-Agent': USER_AGENT})
127
+
128
+ current_id = start_id
129
+ newly_fetched = []
130
+
131
+ rate_limit_info = {
132
+ 'remaining': 50,
133
+ 'reset_in': 30
134
+ }
135
+
136
+ while True:
137
+ # Check if we are about to exceed the rate limit
138
+ if rate_limit_info['remaining'] < 2:
139
+ wait_time = rate_limit_info['reset_in'] + 1 # Add a small buffer
140
+ print(f"Rate limit approaching. Waiting for {wait_time} seconds...")
141
+ time.sleep(wait_time)
142
+
143
+ print(f"Fetching resolution GA#{current_id}...")
144
+
145
+ params = {'wa': COUNCIL_ID, 'id': current_id, 'q': 'resolution'}
146
+ try:
147
+ response = session.get(API_BASE_URL, params=params, timeout=15)
148
+
149
+ # Update rate limit info from headers after every request
150
+ rate_limit_info['remaining'] = int(response.headers.get('RateLimit-Remaining', 50))
151
+ rate_limit_info['reset_in'] = int(response.headers.get('RateLimit-Reset', 30))
152
+
153
+ # Handle API responses
154
+ if response.status_code == 429:
155
+ retry_after = int(response.headers.get('Retry-After', 30))
156
+ print(f"Rate limit exceeded (429). Waiting for {retry_after} seconds as requested by API.")
157
+ time.sleep(retry_after)
158
+ continue # Retry the same ID
159
+
160
+ response.raise_for_status() # Raises an error for other bad responses (4xx or 5xx)
161
+
162
+ except requests.exceptions.RequestException as e:
163
+ print(f"An error occurred during request for GA#{current_id}: {e}")
164
+ print("Stopping script. Run again to resume.")
165
+ break
166
+
167
+ # Parse the response content
168
+ parsed_data = parse_resolution_xml(response.text)
169
+
170
+ if parsed_data:
171
+ newly_fetched.append(parsed_data)
172
+ current_id += 1
173
+ time.sleep(0.7) # Be polite: 50 requests/30s = 0.6s per request. Add a small delay.
174
+ else:
175
+ # API returns empty <RESOLUTION> for non-existent IDs, signaling we are done.
176
+ print(f"GA#{current_id} does not exist. Assuming it's the last one.")
177
+ print("--- Fetching complete. ---")
178
+ break
179
+
180
+ # --- Post-Fetch Processing ---
181
+ if not newly_fetched:
182
+ print("No new resolutions found. Cache is up-to-date.")
183
+ return
184
+
185
+ print(f"Fetched {len(newly_fetched)} new resolutions.")
186
+
187
+ # Update cache with new data
188
+ updates_made = 0
189
+ for res in newly_fetched:
190
+ # Check if this new resolution repeals an older one
191
+ if res['status'] == 'Repealed' and res.get('repealed_by'):
192
+ repealed_id = res['id']
193
+ # Check if we have the repealed resolution in our cache
194
+ if repealed_id in cached_resolutions and cached_resolutions[repealed_id]['status'] == 'Active':
195
+ print(
196
+ f"Updating status for GA#{repealed_id}: was Active, now Repealed by GA#{res['repealed_by']['id']}.")
197
+ cached_resolutions[repealed_id]['status'] = 'Repealed'
198
+ cached_resolutions[repealed_id]['repealed_by'] = res['repealed_by']
199
+ updates_made += 1
200
+
201
+ # Add the new resolution to our collection
202
+ cached_resolutions[res['id']] = res
203
+
204
+ if updates_made:
205
+ print(f"Updated the status of {updates_made} existing resolutions.")
206
+
207
+ # Save the final, complete collection to the cache file
208
+ save_cache(CACHE_FILE, cached_resolutions)
209
+
210
+
211
+ if __name__ == "__main__":
212
+ main()