pdnd-python-client 0.1.0__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Istituto Superiore per la Protezione e la Ricerca Ambientale (ISPRA)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,240 @@
1
+ Metadata-Version: 2.4
2
+ Name: pdnd-python-client
3
+ Version: 0.1.0
4
+ Summary: Client Python per autenticazione e interazione con le API della Piattaforma Digitale Nazionale Dati (PDND).
5
+ Author-email: Francesco Loreti <francesco.loreti@isprambiente.it>
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: PyJWT>=2.0.0
11
+ Requires-Dist: cryptography
12
+ Requires-Dist: requests
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest; extra == "dev"
15
+ Requires-Dist: pytest-watch; extra == "dev"
16
+ Requires-Dist: requests-mock; extra == "dev"
17
+ Dynamic: license-file
18
+
19
+ # pdnd-python-client
20
+
21
+ Client Python per autenticazione e interazione con le API della Piattaforma Digitale Nazionale Dati (PDND).
22
+
23
+ ## Licenza
24
+
25
+ MIT
26
+
27
+ ## Requisiti
28
+
29
+ - Python >= 3.10 (versioni precedenti sono [EOL](https://endoflife.date/python))
30
+ - PIP
31
+
32
+ ## Installazione
33
+
34
+ 1. Installa la libreria via composer:
35
+ ```bash
36
+ pip install pdnd-python-client
37
+ ```
38
+
39
+ 2. Configura il file JSON con i parametri richiesti (esempio in `configs/sample.json`):
40
+ ```json
41
+ {
42
+ "collaudo": {
43
+ "kid": "kid",
44
+ "issuer": "issuer",
45
+ "clientId": "clientId",
46
+ "purposeId": "purposeId",
47
+ "privKeyPath": "/tmp/key.priv"
48
+ },
49
+ "produzione": {
50
+ "kid": "kid",
51
+ "issuer": "issuer",
52
+ "clientId": "clientId",
53
+ "purposeId": "purposeId",
54
+ "privKeyPath": "/tmp/key.priv"
55
+ }
56
+ }
57
+ ```
58
+ ## Istruzioni
59
+
60
+ ```python
61
+
62
+ from pdnd_client.config import Config
63
+ from pdnd_client.jwt_generator import JWTGenerator
64
+ from pdnd_client.client import PDNDClient
65
+
66
+ # Inizializza la configurazione
67
+ # Load the configuration from the specified JSON file and environment key.
68
+ config = Config(args.config, args.env)
69
+
70
+ # Initialize the PDND client with the generated JWT token and SSL verification settings.
71
+ client = PDNDClient()
72
+ client.set_token_file(f"tmp/pdnd_token_{config.get("purposeId")}.json")
73
+ token, exp = client.load_token()
74
+
75
+ if client.is_token_valid(exp):
76
+ # Se il token è valido, lo carica da file
77
+ token, exp = client.load_token()
78
+ else:
79
+ # Generate a JWT token using the loaded configuration.
80
+ jwt_gen = JWTGenerator(config)
81
+ jwt_gen.set_debug(args.debug)
82
+ jwt_gen.set_env(args.env)
83
+ # Se il token non è valido, ne richiede uno nuovo
84
+ token, exp = jwt_gen.request_token()
85
+ # Salva il token per usi futuri
86
+ client.save_token(token, exp)
87
+
88
+ client.set_token(token)
89
+ client.set_expiration(exp)
90
+ client.set_api_url("https://www.tuogateway.example.it/indirizzo/della/api")
91
+ client.set_filters(parse_filters("id=1234"))
92
+ status_code, response = client.get_api(token)
93
+
94
+ # Stampa il risultato
95
+ print(response)
96
+
97
+ ```
98
+
99
+ ### Funzionalità aggiuntive
100
+
101
+ **Disabilita verifica certificato SSL**
102
+
103
+ La funzione `client.set_verify_ssl(False)` Disabilita verifica SSL per ambiente impostato (es. collaudo).
104
+ Default: true
105
+
106
+ **Salva il token**
107
+
108
+ La funzione `client.save_token(token, exp)` consente di memorizzare il token e la scadenza e non doverlo richiedere a ogni chiamata.
109
+
110
+ **Carica il token salvato**
111
+
112
+ La funzione `client.load_token()` consente di richiamare il token precedentemente salvato.
113
+
114
+ **Valida il token salvato**
115
+
116
+ La funzione `client.is_token_valid()` verifica la validità del token salvato.
117
+
118
+ ## Utilizzo da CLI
119
+
120
+ Esegui il client dalla cartella principale:
121
+
122
+ ```python
123
+ python main.py --api-url "https://api.pdnd.example.it/resource" --config /configs/progetto.json
124
+ ```
125
+
126
+ ### Opzioni disponibili
127
+
128
+ - `--env` : Specifica l'ambiente da usare (es. collaudo, produzione). Default: `produzione`
129
+ - `--config` : Specifica il percorso completo del file di configurazione (es: `--config /configs/progetto.json`)
130
+ - `--debug` : Abilita output dettagliato
131
+ - `--api-url` : URL dell’API da chiamare dopo la generazione del token
132
+ - `--api-url-filters` : Filtri da applicare all'API (es. ?parametro=valore)
133
+ - `--status-url` : URL dell’API di status per verificare la validità del token
134
+ - `--json`: Stampa le risposte delle API in formato JSON
135
+ - `--save`: Salva il token per evitare di richiederlo a ogni chiamata
136
+ - `--no-verify-ssl`: Disabilita la verifica SSL (utile per ambienti di collaudo)
137
+ - `--help`: Mostra questa schermata di aiuto
138
+
139
+ ### Esempi
140
+
141
+ **Chiamata API generica:**
142
+ ```bash
143
+ python main.py --api-url="https://api.pdnd.example.it/resource" --config /configs/progetto.json
144
+ ```
145
+
146
+ **Verifica validità token:**
147
+ ```bash
148
+ python main.py --status-url="https://api.pdnd.example.it/status" --config /configs/progetto.json
149
+ ```
150
+
151
+ **Debug attivo:**
152
+ ```bash
153
+ python main.py --debug --api-url="https://api.pdnd.example.it/resource"
154
+ ```
155
+
156
+ ### Opzione di aiuto
157
+
158
+ Se esegui il comando con `--help` oppure senza parametri, viene mostrata una descrizione delle opzioni disponibili e alcuni esempi di utilizzo:
159
+
160
+ ```bash
161
+ python main.py --help
162
+ ```
163
+
164
+ **Output di esempio:**
165
+ ```
166
+ Utilizzo:
167
+ python main.py -c /percorso/config.json [opzioni]
168
+
169
+ Opzioni:
170
+ --env Specifica l'ambiente da usare (es. collaudo, produzione)
171
+ Default: produzione
172
+ --config Specifica il percorso completo del file di configurazione
173
+ --debug Abilita output dettagliato
174
+ --api-url URL dell’API da chiamare dopo la generazione del token
175
+ --api-url-filters Filtri da applicare all'API (es. ?parametro=valore)
176
+ --status-url URL dell’API di status per verificare la validità del token
177
+ --json Stampa le risposte delle API in formato JSON
178
+ --save Salva il token per evitare di richiederlo a ogni chiamata
179
+ --no-verify-ssl Disabilita la verifica SSL (utile per ambienti di collaudo)
180
+ --help Mostra questa schermata di aiuto
181
+
182
+ Esempi:
183
+ python main.py --api-url="https://api.pdnd.example.it/resource" --config /percorso/config.json
184
+ python main.py --status-url="https://api.pdnd.example.it/status" --config /percorso/config.json
185
+ python main.py --debug --api-url="https://api.pdnd.example.it/resource"
186
+ ```
187
+
188
+ ## Variabili di ambiente supportate
189
+
190
+ Se un parametro non è presente nel file di configurazione, puoi definirlo come variabile di ambiente:
191
+
192
+ - `PDND_KID`
193
+ - `PDND_ISSUER`
194
+ - `PDND_CLIENT_ID`
195
+ - `PDND_PURPOSE_ID`
196
+ - `PDND_PRIVKEY_PATH`
197
+
198
+ ## Note
199
+
200
+ - Il token viene salvato in un file temporaneo e riutilizzato finché è valido.
201
+ - Gli errori specifici vengono gestiti tramite la classe `PdndException`.
202
+
203
+ ## Esempio di configurazione minima
204
+
205
+ ```json
206
+ {
207
+ "produzione": {
208
+ "kid": "kid",
209
+ "issuer": "issuer",
210
+ "clientId": "clientId",
211
+ "purposeId": "purposeId",
212
+ "privKeyPath": "/tmp/key.pem"
213
+ }
214
+ }
215
+ ```
216
+ ## Esempio di configurazione per collaudo e prosuzione
217
+
218
+ ```json
219
+ {
220
+ "collaudo": {
221
+ "kid": "kid",
222
+ "issuer": "issuer",
223
+ "clientId": "clientId",
224
+ "purposeId": "purposeId",
225
+ "privKeyPath": "/tmp/key.pem"
226
+ },
227
+ "produzione": {
228
+ "kid": "kid",
229
+ "issuer": "issuer",
230
+ "clientId": "clientId",
231
+ "purposeId": "purposeId",
232
+ "privKeyPath": "/tmp/key.pem"
233
+ }
234
+ }
235
+ ```
236
+ ---
237
+
238
+ ## Contribuire
239
+
240
+ Le pull request sono benvenute! Per problemi o suggerimenti, apri una issue.
@@ -0,0 +1,222 @@
1
+ # pdnd-python-client
2
+
3
+ Client Python per autenticazione e interazione con le API della Piattaforma Digitale Nazionale Dati (PDND).
4
+
5
+ ## Licenza
6
+
7
+ MIT
8
+
9
+ ## Requisiti
10
+
11
+ - Python >= 3.10 (versioni precedenti sono [EOL](https://endoflife.date/python))
12
+ - PIP
13
+
14
+ ## Installazione
15
+
16
+ 1. Installa la libreria via composer:
17
+ ```bash
18
+ pip install pdnd-python-client
19
+ ```
20
+
21
+ 2. Configura il file JSON con i parametri richiesti (esempio in `configs/sample.json`):
22
+ ```json
23
+ {
24
+ "collaudo": {
25
+ "kid": "kid",
26
+ "issuer": "issuer",
27
+ "clientId": "clientId",
28
+ "purposeId": "purposeId",
29
+ "privKeyPath": "/tmp/key.priv"
30
+ },
31
+ "produzione": {
32
+ "kid": "kid",
33
+ "issuer": "issuer",
34
+ "clientId": "clientId",
35
+ "purposeId": "purposeId",
36
+ "privKeyPath": "/tmp/key.priv"
37
+ }
38
+ }
39
+ ```
40
+ ## Istruzioni
41
+
42
+ ```python
43
+
44
+ from pdnd_client.config import Config
45
+ from pdnd_client.jwt_generator import JWTGenerator
46
+ from pdnd_client.client import PDNDClient
47
+
48
+ # Inizializza la configurazione
49
+ # Load the configuration from the specified JSON file and environment key.
50
+ config = Config(args.config, args.env)
51
+
52
+ # Initialize the PDND client with the generated JWT token and SSL verification settings.
53
+ client = PDNDClient()
54
+ client.set_token_file(f"tmp/pdnd_token_{config.get("purposeId")}.json")
55
+ token, exp = client.load_token()
56
+
57
+ if client.is_token_valid(exp):
58
+ # Se il token è valido, lo carica da file
59
+ token, exp = client.load_token()
60
+ else:
61
+ # Generate a JWT token using the loaded configuration.
62
+ jwt_gen = JWTGenerator(config)
63
+ jwt_gen.set_debug(args.debug)
64
+ jwt_gen.set_env(args.env)
65
+ # Se il token non è valido, ne richiede uno nuovo
66
+ token, exp = jwt_gen.request_token()
67
+ # Salva il token per usi futuri
68
+ client.save_token(token, exp)
69
+
70
+ client.set_token(token)
71
+ client.set_expiration(exp)
72
+ client.set_api_url("https://www.tuogateway.example.it/indirizzo/della/api")
73
+ client.set_filters(parse_filters("id=1234"))
74
+ status_code, response = client.get_api(token)
75
+
76
+ # Stampa il risultato
77
+ print(response)
78
+
79
+ ```
80
+
81
+ ### Funzionalità aggiuntive
82
+
83
+ **Disabilita verifica certificato SSL**
84
+
85
+ La funzione `client.set_verify_ssl(False)` Disabilita verifica SSL per ambiente impostato (es. collaudo).
86
+ Default: true
87
+
88
+ **Salva il token**
89
+
90
+ La funzione `client.save_token(token, exp)` consente di memorizzare il token e la scadenza e non doverlo richiedere a ogni chiamata.
91
+
92
+ **Carica il token salvato**
93
+
94
+ La funzione `client.load_token()` consente di richiamare il token precedentemente salvato.
95
+
96
+ **Valida il token salvato**
97
+
98
+ La funzione `client.is_token_valid()` verifica la validità del token salvato.
99
+
100
+ ## Utilizzo da CLI
101
+
102
+ Esegui il client dalla cartella principale:
103
+
104
+ ```python
105
+ python main.py --api-url "https://api.pdnd.example.it/resource" --config /configs/progetto.json
106
+ ```
107
+
108
+ ### Opzioni disponibili
109
+
110
+ - `--env` : Specifica l'ambiente da usare (es. collaudo, produzione). Default: `produzione`
111
+ - `--config` : Specifica il percorso completo del file di configurazione (es: `--config /configs/progetto.json`)
112
+ - `--debug` : Abilita output dettagliato
113
+ - `--api-url` : URL dell’API da chiamare dopo la generazione del token
114
+ - `--api-url-filters` : Filtri da applicare all'API (es. ?parametro=valore)
115
+ - `--status-url` : URL dell’API di status per verificare la validità del token
116
+ - `--json`: Stampa le risposte delle API in formato JSON
117
+ - `--save`: Salva il token per evitare di richiederlo a ogni chiamata
118
+ - `--no-verify-ssl`: Disabilita la verifica SSL (utile per ambienti di collaudo)
119
+ - `--help`: Mostra questa schermata di aiuto
120
+
121
+ ### Esempi
122
+
123
+ **Chiamata API generica:**
124
+ ```bash
125
+ python main.py --api-url="https://api.pdnd.example.it/resource" --config /configs/progetto.json
126
+ ```
127
+
128
+ **Verifica validità token:**
129
+ ```bash
130
+ python main.py --status-url="https://api.pdnd.example.it/status" --config /configs/progetto.json
131
+ ```
132
+
133
+ **Debug attivo:**
134
+ ```bash
135
+ python main.py --debug --api-url="https://api.pdnd.example.it/resource"
136
+ ```
137
+
138
+ ### Opzione di aiuto
139
+
140
+ Se esegui il comando con `--help` oppure senza parametri, viene mostrata una descrizione delle opzioni disponibili e alcuni esempi di utilizzo:
141
+
142
+ ```bash
143
+ python main.py --help
144
+ ```
145
+
146
+ **Output di esempio:**
147
+ ```
148
+ Utilizzo:
149
+ python main.py -c /percorso/config.json [opzioni]
150
+
151
+ Opzioni:
152
+ --env Specifica l'ambiente da usare (es. collaudo, produzione)
153
+ Default: produzione
154
+ --config Specifica il percorso completo del file di configurazione
155
+ --debug Abilita output dettagliato
156
+ --api-url URL dell’API da chiamare dopo la generazione del token
157
+ --api-url-filters Filtri da applicare all'API (es. ?parametro=valore)
158
+ --status-url URL dell’API di status per verificare la validità del token
159
+ --json Stampa le risposte delle API in formato JSON
160
+ --save Salva il token per evitare di richiederlo a ogni chiamata
161
+ --no-verify-ssl Disabilita la verifica SSL (utile per ambienti di collaudo)
162
+ --help Mostra questa schermata di aiuto
163
+
164
+ Esempi:
165
+ python main.py --api-url="https://api.pdnd.example.it/resource" --config /percorso/config.json
166
+ python main.py --status-url="https://api.pdnd.example.it/status" --config /percorso/config.json
167
+ python main.py --debug --api-url="https://api.pdnd.example.it/resource"
168
+ ```
169
+
170
+ ## Variabili di ambiente supportate
171
+
172
+ Se un parametro non è presente nel file di configurazione, puoi definirlo come variabile di ambiente:
173
+
174
+ - `PDND_KID`
175
+ - `PDND_ISSUER`
176
+ - `PDND_CLIENT_ID`
177
+ - `PDND_PURPOSE_ID`
178
+ - `PDND_PRIVKEY_PATH`
179
+
180
+ ## Note
181
+
182
+ - Il token viene salvato in un file temporaneo e riutilizzato finché è valido.
183
+ - Gli errori specifici vengono gestiti tramite la classe `PdndException`.
184
+
185
+ ## Esempio di configurazione minima
186
+
187
+ ```json
188
+ {
189
+ "produzione": {
190
+ "kid": "kid",
191
+ "issuer": "issuer",
192
+ "clientId": "clientId",
193
+ "purposeId": "purposeId",
194
+ "privKeyPath": "/tmp/key.pem"
195
+ }
196
+ }
197
+ ```
198
+ ## Esempio di configurazione per collaudo e prosuzione
199
+
200
+ ```json
201
+ {
202
+ "collaudo": {
203
+ "kid": "kid",
204
+ "issuer": "issuer",
205
+ "clientId": "clientId",
206
+ "purposeId": "purposeId",
207
+ "privKeyPath": "/tmp/key.pem"
208
+ },
209
+ "produzione": {
210
+ "kid": "kid",
211
+ "issuer": "issuer",
212
+ "clientId": "clientId",
213
+ "purposeId": "purposeId",
214
+ "privKeyPath": "/tmp/key.pem"
215
+ }
216
+ }
217
+ ```
218
+ ---
219
+
220
+ ## Contribuire
221
+
222
+ Le pull request sono benvenute! Per problemi o suggerimenti, apri una issue.
File without changes
@@ -0,0 +1,147 @@
1
+ # La classe PDNDClient è responsabile dell'invio di richieste HTTP all'API PDND.
2
+ # Utilizza il JWT generato per l'autenticazione e può gestire sia richieste GET che POST.
3
+ # Lo script include anche opzioni per il debug e la verifica SSL,
4
+ # consentendo agli utenti di visualizzare output dettagliati e controllare la validazione del certificato SSL.
5
+ # La funzione parse_filters viene utilizzata per convertire una stringa di query in un dizionario,
6
+ # che può essere passato come parametro nelle richieste API.
7
+
8
+ import requests
9
+ import os
10
+ import json
11
+ import time
12
+ from datetime import datetime
13
+ from urllib.parse import urlencode
14
+
15
+ # La classe PDNDClient viene inizializzata con un token JWT e un'opzione per verificare i certificati SSL.
16
+ # Fornisce metodi per effettuare richieste GET e POST verso URL specificati.
17
+ class PDNDClient:
18
+ def __init__(self):
19
+ self.verify_ssl = True
20
+ self.api_url = None
21
+ self.status_url = None
22
+ self.filters = {}
23
+ self.debug = False
24
+ self.token = ""
25
+ self.token_file = "pdnd_token.json"
26
+ self.token_exp = None # Token expiration time, if applicable
27
+
28
+ # Questo metodo recupera l'URL dell'API, che può essere sovrascritto dall'utente.
29
+ def get_api_url(self):
30
+ return self.api_url if hasattr(self, 'api_url') else None
31
+
32
+ # Questo metodo imposta l'URL dell'API per le richieste successive.
33
+ def set_api_url(self, api_url):
34
+ self.api_url = api_url
35
+
36
+ # Questo metodo imposta i filtri da utilizzare nelle richieste API.
37
+ def set_filters(self, filters):
38
+ self.filters = filters
39
+
40
+ # Questo metodo imposta la modalità di debug, che controlla se stampare un output dettagliato.
41
+ def set_debug(self, debug):
42
+ self.debug = debug
43
+
44
+ # Questo metodo imposta il tempo di scadenza per il token.
45
+ # Può essere una stringa nel formato "YYYY-MM-DD HH:MM:SS" oppure un oggetto datetime.
46
+ # Se non viene fornito, il valore predefinito è None.
47
+ def set_expiration(self, exp):
48
+ self.token_exp = exp
49
+
50
+ # Questo metodo imposta l'URL di stato per le richieste GET.
51
+ def set_status_url(self, status_url):
52
+ self.status_url = status_url
53
+
54
+ def set_token(self, token):
55
+ self.token = token
56
+
57
+ def set_token_file(self, token_file):
58
+ self.token_file = token_file
59
+
60
+ def set_verify_ssl(self, verify_ssl):
61
+ self.verify_ssl = verify_ssl
62
+
63
+ def get_api(self, token: str):
64
+ url = self.api_url if hasattr(self, 'api_url') and self.api_url else self.get_api_url()
65
+
66
+ # Aggiunta dei filtri come query string
67
+ if hasattr(self, 'filters') and self.filters:
68
+ query = urlencode(self.filters, doseq=True)
69
+ separator = '&' if '?' in url else '?'
70
+ url += separator + query
71
+
72
+ headers = {
73
+ "Authorization": f"Bearer {token}",
74
+ "Accept": "*/*"
75
+ }
76
+
77
+ try:
78
+ response = requests.get(url, headers=headers, verify=self.verify_ssl)
79
+ except requests.exceptions.RequestException as e:
80
+ raise Exception(f"❌ Errore nella chiamata API: {e}")
81
+
82
+ status_code = response.status_code
83
+ body = response.text
84
+
85
+ if not response.ok:
86
+ raise Exception(f"❌ Errore nella chiamata API: {response.text}")
87
+
88
+ if self.debug:
89
+ try:
90
+ decoded = response.json()
91
+ body = json.dumps(decoded, indent=2, ensure_ascii=False)
92
+ except Exception:
93
+ pass # Se non è JSON, lascia il body così com'è
94
+
95
+ return status_code, body
96
+
97
+ # Questo metodo esegue una richiesta GET all'URL specificato e restituisce il codice di stato e il testo della risposta
98
+ def get_status(self, url):
99
+ headers = {"Authorization": f"Bearer {self.token}"}
100
+ response = requests.get(url, headers=headers, verify=self.verify_ssl)
101
+ return response.status_code, response.text
102
+
103
+ def get_token(self):
104
+ return self.token
105
+
106
+ def is_token_valid(self, exp) -> bool:
107
+ if not self.token_exp and not exp:
108
+ return False
109
+ exp = exp or self.token_exp
110
+ exp = datetime.strptime(exp, "%Y-%m-%d %H:%M:%S") if isinstance(exp, str) else exp
111
+ if not isinstance(exp, datetime):
112
+ raise ValueError("L'exp deve essere una stringa o un oggetto datetime")
113
+ return time.time() < exp.timestamp()
114
+
115
+ def load_token(self, file: str = None):
116
+ file = file or self.token_file # Usa il file passato o quello di default
117
+
118
+ if not os.path.exists(file):
119
+ return None
120
+
121
+ try:
122
+ with open(file, "r", encoding="utf-8") as f:
123
+ data = json.load(f)
124
+ except (json.JSONDecodeError, IOError):
125
+ return None
126
+
127
+ if not data or "token" not in data or "exp" not in data:
128
+ return None
129
+
130
+ self.token_exp = data["exp"]
131
+ return data["token"], data["exp"]
132
+
133
+
134
+ def save_token(self, token: str, exp: str, file: str = None):
135
+ file = file or self.token_file # Usa il file passato o quello di default
136
+ exp = exp or self.token_exp # Usa l'exp passato o quello corrente
137
+ data = {
138
+ "token": token,
139
+ "exp": exp
140
+ }
141
+ with open(file, "w", encoding="utf-8") as f:
142
+ json.dump(data, f, ensure_ascii=False)
143
+
144
+
145
+
146
+
147
+
@@ -0,0 +1,21 @@
1
+ # pdnd_client/config.py
2
+
3
+ import json
4
+ import os
5
+
6
+ # La classe Config viene inizializzata con un percorso verso un file di configurazione e una chiave di ambiente.
7
+ # Legge la configurazione dal file e la memorizza in un dizionario.
8
+ # Il metodo get consente di recuperare valori specifici della configurazione, con un valore predefinito opzionale.
9
+ class Config:
10
+ # Questo metodo inizializza l'oggetto Config caricando la configurazione da un file JSON.
11
+ def __init__(self, config_path, env):
12
+ self.env = env
13
+ with open(config_path, "r") as f:
14
+ full_config = json.load(f)
15
+ if env not in full_config:
16
+ raise ValueError(f"Environment '{env}' not found in config file.")
17
+ self.config = full_config[env]
18
+
19
+ # Questo metodo recupera un valore di configurazione tramite una chiave, restituendo un valore predefinito se la chiave non viene trovata.
20
+ def get(self, key, default=None):
21
+ return self.config.get(key, default)
@@ -0,0 +1,124 @@
1
+ # pdnd_client/jwt_generator.py
2
+
3
+ import time
4
+ import json
5
+ import base64
6
+ import requests
7
+ import jwt # PyJWT
8
+ import secrets
9
+ from datetime import datetime, timezone
10
+ from jwt import exceptions as jwt_exceptions
11
+
12
+ # Questa classe è responsabile della generazione di un token JWT basato sulla configurazione fornita.
13
+ # Utilizza la libreria PyJWT per creare e firmare il token con una chiave privata.
14
+ # Il token include claim come issuer, subject, audience e tempo di scadenza.
15
+ # La classe legge la chiave privata da un file specificato nella configurazione,
16
+ # e utilizza l'algoritmo RS256 per firmare il token.
17
+ # Il token generato può essere utilizzato per autenticare le richieste API al servizio PDND.
18
+ class JWTGenerator:
19
+ def __init__(self, config):
20
+ self.config = config
21
+ self.debug = config.get("debug", False)
22
+ self.client_id = config.get("clientId")
23
+ self.endpoint = config.get("endpoint")
24
+ self.env = config.get("env", "produzione")
25
+ self.token_exp = None
26
+ self.endpoint = "https://auth.interop.pagopa.it/token.oauth2"
27
+ self.aud = "auth.interop.pagopa.it/client-assertion"
28
+
29
+ def set_debug(self, debug):
30
+ self.debug = debug
31
+
32
+
33
+ def set_env(self, env):
34
+ self.env = env
35
+ if self.env == "collaudo":
36
+ self.endpoint = "https://auth.uat.interop.pagopa.it/token.oauth2"
37
+ self.aud = "auth.uat.interop.pagopa.it/client-assertion"
38
+
39
+ def request_token(self):
40
+
41
+ with open(self.config.get("privKeyPath"), "rb") as key_file:
42
+ private_key = key_file.read()
43
+
44
+ issued_at = int(time.time())
45
+ expiration_time = issued_at + (43200 * 60) # 30 giorni
46
+ jti = secrets.token_hex(16)
47
+
48
+ payload = {
49
+ "iss": self.config.get("issuer"),
50
+ "sub": self.config.get("clientId"),
51
+ "aud": self.aud,
52
+ "purposeId": self.config.get("purposeId"),
53
+ "jti": jti,
54
+ "iat": issued_at,
55
+ "exp": expiration_time
56
+ }
57
+
58
+ headers = {
59
+ "kid": self.config.get("kid"),
60
+ "alg": "RS256",
61
+ "typ": "JWT"
62
+ }
63
+
64
+ try:
65
+ client_assertion = jwt.encode(
66
+ payload,
67
+ private_key,
68
+ algorithm="RS256",
69
+ headers=headers
70
+ )
71
+ except jwt_exceptions.PyJWTError as e:
72
+ raise Exception(f"❌ Errore durante la generazione del client_assertion JWT:\n{str(e)}")
73
+
74
+ if self.debug:
75
+ print(f"\n✅ Enviroment: {self.env}")
76
+ print("\n✅ Client assertion generato con successo.")
77
+ print(f"\n📄 JWT (client_assertion):\n{client_assertion}")
78
+
79
+ data = {
80
+ "client_id": self.client_id,
81
+ "client_assertion": client_assertion,
82
+ "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
83
+ "grant_type": "client_credentials"
84
+ }
85
+
86
+ headers = {
87
+ "Content-Type": "application/x-www-form-urlencoded"
88
+ }
89
+
90
+ try:
91
+ response = requests.post(self.endpoint, data=data, headers=headers)
92
+ response.raise_for_status() # Solleva eccezione per codici HTTP 4xx/5xx
93
+ except requests.exceptions.RequestException as e:
94
+ print(f"❌ Errore nella richiesta POST: {e}")
95
+ return False
96
+
97
+ if response.status_code == 200:
98
+ json_response = response.json()
99
+ access_token = json_response.get("access_token")
100
+
101
+ if access_token:
102
+ try:
103
+ payload_part = access_token.split('.')[1]
104
+ padded = payload_part + '=' * (-len(payload_part) % 4)
105
+ decoded_payload = json.loads(base64.urlsafe_b64decode(padded))
106
+ self.token_exp = decoded_payload.get("exp")
107
+ except Exception:
108
+ self.token_exp = None
109
+
110
+ if self.debug:
111
+ if self.token_exp:
112
+ dt = datetime.fromtimestamp(self.token_exp, tz=timezone.utc)
113
+ token_exp_str = dt.astimezone().strftime('%Y-%m-%d %H:%M:%S')
114
+ else:
115
+ token_exp_str = 'non disponibile'
116
+
117
+ print(f"\n🔐 Access Token:\n{access_token}")
118
+ print(f"\n⏰ Scadenza token (exp): {token_exp_str}")
119
+
120
+ return access_token, token_exp_str
121
+ else:
122
+ raise Exception(f"⚠️ Nessun access token trovato:\n{json.dumps(json_response, indent=2)}")
123
+
124
+ return access_token
@@ -0,0 +1,240 @@
1
+ Metadata-Version: 2.4
2
+ Name: pdnd-python-client
3
+ Version: 0.1.0
4
+ Summary: Client Python per autenticazione e interazione con le API della Piattaforma Digitale Nazionale Dati (PDND).
5
+ Author-email: Francesco Loreti <francesco.loreti@isprambiente.it>
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: PyJWT>=2.0.0
11
+ Requires-Dist: cryptography
12
+ Requires-Dist: requests
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest; extra == "dev"
15
+ Requires-Dist: pytest-watch; extra == "dev"
16
+ Requires-Dist: requests-mock; extra == "dev"
17
+ Dynamic: license-file
18
+
19
+ # pdnd-python-client
20
+
21
+ Client Python per autenticazione e interazione con le API della Piattaforma Digitale Nazionale Dati (PDND).
22
+
23
+ ## Licenza
24
+
25
+ MIT
26
+
27
+ ## Requisiti
28
+
29
+ - Python >= 3.10 (versioni precedenti sono [EOL](https://endoflife.date/python))
30
+ - PIP
31
+
32
+ ## Installazione
33
+
34
+ 1. Installa la libreria via composer:
35
+ ```bash
36
+ pip install pdnd-python-client
37
+ ```
38
+
39
+ 2. Configura il file JSON con i parametri richiesti (esempio in `configs/sample.json`):
40
+ ```json
41
+ {
42
+ "collaudo": {
43
+ "kid": "kid",
44
+ "issuer": "issuer",
45
+ "clientId": "clientId",
46
+ "purposeId": "purposeId",
47
+ "privKeyPath": "/tmp/key.priv"
48
+ },
49
+ "produzione": {
50
+ "kid": "kid",
51
+ "issuer": "issuer",
52
+ "clientId": "clientId",
53
+ "purposeId": "purposeId",
54
+ "privKeyPath": "/tmp/key.priv"
55
+ }
56
+ }
57
+ ```
58
+ ## Istruzioni
59
+
60
+ ```python
61
+
62
+ from pdnd_client.config import Config
63
+ from pdnd_client.jwt_generator import JWTGenerator
64
+ from pdnd_client.client import PDNDClient
65
+
66
+ # Inizializza la configurazione
67
+ # Load the configuration from the specified JSON file and environment key.
68
+ config = Config(args.config, args.env)
69
+
70
+ # Initialize the PDND client with the generated JWT token and SSL verification settings.
71
+ client = PDNDClient()
72
+ client.set_token_file(f"tmp/pdnd_token_{config.get("purposeId")}.json")
73
+ token, exp = client.load_token()
74
+
75
+ if client.is_token_valid(exp):
76
+ # Se il token è valido, lo carica da file
77
+ token, exp = client.load_token()
78
+ else:
79
+ # Generate a JWT token using the loaded configuration.
80
+ jwt_gen = JWTGenerator(config)
81
+ jwt_gen.set_debug(args.debug)
82
+ jwt_gen.set_env(args.env)
83
+ # Se il token non è valido, ne richiede uno nuovo
84
+ token, exp = jwt_gen.request_token()
85
+ # Salva il token per usi futuri
86
+ client.save_token(token, exp)
87
+
88
+ client.set_token(token)
89
+ client.set_expiration(exp)
90
+ client.set_api_url("https://www.tuogateway.example.it/indirizzo/della/api")
91
+ client.set_filters(parse_filters("id=1234"))
92
+ status_code, response = client.get_api(token)
93
+
94
+ # Stampa il risultato
95
+ print(response)
96
+
97
+ ```
98
+
99
+ ### Funzionalità aggiuntive
100
+
101
+ **Disabilita verifica certificato SSL**
102
+
103
+ La funzione `client.set_verify_ssl(False)` Disabilita verifica SSL per ambiente impostato (es. collaudo).
104
+ Default: true
105
+
106
+ **Salva il token**
107
+
108
+ La funzione `client.save_token(token, exp)` consente di memorizzare il token e la scadenza e non doverlo richiedere a ogni chiamata.
109
+
110
+ **Carica il token salvato**
111
+
112
+ La funzione `client.load_token()` consente di richiamare il token precedentemente salvato.
113
+
114
+ **Valida il token salvato**
115
+
116
+ La funzione `client.is_token_valid()` verifica la validità del token salvato.
117
+
118
+ ## Utilizzo da CLI
119
+
120
+ Esegui il client dalla cartella principale:
121
+
122
+ ```python
123
+ python main.py --api-url "https://api.pdnd.example.it/resource" --config /configs/progetto.json
124
+ ```
125
+
126
+ ### Opzioni disponibili
127
+
128
+ - `--env` : Specifica l'ambiente da usare (es. collaudo, produzione). Default: `produzione`
129
+ - `--config` : Specifica il percorso completo del file di configurazione (es: `--config /configs/progetto.json`)
130
+ - `--debug` : Abilita output dettagliato
131
+ - `--api-url` : URL dell’API da chiamare dopo la generazione del token
132
+ - `--api-url-filters` : Filtri da applicare all'API (es. ?parametro=valore)
133
+ - `--status-url` : URL dell’API di status per verificare la validità del token
134
+ - `--json`: Stampa le risposte delle API in formato JSON
135
+ - `--save`: Salva il token per evitare di richiederlo a ogni chiamata
136
+ - `--no-verify-ssl`: Disabilita la verifica SSL (utile per ambienti di collaudo)
137
+ - `--help`: Mostra questa schermata di aiuto
138
+
139
+ ### Esempi
140
+
141
+ **Chiamata API generica:**
142
+ ```bash
143
+ python main.py --api-url="https://api.pdnd.example.it/resource" --config /configs/progetto.json
144
+ ```
145
+
146
+ **Verifica validità token:**
147
+ ```bash
148
+ python main.py --status-url="https://api.pdnd.example.it/status" --config /configs/progetto.json
149
+ ```
150
+
151
+ **Debug attivo:**
152
+ ```bash
153
+ python main.py --debug --api-url="https://api.pdnd.example.it/resource"
154
+ ```
155
+
156
+ ### Opzione di aiuto
157
+
158
+ Se esegui il comando con `--help` oppure senza parametri, viene mostrata una descrizione delle opzioni disponibili e alcuni esempi di utilizzo:
159
+
160
+ ```bash
161
+ python main.py --help
162
+ ```
163
+
164
+ **Output di esempio:**
165
+ ```
166
+ Utilizzo:
167
+ python main.py -c /percorso/config.json [opzioni]
168
+
169
+ Opzioni:
170
+ --env Specifica l'ambiente da usare (es. collaudo, produzione)
171
+ Default: produzione
172
+ --config Specifica il percorso completo del file di configurazione
173
+ --debug Abilita output dettagliato
174
+ --api-url URL dell’API da chiamare dopo la generazione del token
175
+ --api-url-filters Filtri da applicare all'API (es. ?parametro=valore)
176
+ --status-url URL dell’API di status per verificare la validità del token
177
+ --json Stampa le risposte delle API in formato JSON
178
+ --save Salva il token per evitare di richiederlo a ogni chiamata
179
+ --no-verify-ssl Disabilita la verifica SSL (utile per ambienti di collaudo)
180
+ --help Mostra questa schermata di aiuto
181
+
182
+ Esempi:
183
+ python main.py --api-url="https://api.pdnd.example.it/resource" --config /percorso/config.json
184
+ python main.py --status-url="https://api.pdnd.example.it/status" --config /percorso/config.json
185
+ python main.py --debug --api-url="https://api.pdnd.example.it/resource"
186
+ ```
187
+
188
+ ## Variabili di ambiente supportate
189
+
190
+ Se un parametro non è presente nel file di configurazione, puoi definirlo come variabile di ambiente:
191
+
192
+ - `PDND_KID`
193
+ - `PDND_ISSUER`
194
+ - `PDND_CLIENT_ID`
195
+ - `PDND_PURPOSE_ID`
196
+ - `PDND_PRIVKEY_PATH`
197
+
198
+ ## Note
199
+
200
+ - Il token viene salvato in un file temporaneo e riutilizzato finché è valido.
201
+ - Gli errori specifici vengono gestiti tramite la classe `PdndException`.
202
+
203
+ ## Esempio di configurazione minima
204
+
205
+ ```json
206
+ {
207
+ "produzione": {
208
+ "kid": "kid",
209
+ "issuer": "issuer",
210
+ "clientId": "clientId",
211
+ "purposeId": "purposeId",
212
+ "privKeyPath": "/tmp/key.pem"
213
+ }
214
+ }
215
+ ```
216
+ ## Esempio di configurazione per collaudo e prosuzione
217
+
218
+ ```json
219
+ {
220
+ "collaudo": {
221
+ "kid": "kid",
222
+ "issuer": "issuer",
223
+ "clientId": "clientId",
224
+ "purposeId": "purposeId",
225
+ "privKeyPath": "/tmp/key.pem"
226
+ },
227
+ "produzione": {
228
+ "kid": "kid",
229
+ "issuer": "issuer",
230
+ "clientId": "clientId",
231
+ "purposeId": "purposeId",
232
+ "privKeyPath": "/tmp/key.pem"
233
+ }
234
+ }
235
+ ```
236
+ ---
237
+
238
+ ## Contribuire
239
+
240
+ Le pull request sono benvenute! Per problemi o suggerimenti, apri una issue.
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ pdnd_client/__init__.py
5
+ pdnd_client/client.py
6
+ pdnd_client/config.py
7
+ pdnd_client/jwt_generator.py
8
+ pdnd_python_client.egg-info/PKG-INFO
9
+ pdnd_python_client.egg-info/SOURCES.txt
10
+ pdnd_python_client.egg-info/dependency_links.txt
11
+ pdnd_python_client.egg-info/requires.txt
12
+ pdnd_python_client.egg-info/top_level.txt
13
+ tests/test_pdnd_client.py
@@ -0,0 +1,8 @@
1
+ PyJWT>=2.0.0
2
+ cryptography
3
+ requests
4
+
5
+ [dev]
6
+ pytest
7
+ pytest-watch
8
+ requests-mock
@@ -0,0 +1,30 @@
1
+ [project]
2
+ name = "pdnd-python-client"
3
+ version = "0.1.0"
4
+ description = "Client Python per autenticazione e interazione con le API della Piattaforma Digitale Nazionale Dati (PDND)."
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ license = "MIT"
8
+ authors = [
9
+ {name = "Francesco Loreti", email = "francesco.loreti@isprambiente.it"}
10
+ ]
11
+
12
+ dependencies = [
13
+ "PyJWT>=2.0.0",
14
+ "cryptography",
15
+ "requests"
16
+ ]
17
+
18
+ [tool.setuptools]
19
+ packages = ["pdnd_client"]
20
+
21
+ [project.optional-dependencies]
22
+ dev = [
23
+ "pytest",
24
+ "pytest-watch",
25
+ "requests-mock"
26
+ ]
27
+
28
+ [build-system]
29
+ requires = ["setuptools>=61.0"]
30
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,48 @@
1
+
2
+ import pytest
3
+ from unittest.mock import patch, Mock
4
+ from pdnd_client.client import PDNDClient
5
+
6
+
7
+ # Il codice seguente è una suite di test per la classe PDNDClient,
8
+ # che verifica il funzionamento dei metodi get_status e get_api.
9
+ # Questi test utilizzano la libreria unittest.mock per simulare le risposte delle richieste HTTP
10
+ # e verificare che il client gestisca correttamente le risposte, sia in caso di successo che di errore.
11
+ # La suite include anche un fixture per inizializzare il client con un token di test e disabilitare la verifica SSL.
12
+ @pytest.fixture
13
+
14
+ # Crea un fixture per inizializzare il PDNDClient con un token di test e la verifica SSL disabilitata.
15
+ def client():
16
+ client = PDNDClient()
17
+ client.set_token("test-token")
18
+ return PDNDClient()
19
+
20
+ # La suite di test include test per richieste GET e POST riuscite e fallite,
21
+ # assicurandosi che il client si comporti come previsto in diverse condizioni.
22
+ def test_get_status_success(client):
23
+ mock_response = Mock()
24
+ mock_response.status_code = 200
25
+ mock_response.text = "OK"
26
+
27
+
28
+ # Simula il metodo requests.get per restituire una risposta predefinita
29
+ with patch("pdnd_client.client.requests.get", return_value=mock_response) as mock_get:
30
+ status_code, text = client.get_status("https://example.com/status")
31
+ mock_get.assert_called_once_with(
32
+ "https://example.com/status",
33
+ headers={"Authorization": f"Bearer {client.get_token()}"},
34
+ verify=True
35
+ )
36
+ assert status_code == 200
37
+ assert text == "OK"
38
+
39
+ # Test per una richiesta GET fallita
40
+ def test_get_status_failure(client):
41
+ mock_response = Mock()
42
+ mock_response.status_code = 404
43
+ mock_response.text = "Not Found"
44
+
45
+ with patch("pdnd_client.client.requests.get", return_value=mock_response):
46
+ status_code, text = client.get_status("https://example.com/invalid")
47
+ assert status_code == 404
48
+ assert text == "Not Found"