echr-extractor 0.0.1.dev1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- echr_extractor/ECHR_html_downloader.py +82 -0
- echr_extractor/ECHR_metadata_harvester.py +575 -0
- echr_extractor/ECHR_nodes_edges_list_transform.py +308 -0
- echr_extractor/__init__.py +18 -0
- echr_extractor/_version.py +34 -0
- echr_extractor/clean_ref.py +5 -0
- echr_extractor/cli.py +125 -0
- echr_extractor/echr.py +266 -0
- echr_extractor/testing_file.py +20 -0
- echr_extractor-0.0.1.dev1.dist-info/METADATA +189 -0
- echr_extractor-0.0.1.dev1.dist-info/RECORD +15 -0
- echr_extractor-0.0.1.dev1.dist-info/WHEEL +5 -0
- echr_extractor-0.0.1.dev1.dist-info/entry_points.txt +2 -0
- echr_extractor-0.0.1.dev1.dist-info/licenses/LICENSE +201 -0
- echr_extractor-0.0.1.dev1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
from bs4 import BeautifulSoup
|
|
5
|
+
|
|
6
|
+
base_url = "https://hudoc.echr.coe.int/app/conversion/docx/html/body?library=ECHR&id="
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_full_text_from_html(html_text):
|
|
10
|
+
# This method turns the html code from the summary page into text
|
|
11
|
+
# It has different cases depending on the first character of the CELEX ID
|
|
12
|
+
# Should only be used for summaries extraction
|
|
13
|
+
soup = BeautifulSoup(html_text, "html.parser")
|
|
14
|
+
for script in soup(["script", "style"]):
|
|
15
|
+
script.extract() # rip it out
|
|
16
|
+
text = soup.get_text()
|
|
17
|
+
# break into lines and remove leading and trailing space on each
|
|
18
|
+
lines = (line.strip() for line in text.splitlines())
|
|
19
|
+
# break multi-headlines into a line each
|
|
20
|
+
chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
|
|
21
|
+
# drop blank lines
|
|
22
|
+
text = "\n".join(chunk for chunk in chunks if chunk)
|
|
23
|
+
text = text.replace(",", "_")
|
|
24
|
+
return text
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def download_full_text_main(df, threads):
|
|
28
|
+
item_ids = df["itemid"]
|
|
29
|
+
eclis = df["ecli"]
|
|
30
|
+
length = item_ids.size
|
|
31
|
+
if length > threads:
|
|
32
|
+
at_once_threads = int(length / threads)
|
|
33
|
+
else:
|
|
34
|
+
at_once_threads = length
|
|
35
|
+
all_dict = list()
|
|
36
|
+
threads = []
|
|
37
|
+
for i in range(0, length, at_once_threads):
|
|
38
|
+
curr_ids = item_ids[i : (i + at_once_threads)]
|
|
39
|
+
curr_ecli = eclis[i : (i + at_once_threads)]
|
|
40
|
+
t = threading.Thread(
|
|
41
|
+
target=download_full_text_separate, args=(curr_ids, curr_ecli, all_dict)
|
|
42
|
+
)
|
|
43
|
+
threads.append(t)
|
|
44
|
+
for t in threads:
|
|
45
|
+
t.start()
|
|
46
|
+
for t in threads:
|
|
47
|
+
t.join()
|
|
48
|
+
|
|
49
|
+
json_file = list()
|
|
50
|
+
for item in all_dict:
|
|
51
|
+
if len(item) > 0:
|
|
52
|
+
json_file.extend(item)
|
|
53
|
+
return json_file
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def download_full_text_separate(item_ids, eclis, dict_list):
|
|
57
|
+
full_list = []
|
|
58
|
+
eclis = eclis.reset_index(drop=True)
|
|
59
|
+
item_ids = item_ids.reset_index(drop=True)
|
|
60
|
+
|
|
61
|
+
def download_html(item_ids, eclis):
|
|
62
|
+
retry_ids = []
|
|
63
|
+
retry_eclis = []
|
|
64
|
+
for i in range(len(item_ids)):
|
|
65
|
+
item_id = item_ids[i]
|
|
66
|
+
ecli = eclis[i]
|
|
67
|
+
try:
|
|
68
|
+
r = requests.get(base_url + item_id, timeout=1)
|
|
69
|
+
json_dict = {
|
|
70
|
+
"item_id": item_id,
|
|
71
|
+
"ecli": ecli,
|
|
72
|
+
"full_text": get_full_text_from_html(r.text),
|
|
73
|
+
}
|
|
74
|
+
full_list.append(json_dict)
|
|
75
|
+
except Exception:
|
|
76
|
+
retry_ids.append(item_id)
|
|
77
|
+
retry_eclis.append(ecli)
|
|
78
|
+
return retry_ids, retry_eclis
|
|
79
|
+
|
|
80
|
+
retry_ids, retry_eclis = download_html(item_ids, eclis)
|
|
81
|
+
download_html(retry_ids, retry_eclis)
|
|
82
|
+
dict_list.append(full_list)
|
|
@@ -0,0 +1,575 @@
|
|
|
1
|
+
import gc
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
import urllib.parse
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import requests
|
|
10
|
+
from tqdm import tqdm
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_r(url, timeout, retry, verbose, max_attempts=20):
|
|
14
|
+
"""
|
|
15
|
+
Enhanced get data from a URL with improved error handling and retry logic.
|
|
16
|
+
|
|
17
|
+
:param str url: The data source URL.
|
|
18
|
+
:param float timeout: The amount of time to wait for a response each attempt.
|
|
19
|
+
:param int retry: The number of times to retry upon failure.
|
|
20
|
+
:param bool verbose: Whether or not to print extra information.
|
|
21
|
+
:param int max_attempts: Maximum number of attempts before giving up.
|
|
22
|
+
:return: requests.Response object or None if all attempts failed.
|
|
23
|
+
"""
|
|
24
|
+
count = 0
|
|
25
|
+
last_exception = None
|
|
26
|
+
|
|
27
|
+
while count < max_attempts:
|
|
28
|
+
try:
|
|
29
|
+
r = requests.get(url, timeout=timeout)
|
|
30
|
+
r.raise_for_status() # Raise an exception for bad status codes
|
|
31
|
+
return r
|
|
32
|
+
except (
|
|
33
|
+
requests.exceptions.ReadTimeout,
|
|
34
|
+
requests.exceptions.ConnectTimeout,
|
|
35
|
+
requests.exceptions.ConnectionError,
|
|
36
|
+
requests.exceptions.HTTPError,
|
|
37
|
+
) as e:
|
|
38
|
+
last_exception = e
|
|
39
|
+
count += 1
|
|
40
|
+
if verbose:
|
|
41
|
+
logging.warning(
|
|
42
|
+
f"Request failed (attempt {count}/{max_attempts}): {type(e).__name__}: {str(e)}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if count <= retry:
|
|
46
|
+
# Exponential backoff
|
|
47
|
+
wait_time = min(2**count, 30) # Cap at 30 seconds
|
|
48
|
+
if verbose:
|
|
49
|
+
logging.info(f"Retrying in {wait_time} seconds...")
|
|
50
|
+
time.sleep(wait_time)
|
|
51
|
+
else:
|
|
52
|
+
if verbose:
|
|
53
|
+
logging.error(
|
|
54
|
+
f"Unable to connect to {url} after {count} attempts. Last error: {last_exception}"
|
|
55
|
+
)
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
if verbose:
|
|
59
|
+
logging.error(
|
|
60
|
+
f"Max attempts ({max_attempts}) exceeded for {url}. Last error: {last_exception}"
|
|
61
|
+
)
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_date_ranges(start_date, end_date, days_per_batch=365):
|
|
66
|
+
"""
|
|
67
|
+
Split a date range into smaller batches to prevent timeouts and memory issues.
|
|
68
|
+
|
|
69
|
+
:param str start_date: Start date in YYYY-MM-DD format
|
|
70
|
+
:param str end_date: End date in YYYY-MM-DD format
|
|
71
|
+
:param int days_per_batch: Number of days per batch
|
|
72
|
+
:return: List of (start_date, end_date) tuples
|
|
73
|
+
"""
|
|
74
|
+
if not start_date or not end_date:
|
|
75
|
+
return [(start_date, end_date)]
|
|
76
|
+
|
|
77
|
+
start_dt = datetime.strptime(start_date, "%Y-%m-%d")
|
|
78
|
+
end_dt = datetime.strptime(end_date, "%Y-%m-%d")
|
|
79
|
+
|
|
80
|
+
date_ranges = []
|
|
81
|
+
current_start = start_dt
|
|
82
|
+
|
|
83
|
+
while current_start < end_dt:
|
|
84
|
+
current_end = min(current_start + timedelta(days=days_per_batch - 1), end_dt)
|
|
85
|
+
date_ranges.append(
|
|
86
|
+
(current_start.strftime("%Y-%m-%d"), current_end.strftime("%Y-%m-%d"))
|
|
87
|
+
)
|
|
88
|
+
current_start = current_end + timedelta(days=1)
|
|
89
|
+
|
|
90
|
+
return date_ranges
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def basic_function(term, values):
|
|
94
|
+
values = ['"' + i + '"' for i in values]
|
|
95
|
+
main_body = list()
|
|
96
|
+
cut_term = term.replace('"', "")
|
|
97
|
+
for v in values:
|
|
98
|
+
main_body.append(f"({cut_term}={v}) OR ({cut_term}:{v})")
|
|
99
|
+
query = f"({' OR '.join(main_body)})"
|
|
100
|
+
return query
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def link_to_query(link):
|
|
104
|
+
# Fixing brackets
|
|
105
|
+
link = link.replace("%7B", "{")
|
|
106
|
+
link = link.replace("%7D", "}")
|
|
107
|
+
link = link.replace("%5B", "[")
|
|
108
|
+
link = link.replace("%5D", "]")
|
|
109
|
+
link = link.replace("%22", '"')
|
|
110
|
+
link = link.replace("%27", "'")
|
|
111
|
+
|
|
112
|
+
# fixing fulltext shenanigans - happen because of people using "
|
|
113
|
+
# in the queries.
|
|
114
|
+
|
|
115
|
+
full_text_input = ""
|
|
116
|
+
fulltext_end = -1
|
|
117
|
+
fulltext_start = link.find("fulltext")
|
|
118
|
+
if fulltext_start != -1: # Fixed: check for -1 instead of truthy value
|
|
119
|
+
start = link[fulltext_start:].find("[") + fulltext_start + 1
|
|
120
|
+
fulltext_end = link[fulltext_start:].find("]") + fulltext_start
|
|
121
|
+
fragment_to_fix = link[start:fulltext_end]
|
|
122
|
+
full_text_input = (
|
|
123
|
+
"("
|
|
124
|
+
+ "".join(fragment_to_fix.rsplit('"', 1)).replace('"', "", 1)
|
|
125
|
+
+ ")".replace("\\", "")
|
|
126
|
+
)
|
|
127
|
+
full_text_input = full_text_input.replace("\\", "")
|
|
128
|
+
# removing first and last " elements and saving the output to
|
|
129
|
+
# put manually later
|
|
130
|
+
if fulltext_end != -1: # Fixed: check for -1 instead of truthy value
|
|
131
|
+
|
|
132
|
+
if link[fulltext_end + 1] == ",":
|
|
133
|
+
to_replace = link[fulltext_start - 1 : fulltext_end + 2]
|
|
134
|
+
else:
|
|
135
|
+
to_replace = link[fulltext_start - 1 : fulltext_end + 1]
|
|
136
|
+
link = link.replace(to_replace, "")
|
|
137
|
+
|
|
138
|
+
extra_cases_map = {
|
|
139
|
+
"bodyprocedure": (
|
|
140
|
+
'("PROCEDURE" ONEAR(n=1000) terms OR "PROCÉDURE" ONEAR(n=1000) terms)'
|
|
141
|
+
),
|
|
142
|
+
"bodyfacts": (
|
|
143
|
+
'("THE FACTS" ONEAR(n=1000) terms OR "EN FAIT" ONEAR(n=1000) terms)'
|
|
144
|
+
),
|
|
145
|
+
"bodycomplaints": (
|
|
146
|
+
'("COMPLAINTS" ONEAR(n=1000) terms OR "GRIEFS" ONEAR(n=1000) terms)'
|
|
147
|
+
),
|
|
148
|
+
"bodylaw": (
|
|
149
|
+
'("THE LAW" ONEAR(n=1000) terms OR "EN DROIT" ONEAR(n=1000) terms)'
|
|
150
|
+
),
|
|
151
|
+
"bodyreasons": (
|
|
152
|
+
'("FOR THESE REASONS" ONEAR(n=1000) terms OR '
|
|
153
|
+
'"PAR CES MOTIFS" ONEAR(n=1000) terms)'
|
|
154
|
+
),
|
|
155
|
+
"bodyseparateopinions": (
|
|
156
|
+
'(("SEPARATE OPINION" OR "SEPARATE OPINIONS") ONEAR(n=5000) terms OR '
|
|
157
|
+
'"OPINION SÉPARÉE" ONEAR(n=5000) terms)'
|
|
158
|
+
),
|
|
159
|
+
"bodyappendix": (
|
|
160
|
+
'("APPENDIX" ONEAR(n=1000) terms OR "ANNEXE" ONEAR(n=1000) terms)'
|
|
161
|
+
),
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
def full_text_function(term, values):
|
|
165
|
+
return f"({','.join(values)})"
|
|
166
|
+
|
|
167
|
+
def date_function(term, values):
|
|
168
|
+
values = ['"' + i + '"' for i in values]
|
|
169
|
+
query = "(kpdate>=first_term AND kpdate<=second_term)"
|
|
170
|
+
first = values[0]
|
|
171
|
+
second = values[1]
|
|
172
|
+
if first == '""':
|
|
173
|
+
first = '"1900-01-01"'
|
|
174
|
+
if second == '""':
|
|
175
|
+
second = str(datetime.today().date())
|
|
176
|
+
query = query.replace("first_term", first)
|
|
177
|
+
query = query.replace("second_term", second)
|
|
178
|
+
return query
|
|
179
|
+
|
|
180
|
+
def advanced_function(term, values):
|
|
181
|
+
body = extra_cases_map.get(term)
|
|
182
|
+
query = body.replace("terms", ",".join(values))
|
|
183
|
+
return query
|
|
184
|
+
|
|
185
|
+
query_map = {
|
|
186
|
+
"docname": basic_function,
|
|
187
|
+
"appno": basic_function,
|
|
188
|
+
"scl": basic_function,
|
|
189
|
+
"rulesofcourt": basic_function,
|
|
190
|
+
"applicability": basic_function,
|
|
191
|
+
"ecli": basic_function,
|
|
192
|
+
"conclusion": basic_function,
|
|
193
|
+
"resolutionnumber": basic_function,
|
|
194
|
+
"separateopinions": basic_function,
|
|
195
|
+
"externalsources": basic_function,
|
|
196
|
+
"kpthesaurus": basic_function,
|
|
197
|
+
"advopidentifier": basic_function,
|
|
198
|
+
"documentcollectionid2": basic_function,
|
|
199
|
+
"itemid": basic_function, # Added support for itemid
|
|
200
|
+
"fulltext": full_text_function,
|
|
201
|
+
"kpdate": date_function,
|
|
202
|
+
"bodyprocedure": advanced_function,
|
|
203
|
+
"bodyfacts": advanced_function,
|
|
204
|
+
"bodycomplaints": advanced_function,
|
|
205
|
+
"bodylaw": advanced_function,
|
|
206
|
+
"bodyreasons": advanced_function,
|
|
207
|
+
"bodyseparateopinions": advanced_function,
|
|
208
|
+
"bodyappendix": advanced_function,
|
|
209
|
+
"languageisocode": basic_function,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
start = link.index("{")
|
|
213
|
+
end = link.rindex("}")
|
|
214
|
+
json_str = link[start : end + 1].replace("'", '"')
|
|
215
|
+
|
|
216
|
+
# URL decode the JSON string before parsing
|
|
217
|
+
decoded_json_str = urllib.parse.unquote(json_str)
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
link_dictionary = json.loads(decoded_json_str)
|
|
221
|
+
except json.JSONDecodeError:
|
|
222
|
+
# Fallback parsing for malformed JSON
|
|
223
|
+
link_dictionary = {}
|
|
224
|
+
pairs = decoded_json_str.strip("{}").split(",")
|
|
225
|
+
for pair in pairs:
|
|
226
|
+
key, value = pair.split(":", 1)
|
|
227
|
+
key = key.strip().strip('"')
|
|
228
|
+
value = value.strip().strip("[]").split(",")
|
|
229
|
+
link_dictionary[key] = [v.strip().strip('"') for v in value]
|
|
230
|
+
|
|
231
|
+
base_query = (
|
|
232
|
+
"https://hudoc.echr.coe.int/app/query/results?query=contentsitename:ECHR"
|
|
233
|
+
" AND (NOT (doctype=PR OR doctype=HFCOMOLD OR doctype=HECOMOLD)) AND "
|
|
234
|
+
"inPutter&select={select}&sort=itemid%20Ascending&start={start}"
|
|
235
|
+
"&length={length}"
|
|
236
|
+
)
|
|
237
|
+
query_elements = list()
|
|
238
|
+
if full_text_input:
|
|
239
|
+
query_elements.append(full_text_input)
|
|
240
|
+
date_addition = ""
|
|
241
|
+
for key in list(link_dictionary.keys()):
|
|
242
|
+
if key == "kpdate":
|
|
243
|
+
vals = link_dictionary.get(key)
|
|
244
|
+
funct = query_map.get(key)
|
|
245
|
+
date_addition = funct(key, vals)
|
|
246
|
+
elif key == "sort":
|
|
247
|
+
continue
|
|
248
|
+
else:
|
|
249
|
+
vals = link_dictionary.get(key)
|
|
250
|
+
funct = query_map.get(key)
|
|
251
|
+
if funct is not None:
|
|
252
|
+
query_elements.append(funct(key, vals))
|
|
253
|
+
else:
|
|
254
|
+
# Handle unknown keys by using basic_function as fallback
|
|
255
|
+
query_elements.append(basic_function(key, vals))
|
|
256
|
+
if date_addition:
|
|
257
|
+
query_elements.append(date_addition)
|
|
258
|
+
query_total = " AND ".join(query_elements)
|
|
259
|
+
final_query = base_query.replace("inPutter", query_total)
|
|
260
|
+
|
|
261
|
+
return final_query
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def determine_meta_url(link, query_payload, start_date, end_date):
|
|
265
|
+
if query_payload:
|
|
266
|
+
# URL encode the query_payload to avoid issues with special characters
|
|
267
|
+
encoded_payload = urllib.parse.quote(query_payload, safe="")
|
|
268
|
+
META_URL = (
|
|
269
|
+
"http://hudoc.echr.coe.int/app/query/results"
|
|
270
|
+
f"?query={encoded_payload}"
|
|
271
|
+
"&select={select}"
|
|
272
|
+
+ "&sort=itemid Ascending"
|
|
273
|
+
+ "&start={start}&length={length}"
|
|
274
|
+
)
|
|
275
|
+
elif link:
|
|
276
|
+
META_URL = link_to_query(link)
|
|
277
|
+
else:
|
|
278
|
+
META_URL = (
|
|
279
|
+
"http://hudoc.echr.coe.int/app/query/results"
|
|
280
|
+
"?query=(contentsitename=ECHR) AND "
|
|
281
|
+
'(documentcollectionid2:"JUDGMENTS" OR '
|
|
282
|
+
'documentcollectionid2:"COMMUNICATEDCASES" OR '
|
|
283
|
+
'documentcollectionid2:"DECISIONS" OR '
|
|
284
|
+
'documentcollectionid2:"CLIN") AND '
|
|
285
|
+
"lang_inputter"
|
|
286
|
+
"&select={select}"
|
|
287
|
+
+ "&sort=itemid Ascending"
|
|
288
|
+
+ "&start={start}&length={length}"
|
|
289
|
+
)
|
|
290
|
+
if start_date and end_date:
|
|
291
|
+
addition = f'(kpdate>="{start_date}" AND kpdate<="{end_date}")'
|
|
292
|
+
elif start_date:
|
|
293
|
+
end_date = datetime.today().date()
|
|
294
|
+
addition = f'(kpdate>="{start_date}" AND kpdate<="{end_date}")'
|
|
295
|
+
elif end_date:
|
|
296
|
+
start_date = "1900-01-01"
|
|
297
|
+
addition = f'(kpdate>="{start_date}" AND kpdate<="{end_date}")'
|
|
298
|
+
else:
|
|
299
|
+
addition = ""
|
|
300
|
+
|
|
301
|
+
if addition:
|
|
302
|
+
addition = " AND " + addition
|
|
303
|
+
META_URL = META_URL.replace("&select", addition + "&select")
|
|
304
|
+
return META_URL
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def get_echr_metadata(
|
|
308
|
+
start_id,
|
|
309
|
+
end_id,
|
|
310
|
+
verbose,
|
|
311
|
+
fields,
|
|
312
|
+
start_date,
|
|
313
|
+
end_date,
|
|
314
|
+
link,
|
|
315
|
+
language,
|
|
316
|
+
query_payload,
|
|
317
|
+
# New configuration parameters with defaults for backward compatibility
|
|
318
|
+
batch_size=500,
|
|
319
|
+
timeout=60,
|
|
320
|
+
retry_attempts=3,
|
|
321
|
+
max_attempts=20,
|
|
322
|
+
days_per_batch=365,
|
|
323
|
+
progress_bar=True,
|
|
324
|
+
memory_efficient=True,
|
|
325
|
+
):
|
|
326
|
+
"""
|
|
327
|
+
Enhanced ECHR metadata extraction with improved batching, error handling, and memory management.
|
|
328
|
+
|
|
329
|
+
:param int start_id: The index to start the search from.
|
|
330
|
+
:param int end_id: The index to end search at, where the default fetches all results.
|
|
331
|
+
:param bool verbose: Whether or not to print extra information.
|
|
332
|
+
:param list fields: List of fields to extract.
|
|
333
|
+
:param str start_date: The point from which to save cases (YYYY-MM-DD format).
|
|
334
|
+
:param str end_date: The point before which to save cases (YYYY-MM-DD format).
|
|
335
|
+
:param str link: Custom HUDOC link.
|
|
336
|
+
:param list language: List of language codes.
|
|
337
|
+
:param str query_payload: Custom query payload.
|
|
338
|
+
:param int batch_size: Number of records to fetch per batch (max 500).
|
|
339
|
+
:param float timeout: Request timeout in seconds.
|
|
340
|
+
:param int retry_attempts: Number of retry attempts for failed requests.
|
|
341
|
+
:param int max_attempts: Maximum total attempts before giving up.
|
|
342
|
+
:param int days_per_batch: Number of days per date batch for large date ranges.
|
|
343
|
+
:param bool progress_bar: Whether to show progress bar.
|
|
344
|
+
:param bool memory_efficient: Whether to use memory-efficient processing.
|
|
345
|
+
:return: pandas.DataFrame or False if no data retrieved.
|
|
346
|
+
"""
|
|
347
|
+
# Set default fields if not provided
|
|
348
|
+
if not fields:
|
|
349
|
+
fields = [
|
|
350
|
+
"itemid",
|
|
351
|
+
"applicability",
|
|
352
|
+
"appno",
|
|
353
|
+
"article",
|
|
354
|
+
"conclusion",
|
|
355
|
+
"docname",
|
|
356
|
+
"doctype",
|
|
357
|
+
"doctypebranch",
|
|
358
|
+
"ecli",
|
|
359
|
+
"importance",
|
|
360
|
+
"judgementdate",
|
|
361
|
+
"languageisocode",
|
|
362
|
+
"originatingbody",
|
|
363
|
+
"violation",
|
|
364
|
+
"nonviolation",
|
|
365
|
+
"extractedappno",
|
|
366
|
+
"scl",
|
|
367
|
+
"publishedby",
|
|
368
|
+
"representedby",
|
|
369
|
+
"respondent",
|
|
370
|
+
"separateopinion",
|
|
371
|
+
"sharepointid",
|
|
372
|
+
"externalsources",
|
|
373
|
+
"issue",
|
|
374
|
+
"referencedate",
|
|
375
|
+
"rulesofcourt",
|
|
376
|
+
"DocId",
|
|
377
|
+
"WorkId",
|
|
378
|
+
"Rank",
|
|
379
|
+
"Author",
|
|
380
|
+
"Size",
|
|
381
|
+
"Path",
|
|
382
|
+
"Description",
|
|
383
|
+
"Write",
|
|
384
|
+
"CollapsingStatus",
|
|
385
|
+
"HighlightedSummary",
|
|
386
|
+
"HighlightedProperties",
|
|
387
|
+
"contentclass",
|
|
388
|
+
"PictureThumbnailURL",
|
|
389
|
+
"ServerRedirectedURL",
|
|
390
|
+
"ServerRedirectedEmbedURL",
|
|
391
|
+
"ServerRedirectedPreviewURL",
|
|
392
|
+
"FileExtension",
|
|
393
|
+
"ContentTypeId",
|
|
394
|
+
"ParentLink",
|
|
395
|
+
"ViewsLifeTime",
|
|
396
|
+
"ViewsRecent",
|
|
397
|
+
"SectionNames",
|
|
398
|
+
"SectionIndexes",
|
|
399
|
+
"SiteLogo",
|
|
400
|
+
"SiteDescription",
|
|
401
|
+
"deeplinks",
|
|
402
|
+
"SiteName",
|
|
403
|
+
"IsDocument",
|
|
404
|
+
"LastModifiedTime",
|
|
405
|
+
"FileType",
|
|
406
|
+
"IsContainer",
|
|
407
|
+
"WebTemplate",
|
|
408
|
+
"SecondaryFileExtension",
|
|
409
|
+
"docaclmeta",
|
|
410
|
+
"OriginalPath",
|
|
411
|
+
"EditorOWSUSER",
|
|
412
|
+
"DisplayAuthor",
|
|
413
|
+
"ResultTypeIdList",
|
|
414
|
+
"PartitionId",
|
|
415
|
+
"UrlZone",
|
|
416
|
+
"AAMEnabledManagedProperties",
|
|
417
|
+
"ResultTypeId",
|
|
418
|
+
"rendertemplateid",
|
|
419
|
+
]
|
|
420
|
+
|
|
421
|
+
# Determine if we need date batching
|
|
422
|
+
use_date_batching = start_date and end_date and not link and not query_payload
|
|
423
|
+
|
|
424
|
+
if use_date_batching:
|
|
425
|
+
date_ranges = get_date_ranges(start_date, end_date, days_per_batch)
|
|
426
|
+
if verbose:
|
|
427
|
+
logging.info(
|
|
428
|
+
f"Date range split into {len(date_ranges)} batches of {days_per_batch} days each"
|
|
429
|
+
)
|
|
430
|
+
else:
|
|
431
|
+
date_ranges = [(start_date, end_date)]
|
|
432
|
+
|
|
433
|
+
all_data = []
|
|
434
|
+
total_processed = 0
|
|
435
|
+
total_failed = 0
|
|
436
|
+
|
|
437
|
+
for batch_idx, (batch_start_date, batch_end_date) in enumerate(date_ranges):
|
|
438
|
+
if verbose:
|
|
439
|
+
logging.info(
|
|
440
|
+
f"Processing date batch {batch_idx + 1}/{len(date_ranges)}: {batch_start_date} to {batch_end_date}"
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
# Determine meta URL for this batch
|
|
444
|
+
META_URL = determine_meta_url(
|
|
445
|
+
link, query_payload, batch_start_date, batch_end_date
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# URL encoding
|
|
449
|
+
META_URL = META_URL.replace(" ", "%20")
|
|
450
|
+
META_URL = META_URL.replace('"', "%22")
|
|
451
|
+
META_URL = META_URL.replace("%5C", "")
|
|
452
|
+
|
|
453
|
+
# Language handling
|
|
454
|
+
language_input = basic_function("languageisocode", language)
|
|
455
|
+
if not link:
|
|
456
|
+
META_URL = META_URL.replace("lang_inputter", language_input)
|
|
457
|
+
|
|
458
|
+
META_URL = META_URL.replace("{select}", ",".join(fields))
|
|
459
|
+
|
|
460
|
+
# Get total result count for this batch
|
|
461
|
+
url = META_URL.format(start=0, length=1)
|
|
462
|
+
if verbose:
|
|
463
|
+
logging.info(f"Checking result count: {url}")
|
|
464
|
+
|
|
465
|
+
r = get_r(url, timeout, retry_attempts, verbose, max_attempts)
|
|
466
|
+
if r is None:
|
|
467
|
+
logging.error(f"Failed to get result count for batch {batch_idx + 1}")
|
|
468
|
+
total_failed += 1
|
|
469
|
+
continue
|
|
470
|
+
|
|
471
|
+
try:
|
|
472
|
+
resultcount = r.json()["resultcount"]
|
|
473
|
+
if verbose:
|
|
474
|
+
logging.info(f"Available results for this batch: {resultcount}")
|
|
475
|
+
except (KeyError, ValueError) as e:
|
|
476
|
+
logging.error(f"Failed to parse result count: {e}")
|
|
477
|
+
total_failed += 1
|
|
478
|
+
continue
|
|
479
|
+
|
|
480
|
+
if resultcount == 0:
|
|
481
|
+
if verbose:
|
|
482
|
+
logging.info(f"No results found for batch {batch_idx + 1}")
|
|
483
|
+
continue
|
|
484
|
+
|
|
485
|
+
# Determine actual end_id for this batch
|
|
486
|
+
batch_end_id = min(end_id, resultcount) if end_id else resultcount
|
|
487
|
+
batch_start_id = start_id if batch_idx == 0 else 0
|
|
488
|
+
|
|
489
|
+
if verbose:
|
|
490
|
+
msg = f"Fetching {batch_end_id - batch_start_id} results from index {batch_start_id} to {batch_end_id}"
|
|
491
|
+
if batch_start_date and batch_end_date:
|
|
492
|
+
msg += f" for date range {batch_start_date} to {batch_end_date}"
|
|
493
|
+
logging.info(msg)
|
|
494
|
+
|
|
495
|
+
# Process this batch
|
|
496
|
+
batch_data = []
|
|
497
|
+
batch_processed = 0
|
|
498
|
+
batch_failed = 0
|
|
499
|
+
|
|
500
|
+
# Create progress bar for this batch
|
|
501
|
+
if progress_bar and (batch_end_id - batch_start_id) > batch_size:
|
|
502
|
+
pbar = tqdm(
|
|
503
|
+
total=batch_end_id - batch_start_id,
|
|
504
|
+
desc=f"Batch {batch_idx + 1}/{len(date_ranges)}",
|
|
505
|
+
unit="records",
|
|
506
|
+
leave=False,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# Process in batches of batch_size
|
|
510
|
+
for i in range(batch_start_id, batch_end_id, batch_size):
|
|
511
|
+
current_batch_size = min(batch_size, batch_end_id - i)
|
|
512
|
+
|
|
513
|
+
if verbose and not progress_bar:
|
|
514
|
+
logging.info(f"Fetching records {i} to {i + current_batch_size}")
|
|
515
|
+
|
|
516
|
+
url = META_URL.format(start=i, length=current_batch_size)
|
|
517
|
+
r = get_r(url, timeout, retry_attempts, verbose, max_attempts)
|
|
518
|
+
|
|
519
|
+
if r is not None:
|
|
520
|
+
try:
|
|
521
|
+
temp_dict = r.json()["results"]
|
|
522
|
+
for result in temp_dict:
|
|
523
|
+
batch_data.append(result["columns"])
|
|
524
|
+
batch_processed += len(temp_dict)
|
|
525
|
+
|
|
526
|
+
if progress_bar and (batch_end_id - batch_start_id) > batch_size:
|
|
527
|
+
pbar.update(len(temp_dict))
|
|
528
|
+
|
|
529
|
+
except (KeyError, ValueError) as e:
|
|
530
|
+
logging.error(f"Failed to parse results: {e}")
|
|
531
|
+
batch_failed += 1
|
|
532
|
+
else:
|
|
533
|
+
batch_failed += 1
|
|
534
|
+
if progress_bar and (batch_end_id - batch_start_id) > batch_size:
|
|
535
|
+
pbar.update(current_batch_size)
|
|
536
|
+
|
|
537
|
+
# Memory management: process data in chunks if memory_efficient is True
|
|
538
|
+
if memory_efficient and len(batch_data) > 10000:
|
|
539
|
+
all_data.extend(batch_data)
|
|
540
|
+
batch_data = []
|
|
541
|
+
gc.collect() # Force garbage collection
|
|
542
|
+
|
|
543
|
+
# Close progress bar for this batch
|
|
544
|
+
if progress_bar and (batch_end_id - batch_start_id) > batch_size:
|
|
545
|
+
pbar.close()
|
|
546
|
+
|
|
547
|
+
# Add remaining batch data
|
|
548
|
+
all_data.extend(batch_data)
|
|
549
|
+
total_processed += batch_processed
|
|
550
|
+
total_failed += batch_failed
|
|
551
|
+
|
|
552
|
+
if verbose:
|
|
553
|
+
logging.info(
|
|
554
|
+
f"Batch {batch_idx + 1} completed: {batch_processed} processed, {batch_failed} failed"
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
# Final summary
|
|
558
|
+
if verbose:
|
|
559
|
+
logging.info(
|
|
560
|
+
f"Total processing complete: {total_processed} records processed, {total_failed} batches failed"
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
if len(all_data) == 0:
|
|
564
|
+
logging.warning("No data retrieved from any batch")
|
|
565
|
+
return False
|
|
566
|
+
|
|
567
|
+
# Create DataFrame
|
|
568
|
+
df = pd.DataFrame.from_records(all_data)
|
|
569
|
+
|
|
570
|
+
if verbose:
|
|
571
|
+
logging.info(
|
|
572
|
+
f"Created DataFrame with {len(df)} records and {len(df.columns)} columns"
|
|
573
|
+
)
|
|
574
|
+
|
|
575
|
+
return df
|