echr-extractor 1.0.44__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.
@@ -0,0 +1,80 @@
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(target=download_full_text_separate, args=(curr_ids, curr_ecli, all_dict))
41
+ threads.append(t)
42
+ for t in threads:
43
+ t.start()
44
+ for t in threads:
45
+ t.join()
46
+
47
+ json_file = list()
48
+ for l in all_dict:
49
+ if len(l) > 0:
50
+ json_file.extend(l)
51
+ return json_file
52
+
53
+
54
+ def download_full_text_separate(item_ids, eclis, dict_list):
55
+ full_list = []
56
+ eclis = eclis.reset_index(drop=True)
57
+ item_ids = item_ids.reset_index(drop=True)
58
+
59
+ def download_html(item_ids, eclis):
60
+ retry_ids = []
61
+ retry_eclis = []
62
+ for i in range(len(item_ids)):
63
+ item_id = item_ids[i]
64
+ ecli = eclis[i]
65
+ try:
66
+ r = requests.get(base_url + item_id, timeout=1)
67
+ json_dict = {
68
+ 'item_id': item_id,
69
+ 'ecli': ecli,
70
+ 'full_text': get_full_text_from_html(r.text)
71
+ }
72
+ full_list.append(json_dict)
73
+ except Exception:
74
+ retry_ids.append(item_id)
75
+ retry_eclis.append(ecli)
76
+ return retry_ids, retry_eclis
77
+
78
+ retry_ids, retry_eclis = download_html(item_ids, eclis)
79
+ download_html(retry_ids, retry_eclis)
80
+ dict_list.append(full_list)
@@ -0,0 +1,390 @@
1
+ import logging
2
+ from datetime import datetime
3
+
4
+ import pandas as pd
5
+ import requests
6
+
7
+
8
+ def get_r(url, timeout, retry, verbose):
9
+ """
10
+ Get data from a URL. If this is uncuccessful it is attempted again
11
+ up to a number of tries
12
+ given by retry. If it is still unsuccessful the batch is skipped.
13
+ :param str url: The data source URL.
14
+ :param double timeout: The amount of time to wait for a response
15
+ each attempt.
16
+ :param int retry: The number of times to retry upon failure.
17
+ :param bool verbose: Whether or not to print extra information.
18
+ """
19
+ count = 0
20
+ max_attempts = 20
21
+ while count < max_attempts:
22
+ try:
23
+ r = requests.get(url, timeout=timeout)
24
+ return r
25
+ except (requests.exceptions.ReadTimeout,
26
+ requests.exceptions.ConnectTimeout):
27
+ count += 1
28
+ if verbose:
29
+ logging.info(f"Timeout. Retry attempt {count}.")
30
+ if count > retry:
31
+ if verbose:
32
+ logging.info(f"Unable to connect to {url}."
33
+ f"Skipping this batch.")
34
+ return None
35
+ return None
36
+
37
+
38
+ def basic_function(term, values):
39
+ values = ['"' + i + '"' for i in values]
40
+ main_body = list()
41
+ cut_term = term.replace('"', "")
42
+ for v in values:
43
+ main_body.append(f"({cut_term}={v}) OR ({cut_term}:{v})")
44
+ query = f"({' OR '.join(main_body)})"
45
+ return query
46
+
47
+
48
+ def link_to_query(link):
49
+ # Fixing brackets
50
+ link = link.replace("%7B", "{")
51
+ link = link.replace("%7D", "}")
52
+ link = link.replace("%5B", "[")
53
+ link = link.replace("%5D", "]")
54
+ link = link.replace("%22", '"')
55
+ link = link.replace("%27", "'")
56
+
57
+ # fixing fulltext shenanigans - happen because of people using "
58
+ # in the queries.
59
+
60
+ full_text_input = ""
61
+ fulltext_end = -1
62
+ fulltext_start = link.find("fulltext")
63
+ if fulltext_start:
64
+ start = link[fulltext_start:].find("[") + fulltext_start + 1
65
+ fulltext_end = link[fulltext_start:].find("]") + fulltext_start
66
+ fragment_to_fix = link[start:fulltext_end]
67
+ full_text_input = (
68
+ "("
69
+ + "".join(fragment_to_fix.rsplit('"', 1)).replace('"', "", 1)
70
+ + ")".replace("\\", "")
71
+ )
72
+ full_text_input = full_text_input.replace("\\", "")
73
+ b = 2
74
+ # removing first and last " elements and saving the output to
75
+ # put manually later
76
+ if fulltext_end:
77
+
78
+ if link[fulltext_end + 1] == ",":
79
+ to_replace = link[fulltext_start - 1 : fulltext_end + 2]
80
+ else:
81
+ to_replace = link[fulltext_start - 1 : fulltext_end + 1]
82
+ link = link.replace(to_replace, "")
83
+
84
+ extra_cases_map = {
85
+ "bodyprocedure": '("PROCEDURE" ONEAR(n=1000) terms OR "PROCÉDURE" ONEAR(n=1000) terms)',
86
+ "bodyfacts": '("THE FACTS" ONEAR(n=1000) terms OR "EN FAIT" ONEAR(n=1000) terms)',
87
+ "bodycomplaints": '("COMPLAINTS" ONEAR(n=1000) terms OR "GRIEFS" ONEAR(n=1000) terms)',
88
+ "bodylaw": '("THE LAW" ONEAR(n=1000) terms OR "EN DROIT" ONEAR(n=1000) terms)',
89
+ "bodyreasons": '("FOR THESE REASONS" ONEAR(n=1000) terms OR "PAR CES MOTIFS" ONEAR(n=1000) terms)',
90
+ "bodyseparateopinions": '(("SEPARATE OPINION" OR "SEPARATE OPINIONS") ONEAR(n=5000) terms OR "OPINION '
91
+ 'SÉPARÉE" ONEAR(n=5000) terms)',
92
+ "bodyappendix": '("APPENDIX" ONEAR(n=1000) terms OR "ANNEXE" ONEAR(n=1000) terms)',
93
+ }
94
+
95
+ def full_text_function(term, values):
96
+ return f"({','.join(values)})"
97
+
98
+ def date_function(term, values):
99
+ values = ['"' + i + '"' for i in values]
100
+ query = "(kpdate>=first_term AND kpdate<=second_term)"
101
+ first = values[0]
102
+ second = values[1]
103
+ if first == '""':
104
+ first = '"1900-01-01"'
105
+ if second == '""':
106
+ second = datetime.today().date()
107
+ query = query.replace("first_term", first)
108
+ query = query.replace("second_term", second)
109
+ return query
110
+
111
+ def advanced_function(term, values):
112
+ body = extra_cases_map.get(term)
113
+ query = body.replace("terms", ",".join(vals))
114
+ return query
115
+
116
+ query_map = {
117
+ "docname": basic_function,
118
+ "appno": basic_function,
119
+ "scl": basic_function,
120
+ "rulesofcourt": basic_function,
121
+ "applicability": basic_function,
122
+ "ecli": basic_function,
123
+ "conclusion": basic_function,
124
+ "resolutionnumber": basic_function,
125
+ "separateopinions": basic_function,
126
+ "externalsources": basic_function,
127
+ "kpthesaurus": basic_function,
128
+ "advopidentifier": basic_function,
129
+ "documentcollectionid2": basic_function,
130
+ "fulltext": full_text_function,
131
+ "kpdate": date_function,
132
+ "bodyprocedure": advanced_function,
133
+ "bodyfacts": advanced_function,
134
+ "bodycomplaints": advanced_function,
135
+ "bodylaw": advanced_function,
136
+ "bodyreasons": advanced_function,
137
+ "bodyseparateopinions": advanced_function,
138
+ "bodyappendix": advanced_function,
139
+ "languageisocode": basic_function,
140
+ }
141
+
142
+ start = link.index("{")
143
+ end = link.rindex("}")
144
+ json_str = link[start : end + 1].replace("'", '"')
145
+
146
+ try:
147
+ link_dictionary = json.loads(json_str)
148
+ except json.JSONDecodeError:
149
+
150
+ print(f"Failed to parse JSON: {json_str}")
151
+ link_dictionary = {}
152
+ pairs = json_str.strip("{}").split(",")
153
+ for pair in pairs:
154
+ key, value = pair.split(":", 1)
155
+ key = key.strip().strip('"')
156
+ value = value.strip().strip("[]").split(",")
157
+ link_dictionary[key] = [v.strip().strip('"') for v in value]
158
+
159
+ base_query = (
160
+ "https://hudoc.echr.coe.int/app/query/results?query=contentsitename:ECHR"
161
+ " AND (NOT (doctype=PR OR doctype=HFCOMOLD OR doctype=HECOMOLD)) AND "
162
+ "inPutter&select={select}&sort=itemid%20Ascending&start={start}&length={length}"
163
+ )
164
+ query_elements = list()
165
+ if full_text_input:
166
+ query_elements.append(full_text_input)
167
+ date_addition = ""
168
+ for key in list(link_dictionary.keys()):
169
+ if key == "kpdate":
170
+ vals = link_dictionary.get(key)
171
+ funct = query_map.get(key)
172
+ date_addition = funct(key, vals)
173
+ elif key == "sort":
174
+ continue
175
+ else:
176
+ vals = link_dictionary.get(key)
177
+ funct = query_map.get(key)
178
+ query_elements.append(funct(key, vals))
179
+ if date_addition:
180
+ query_elements.append(date_addition)
181
+ query_total = " AND ".join(query_elements)
182
+ final_query = base_query.replace("inPutter", query_total)
183
+
184
+ return final_query
185
+
186
+
187
+ def determine_meta_url(link, query_payload, start_date, end_date):
188
+ if query_payload:
189
+ META_URL = (
190
+ "http://hudoc.echr.coe.int/app/query/results"
191
+ f"?query={query_payload}"
192
+ "&select={select}"
193
+ + "&sort=itemid Ascending"
194
+ + "&start={start}&length={length}"
195
+ )
196
+ elif link:
197
+ META_URL = link_to_query(link)
198
+ else:
199
+ META_URL = (
200
+ "http://hudoc.echr.coe.int/app/query/results"
201
+ "?query=(contentsitename=ECHR) AND "
202
+ '(documentcollectionid2:"JUDGMENTS" OR '
203
+ 'documentcollectionid2:"COMMUNICATEDCASES" OR '
204
+ 'documentcollectionid2:"DECISIONS" OR '
205
+ 'documentcollectionid2:"CLIN") AND '
206
+ "lang_inputter"
207
+ "&select={select}"
208
+ + "&sort=itemid Ascending"
209
+ + "&start={start}&length={length}"
210
+ )
211
+ if start_date and end_date:
212
+ addition = f'(kpdate>="{start_date}" AND kpdate<="{end_date}")'
213
+ elif start_date:
214
+ end_date = datetime.today().date()
215
+ addition = f'(kpdate>="{start_date}" AND kpdate<="{end_date}")'
216
+ elif end_date:
217
+ start_date = "1900-01-01"
218
+ addition = f'(kpdate>="{start_date}" AND kpdate<="{end_date}")'
219
+ else:
220
+ addition = ""
221
+
222
+ if addition:
223
+ addition = " AND " + addition
224
+ META_URL = META_URL.replace("&select", addition + "&select")
225
+ return META_URL
226
+
227
+
228
+ def get_echr_metadata(
229
+ start_id,
230
+ end_id,
231
+ verbose,
232
+ fields,
233
+ start_date,
234
+ end_date,
235
+ link,
236
+ language,
237
+ query_payload,
238
+ ):
239
+ """
240
+ Read ECHR metadata into a Pandas DataFrame.
241
+ :param int start_id: The index to start the search from.
242
+ :param int end_id: The index to end search at, where the default
243
+ fetches all results.
244
+ :param date start_date: The point from which to save cases.
245
+ :param date end_date: The point before which to save cases.
246
+ :param bool verbose: Whether or not to print extra information.
247
+ """
248
+ data = []
249
+ if not fields:
250
+ fields = [
251
+ "itemid",
252
+ "applicability",
253
+ "appno",
254
+ "article",
255
+ "conclusion",
256
+ "docname",
257
+ "doctype",
258
+ "doctypebranch",
259
+ "ecli",
260
+ "importance",
261
+ "judgementdate",
262
+ "languageisocode",
263
+ "originatingbody",
264
+ "violation",
265
+ "nonviolation",
266
+ "extractedappno",
267
+ "scl",
268
+ "publishedby",
269
+ "representedby",
270
+ "respondent",
271
+ "separateopinion",
272
+ "sharepointid",
273
+ "externalsources",
274
+ "issue",
275
+ "referencedate",
276
+ "rulesofcourt",
277
+ "DocId",
278
+ "WorkId",
279
+ "Rank",
280
+ "Author",
281
+ "Size",
282
+ "Path",
283
+ "Description",
284
+ "Write",
285
+ "CollapsingStatus",
286
+ "HighlightedSummary",
287
+ "HighlightedProperties",
288
+ "contentclass",
289
+ "PictureThumbnailURL",
290
+ "ServerRedirectedURL",
291
+ "ServerRedirectedEmbedURL",
292
+ "ServerRedirectedPreviewURL",
293
+ "FileExtension",
294
+ "ContentTypeId",
295
+ "ParentLink",
296
+ "ViewsLifeTime",
297
+ "ViewsRecent",
298
+ "SectionNames",
299
+ "SectionIndexes",
300
+ "SiteLogo",
301
+ "SiteDescription",
302
+ "deeplinks",
303
+ "SiteName",
304
+ "IsDocument",
305
+ "LastModifiedTime",
306
+ "FileType",
307
+ "IsContainer",
308
+ "WebTemplate",
309
+ "SecondaryFileExtension",
310
+ "docaclmeta",
311
+ "OriginalPath",
312
+ "EditorOWSUSER",
313
+ "DisplayAuthor",
314
+ "ResultTypeIdList",
315
+ "PartitionId",
316
+ "UrlZone",
317
+ "AAMEnabledManagedProperties",
318
+ "ResultTypeId",
319
+ "rendertemplateid",
320
+ ]
321
+
322
+ META_URL = determine_meta_url(link, query_payload, start_date, end_date)
323
+ # An example url: "https://hudoc.echr.coe.int/app/query/results?query=(contentsitename=ECHR)%20AND%20(documentcollectionid2:%22JUDGMENTS%22%20OR%20documentcollectionid2:%22COMMUNICATEDCASES%22%20OR%20documentcollectionid2:%22DECISIONS%22%20OR%20documentcollectionid2:%22CLIN%22)&select=itemid,applicability,application,appno,article,conclusion,decisiondate,docname,documentcollectionid,%20documentcollectionid2,doctype,doctypebranch,ecli,externalsources,extractedappno,importance,introductiondate,%20isplaceholder,issue,judgementdate,kpdate,kpdateAsText,kpthesaurus,languageisocode,meetingnumber,%20originatingbody,publishedby,Rank,referencedate,reportdate,representedby,resolutiondate,%20resolutionnumber,respondent,respondentOrderEng,rulesofcourt,separateopinion,scl,sharepointid,typedescription,%20nonviolation,violation&sort=itemid%20Ascending&start=0&length=200"
324
+
325
+ META_URL = META_URL.replace(" ", "%20")
326
+ META_URL = META_URL.replace('"', "%22")
327
+ META_URL = META_URL.replace("%5C", "")
328
+
329
+ language_input = basic_function("languageisocode", language)
330
+ if not link:
331
+ META_URL = META_URL.replace("lang_inputter", language_input)
332
+
333
+ META_URL = META_URL.replace("{select}", ",".join(fields))
334
+
335
+ url = META_URL.format(start=0, length=1)
336
+ logging.info(url)
337
+ r = requests.get(url)
338
+ resultcount = r.json()["resultcount"]
339
+ logging.info("available results: " + str(resultcount))
340
+
341
+ if not end_id:
342
+ end_id = resultcount
343
+ if verbose:
344
+ logging.info(
345
+ f"Fetching {end_id - start_id} results from index {start_id} to index {end_id} "
346
+ + f'{f" and filtering cases after {start_date}" if start_date and not link and not query_payload else ""} {f"and filtering cases before {end_date}" if end_date and not link and not query_payload else "."}'
347
+ )
348
+
349
+ timeout = 60
350
+ retry = 3
351
+ if (
352
+ start_id + end_id > 500
353
+ ): # HUDOC does not let you fetch more than 500 items in one go.
354
+ for i in range(start_id, end_id, 500):
355
+ if verbose:
356
+ logging.info(
357
+ " - Fetching information from cases {} to {}.".format(i, i + 500)
358
+ )
359
+ # Format URL based on the incremented index.
360
+ url = META_URL.format(start=i, length=500)
361
+ if verbose:
362
+ logging.info(url)
363
+
364
+ # Get the response.
365
+ r = get_r(url, timeout, retry, verbose)
366
+ if r is not None:
367
+ # Get the results list
368
+ temp_dict = r.json()["results"]
369
+ # Get every document from the results list.
370
+ for result in temp_dict:
371
+ data.append(result["columns"])
372
+
373
+ else:
374
+ # Format URL based on start and length
375
+ url = META_URL.format(start=start_id, length=end_id)
376
+ if verbose:
377
+ logging.info(url)
378
+
379
+ r = get_r(url, timeout, retry, verbose)
380
+ if r is not None:
381
+ # Get the results list
382
+ temp_dict = r.json()["results"]
383
+ # Get every document from the results list.
384
+ for result in temp_dict:
385
+ data.append(result["columns"])
386
+
387
+ if len(data) == 0:
388
+ logging.info("Search results ended up empty")
389
+ return False
390
+ return pd.DataFrame.from_records(data)