tulit 0.0.1__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.
tulit/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """
2
+ This package provides functions to send a SPARQL query to the SPARQL endpoint of the Publications Office of the European Union (CELLAR),
3
+ retrieve the results, and store them in a JSON file.
4
+ """
@@ -0,0 +1,23 @@
1
+ """
2
+ This module provides functionality for [describe the main purpose of the module].
3
+
4
+ Classes:
5
+ [ClassName]: [Brief description of the class]
6
+
7
+ Functions:
8
+ [function_name_1]: [Brief description of the function]
9
+ [function_name_2]: [Brief description of the function]
10
+ ...
11
+
12
+ Usage:
13
+ [Provide a brief example of how to use the module]
14
+
15
+ Notes:
16
+ [Any additional notes or important information about the module]
17
+
18
+ Author:
19
+ [Your Name]
20
+
21
+ Date:
22
+ [Date of creation or last modification]
23
+ """
@@ -0,0 +1,163 @@
1
+ import logging
2
+
3
+ import requests
4
+ import json
5
+ from tulit.download.download import DocumentDownloader
6
+
7
+ class CellarDownloader(DocumentDownloader):
8
+
9
+ def __init__(self, download_dir, log_dir):
10
+ super().__init__(download_dir, log_dir)
11
+ self.endpoint = 'http://publications.europa.eu/resource/cellar/'
12
+
13
+
14
+ def fetch_content(self, url) -> requests.Response:
15
+ """
16
+ Send a GET request to download a file
17
+
18
+ Parameters
19
+ ----------
20
+ url : str
21
+ The URL to send the request to.
22
+
23
+ Returns
24
+ -------
25
+ requests.Response
26
+ The response from the server.
27
+
28
+ Notes
29
+ -----
30
+ The request is sent with the following headers:
31
+ - Accept: application/zip;mtype=fmx4, application/xml;mtype=fmx4, application/xhtml+xml, text/html, text/html;type=simplified, application/msword, text/plain, application/xml;notice=object
32
+ - Accept-Language: eng
33
+ - Content-Type: application/x-www-form-urlencoded
34
+ - Host: publications.europa.eu
35
+
36
+ Raises
37
+ ------
38
+ requests.RequestException
39
+ If there is an error sending the request.
40
+
41
+ See Also
42
+ --------
43
+ requests : The underlying library used for making HTTP requests.
44
+
45
+ """
46
+ try:
47
+ headers = {
48
+ 'Accept': "*, application/zip, application/zip;mtype=fmx4, application/xml;mtype=fmx4, application/xhtml+xml, text/html, text/html;type=simplified, application/msword, text/plain, application/xml, application/xml;notice=object",
49
+ 'Accept-Language': "eng",
50
+ 'Content-Type': "application/x-www-form-urlencoded",
51
+ 'Host': "publications.europa.eu"
52
+ }
53
+ response = requests.request("GET", url, headers=headers)
54
+ response.raise_for_status()
55
+ return response
56
+ except requests.RequestException as e:
57
+ logging.error(f"Error sending GET request: {e}")
58
+ return None
59
+
60
+ def build_request_url(self, params):
61
+ """
62
+ Build the request URL based on the source and parameters.
63
+ """
64
+ url = f"{self.endpoint}{params['cellar']}"
65
+
66
+ return url
67
+
68
+ def get_cellar_ids_from_json_results(self, results, format):
69
+ """
70
+ Extract CELLAR ids from a JSON dictionary.
71
+
72
+ Parameters
73
+ ----------
74
+ cellar_results : dict
75
+ A dictionary containing the response of the CELLAR SPARQL query
76
+
77
+ Returns
78
+ -------
79
+ list
80
+ A list of CELLAR ids.
81
+
82
+ Notes
83
+ -----
84
+ The function assumes that the JSON dictionary has the following structure:
85
+ - The dictionary contains a key "results" that maps to another dictionary.
86
+ - The inner dictionary contains a key "bindings" that maps to a list of dictionaries.
87
+ - Each dictionary in the list contains a key "cellarURIs" that maps to a dictionary.
88
+ - The innermost dictionary contains a key "value" that maps to a string representing the CELLAR URI.
89
+
90
+ The function extracts the CELLAR id by splitting the CELLAR URI at "cellar/" and taking the second part.
91
+
92
+ Examples
93
+ --------
94
+ >>> cellar_results = {
95
+ ... "results": {
96
+ ... "bindings": [
97
+ ... {"cellarURIs": {"value": "https://example.com/cellar/some_id"}},
98
+ ... {"cellarURIs": {"value": "https://example.com/cellar/another_id"}}
99
+ ... ]
100
+ ... }
101
+ ... }
102
+ >>> cellar_ids = get_cellar_ids_from_json_results(cellar_results)
103
+ >>> print(cellar_ids)
104
+ ['some_id', 'another_id']
105
+ """
106
+ cellar_ids = []
107
+ results_list = results["results"]["bindings"]
108
+ for i, file in enumerate(results_list):
109
+ if file['format']['value'] == format:
110
+ cellar_ids.append(file['cellarURIs']["value"].split("cellar/")[1])
111
+
112
+ return cellar_ids
113
+
114
+ def download(self, results, format=None):
115
+ """
116
+ Sends a REST query to the specified source APIs and downloads the documents
117
+ corresponding to the given results.
118
+
119
+ Parameters
120
+ ----------
121
+ results : dict
122
+ A dictionary containing the JSON results from the APIs.
123
+ format : str, optional
124
+ The format of the documents to download.
125
+
126
+ Returns
127
+ -------
128
+ list
129
+ A list of paths to the downloaded documents.
130
+ """
131
+
132
+ cellar_ids = self.get_cellar_ids_from_json_results(results, format=format)
133
+
134
+ try:
135
+ document_paths = []
136
+
137
+ for id in cellar_ids:
138
+ # Build the request URL
139
+ url = self.build_request_url(params={'cellar': id})
140
+
141
+ # Send the GET request
142
+ response = self.fetch_content(url)
143
+ # Handle the response
144
+ file_path = self.handle_response(response=response, filename=id)
145
+ # Append the file path to the list
146
+ document_paths.append(file_path)
147
+
148
+ return document_paths
149
+
150
+ except Exception as e:
151
+ logging.error(f"Error processing range: {e}")
152
+
153
+ return document_paths
154
+
155
+
156
+ # Example usage
157
+ if __name__ == "__main__":
158
+ downloader = CellarDownloader(download_dir='./tests/data/formex', log_dir='./tests/logs')
159
+ with open('./tests/metadata/query_results/query_results.json', 'r') as f:
160
+ results = json.loads(f.read())
161
+ documents = downloader.download(results, format='fmx4')
162
+ print(documents)
163
+
@@ -0,0 +1,85 @@
1
+ import os
2
+ import io
3
+ import logging
4
+ import zipfile
5
+ import requests
6
+
7
+ class DocumentDownloader:
8
+ """
9
+ A generic document downloader class.
10
+ """
11
+ def __init__(self, download_dir, log_dir):
12
+ self.download_dir = download_dir
13
+ self.log_dir = log_dir
14
+ self._ensure_directories()
15
+
16
+ def _ensure_directories(self):
17
+ if not os.path.exists(self.download_dir):
18
+ os.makedirs(self.download_dir)
19
+ if not os.path.exists(self.log_dir):
20
+ os.makedirs(self.log_dir)
21
+
22
+ def handle_response(self, response, filename):
23
+ """
24
+ Handle a server response by saving or extracting its content.
25
+
26
+ Parameters
27
+ ----------
28
+ response : requests.Response
29
+ The HTTP response object.
30
+ folder_path : str
31
+ Directory where the file will be saved.
32
+ cid : str
33
+ CELLAR ID of the document.
34
+
35
+ Returns
36
+ -------
37
+ str or None
38
+ Path to the saved file or None if the response couldn't be processed.
39
+ """
40
+ content_type = response.headers.get('Content-Type', '')
41
+
42
+ # The return file is usually either a zip file, or a file with the name DOC_* inside a folder named as the cellar_id
43
+ target_path = os.path.join(self.download_dir, filename)
44
+ os.makedirs(os.path.dirname(target_path), exist_ok=True)
45
+
46
+ if 'zip' in content_type:
47
+ self.extract_zip(response, target_path)
48
+ return target_path
49
+ else:
50
+ extension = self.get_extension_from_content_type(content_type)
51
+ if not extension:
52
+ logging.warning(f"Unknown content type for ID {filename}: {content_type}")
53
+ return None
54
+
55
+ file_path = f"{target_path}.{extension}"
56
+ file_path = os.path.normpath(file_path)
57
+
58
+ with open(file_path, mode='wb+') as f:
59
+ f.write(response.content)
60
+
61
+ return file_path
62
+
63
+ def get_extension_from_content_type(self, content_type):
64
+ """Map Content-Type to a file extension."""
65
+ content_type_mapping = {
66
+ 'text/html': 'html',
67
+ 'application/json': 'json',
68
+ 'application/xml': 'xml',
69
+ 'text/plain': 'txt',
70
+ 'application/zip': 'zip',
71
+ 'text/xml': 'xml',
72
+ 'application/xhtml+xml': 'xhtml',
73
+ }
74
+ for ext, mapped_ext in content_type_mapping.items():
75
+ if ext in content_type:
76
+ return mapped_ext
77
+
78
+ # Function to download a zip file and extract it
79
+ def extract_zip(self, response: requests.Response, folder_path: str):
80
+ try:
81
+ z = zipfile.ZipFile(io.BytesIO(response.content))
82
+ z.extractall(folder_path)
83
+ except Exception as e:
84
+ logging.error(f"Error downloading zip: {e}")
85
+
@@ -0,0 +1,39 @@
1
+ import requests
2
+ from tulit.download.download import DocumentDownloader
3
+
4
+ class LegiluxDownloader(DocumentDownloader):
5
+ def __init__(self, download_dir, log_dir):
6
+ super().__init__(download_dir, log_dir)
7
+ #self.endpoint = "https://legilux.public.lu/eli/etat/leg/loi"
8
+
9
+ def build_request_url(self, eli) -> str:
10
+ """
11
+ Build the request URL based on the source and parameters.
12
+ """
13
+ url = eli
14
+ return url
15
+
16
+ def fetch_content(self, url):
17
+ """
18
+ Fetch the content of the document.
19
+ """
20
+ headers = {"Accept": "application/xml"}
21
+ response = requests.get(url, headers=headers)
22
+ return response
23
+
24
+ def download(self, eli):
25
+ file_paths = []
26
+ url = self.build_request_url(eli)
27
+ response = self.fetch_content(url)
28
+ filename = eli.split('loi/')[1].replace('/', '_')
29
+ if response.status_code == 200:
30
+ file_paths.append(self.handle_response(response, filename=filename))
31
+ print(f"Document downloaded successfully and saved to {file_paths}")
32
+ return file_paths
33
+ else:
34
+ print(f"Failed to download document. Status code: {response.status_code}")
35
+ return None
36
+
37
+ if __name__ == "__main__":
38
+ downloader = LegiluxDownloader(download_dir='./tests/data/legilux', log_dir='./tests/metadata/logs')
39
+ downloader.download(eli='http://data.legilux.public.lu/eli/etat/leg/loi/2006/07/31/n2/jo')
@@ -0,0 +1,97 @@
1
+ from tulit.download.download import DocumentDownloader
2
+ import requests
3
+ import logging
4
+ from datetime import datetime
5
+
6
+
7
+ class NormattivaDownloader(DocumentDownloader):
8
+ def __init__(self, download_dir, log_dir):
9
+ super().__init__(download_dir, log_dir)
10
+ self.endpoint = "https://www.normattiva.it/do/atto/caricaAKN"
11
+
12
+ def build_request_url(self, params=None) -> str:
13
+ """
14
+ Build the request URL based on the source and parameters.
15
+ """
16
+ uri = f"https://www.normattiva.it/eli/id/{params['date']}//{params['codiceRedaz']}/CONSOLIDATED"
17
+ # In case we want to use the NIR:URI instead of ELI
18
+ #uri = f"https://www.normattiva.it/uri-res/N2Ls?urn:nir:stato:decreto.legislativo:{params['date']};{params['number']}"
19
+ url = f"{self.endpoint}?dataGU={params['dataGU']}&codiceRedaz={params['codiceRedaz']}&dataVigenza={params['dataVigenza']}"
20
+
21
+ return uri, url
22
+
23
+ def fetch_content(self, uri, url) -> requests.Response:
24
+ """
25
+ Send a GET request to download a file
26
+
27
+ Parameters
28
+ ----------
29
+ url : str
30
+ The URL to send the request to.
31
+
32
+ Returns
33
+ -------
34
+ requests.Response
35
+ The response from the server.
36
+
37
+ Raises
38
+ ------
39
+ requests.RequestException
40
+ If there is an error sending the request.
41
+ """
42
+ try:
43
+
44
+ # Make a GET request to the URI to get the cookies
45
+ cookies_response = requests.get(uri)
46
+ cookies_response.raise_for_status()
47
+ cookies = cookies_response.cookies
48
+
49
+ headers = {
50
+ 'Accept': "text/xml",
51
+ 'Accept-Encoding': "gzip, deflate, br, zstd",
52
+ 'Accept-Language': "en-US,en;q=0.9",
53
+
54
+ }
55
+ response = requests.get(url, headers=headers, cookies=cookies)
56
+ response.raise_for_status()
57
+ return response
58
+ except requests.RequestException as e:
59
+ logging.error(f"Error sending GET request: {e}")
60
+ return None
61
+
62
+ def download(self, dataGU, codiceRedaz, dataVigenza = datetime.today().strftime('%Y%m%d')):
63
+ document_paths = []
64
+
65
+ # Convert the dataGU to a datetime object
66
+ dataGU = datetime.strptime(dataGU, '%Y%m%d')
67
+
68
+ params = {
69
+ # dataGU as a string in the format YYYYMMDD
70
+ 'dataGU': dataGU.strftime('%Y%m%d'),
71
+ 'codiceRedaz': codiceRedaz,
72
+ 'dataVigenza': dataVigenza,
73
+ # dataGU as a string in the format YYYY/MM/DD
74
+ 'date': dataGU.strftime('%Y/%m/%d')
75
+ }
76
+
77
+ uri, url = self.build_request_url(params)
78
+ response = self.fetch_content(uri, url)
79
+
80
+ # If the response in HTML, raise an error saying that the date or codiceRedaz is wrong
81
+ if 'text/html' in response.headers.get('Content-Type', ''):
82
+ logging.error(f"Error downloading document: there is not an XML file with the following parameters: {params}")
83
+ return None
84
+
85
+ file_path = self.handle_response(response=response, filename=f"{params['dataGU']}_{params['codiceRedaz']}_VIGENZA_{params['dataVigenza']}")
86
+ document_paths.append(file_path)
87
+ return document_paths
88
+
89
+ # Example usage
90
+ if __name__ == "__main__":
91
+
92
+ downloader = NormattivaDownloader(download_dir='./tests/data/akn/italy', log_dir='./tests/logs')
93
+ #documents = downloader.download(dataGU='19410716', codiceRedaz='041U0633')
94
+ documents = downloader.download(dataGU='19410716', codiceRedaz='041U0633', dataVigenza='20211231')
95
+
96
+
97
+ print(documents)
tulit/main.py ADDED
@@ -0,0 +1,64 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ from tulit.download.download import download_documents
5
+ from sparql import send_sparql_query
6
+ from parsers.html import HTMLParser
7
+ from parsers.formex import Formex4Parser
8
+
9
+ def main():
10
+ """
11
+ Main function to execute SPARQL query and download documents
12
+ """
13
+ # Configure logging
14
+ logging.basicConfig(level=logging.INFO)
15
+ logger = logging.getLogger(__name__)
16
+
17
+ try:
18
+
19
+ # Send SPARQL query
20
+ logger.info("Executing SPARQL query")
21
+ results = send_sparql_query('./tests/metadata/queries/formex_query.rq', celex='32008R1137')
22
+
23
+
24
+ # Save query results to JSON
25
+ results_file = './tests/metadata/query_results/query_results.json'
26
+ with open(results_file, "w") as f:
27
+ json.dump(results, f, indent=4)
28
+ logger.info(f"Results dumped in {results_file}")
29
+
30
+ # Load query results
31
+ with open('./tests/metadata/query_results/query_results.json', 'r') as f:
32
+ results = json.loads(f.read())
33
+
34
+ # Download documents
35
+ logger.info("Downloading documents")
36
+ downloaded_document_paths = download_documents(
37
+ results,
38
+ './tests/data/formex',
39
+ log_dir='./tests/logs',
40
+ format='fmx4'
41
+ )
42
+ logger.info(f'{len(downloaded_document_paths)} documents downloaded in {downloaded_document_paths}')
43
+
44
+ # Extract the directory path (removing what's after the last '/')
45
+
46
+ # List the contents of the first directory
47
+ first_path = downloaded_document_paths[0]
48
+ first_item = os.listdir(first_path)[0]
49
+ file_path = os.path.join(*first_path.split('/'), first_item)
50
+
51
+ print(f'Parsing {file_path}')
52
+ # Sort the contents alphabetically and get the first item
53
+
54
+ parser = Formex4Parser()
55
+ parser.parse(file_path)
56
+ print(parser.articles)
57
+ #print(document_tree)
58
+
59
+ except Exception as e:
60
+ logger.error(f"An error occurred: {e}")
61
+ raise
62
+
63
+ if __name__ == "__main__":
64
+ main()
@@ -0,0 +1,3 @@
1
+ """
2
+ This subpackage provides classes and functions to parse a given file and extract the text within it.
3
+ """