pdnd-python-client 0.1.0__tar.gz → 0.1.2__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.
- {pdnd_python_client-0.1.0 → pdnd_python_client-0.1.2}/LICENSE +0 -0
- {pdnd_python_client-0.1.0/pdnd_python_client.egg-info → pdnd_python_client-0.1.2}/PKG-INFO +1 -1
- {pdnd_python_client-0.1.0 → pdnd_python_client-0.1.2}/README.md +0 -0
- {pdnd_python_client-0.1.0 → pdnd_python_client-0.1.2}/pdnd_client/__init__.py +0 -0
- {pdnd_python_client-0.1.0 → pdnd_python_client-0.1.2}/pdnd_client/client.py +59 -25
- {pdnd_python_client-0.1.0 → pdnd_python_client-0.1.2}/pdnd_client/config.py +2 -2
- {pdnd_python_client-0.1.0 → pdnd_python_client-0.1.2}/pdnd_client/jwt_generator.py +35 -11
- {pdnd_python_client-0.1.0 → pdnd_python_client-0.1.2/pdnd_python_client.egg-info}/PKG-INFO +1 -1
- {pdnd_python_client-0.1.0 → pdnd_python_client-0.1.2}/pdnd_python_client.egg-info/SOURCES.txt +0 -0
- {pdnd_python_client-0.1.0 → pdnd_python_client-0.1.2}/pdnd_python_client.egg-info/dependency_links.txt +0 -0
- {pdnd_python_client-0.1.0 → pdnd_python_client-0.1.2}/pdnd_python_client.egg-info/requires.txt +0 -0
- {pdnd_python_client-0.1.0 → pdnd_python_client-0.1.2}/pdnd_python_client.egg-info/top_level.txt +0 -0
- {pdnd_python_client-0.1.0 → pdnd_python_client-0.1.2}/pyproject.toml +1 -1
- {pdnd_python_client-0.1.0 → pdnd_python_client-0.1.2}/setup.cfg +0 -0
- {pdnd_python_client-0.1.0 → pdnd_python_client-0.1.2}/tests/test_pdnd_client.py +2 -1
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pdnd-python-client
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Client Python per autenticazione e interazione con le API della Piattaforma Digitale Nazionale Dati (PDND).
|
|
5
5
|
Author-email: Francesco Loreti <francesco.loreti@isprambiente.it>
|
|
6
6
|
License-Expression: MIT
|
|
File without changes
|
|
File without changes
|
|
@@ -22,45 +22,60 @@ class PDNDClient:
|
|
|
22
22
|
self.filters = {}
|
|
23
23
|
self.debug = False
|
|
24
24
|
self.token = ""
|
|
25
|
-
self.token_file = "pdnd_token.json"
|
|
25
|
+
self.token_file = "tmp/pdnd_token.json"
|
|
26
26
|
self.token_exp = None # Token expiration time, if applicable
|
|
27
27
|
|
|
28
28
|
# Questo metodo recupera l'URL dell'API, che può essere sovrascritto dall'utente.
|
|
29
|
-
def get_api_url(self):
|
|
29
|
+
def get_api_url(self) -> str:
|
|
30
30
|
return self.api_url if hasattr(self, 'api_url') else None
|
|
31
31
|
|
|
32
32
|
# Questo metodo imposta l'URL dell'API per le richieste successive.
|
|
33
|
-
def set_api_url(self, api_url):
|
|
33
|
+
def set_api_url(self, api_url) -> bool:
|
|
34
34
|
self.api_url = api_url
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
# Imposta i filtri da utilizzare nelle richieste API.
|
|
38
|
+
# Se viene fornita una stringa, la converte in un dizionario.
|
|
39
|
+
def set_filters(self, filters) -> bool:
|
|
40
|
+
if isinstance(filters, str):
|
|
41
|
+
# Analizza la stringa nel formato "chiave1=val1&chiave2=val2"
|
|
42
|
+
self.filters = dict(pair.split("=", 1) for pair in filters.split("&") if "=" in pair)
|
|
43
|
+
elif isinstance(filters, dict):
|
|
44
|
+
self.filters = filters
|
|
45
|
+
else:
|
|
46
|
+
raise ValueError("I filtri devono essere una stringa o un dizionario.")
|
|
47
|
+
return True
|
|
39
48
|
|
|
40
49
|
# Questo metodo imposta la modalità di debug, che controlla se stampare un output dettagliato.
|
|
41
|
-
def set_debug(self, debug):
|
|
50
|
+
def set_debug(self, debug) -> bool:
|
|
42
51
|
self.debug = debug
|
|
52
|
+
return True
|
|
43
53
|
|
|
44
54
|
# Questo metodo imposta il tempo di scadenza per il token.
|
|
45
55
|
# Può essere una stringa nel formato "YYYY-MM-DD HH:MM:SS" oppure un oggetto datetime.
|
|
46
56
|
# Se non viene fornito, il valore predefinito è None.
|
|
47
|
-
def set_expiration(self, exp):
|
|
57
|
+
def set_expiration(self, exp) -> bool:
|
|
48
58
|
self.token_exp = exp
|
|
59
|
+
return True
|
|
49
60
|
|
|
50
61
|
# Questo metodo imposta l'URL di stato per le richieste GET.
|
|
51
|
-
def set_status_url(self, status_url):
|
|
62
|
+
def set_status_url(self, status_url) -> bool:
|
|
52
63
|
self.status_url = status_url
|
|
64
|
+
return True
|
|
53
65
|
|
|
54
|
-
def set_token(self, token):
|
|
66
|
+
def set_token(self, token) -> bool:
|
|
55
67
|
self.token = token
|
|
68
|
+
return True
|
|
56
69
|
|
|
57
|
-
def set_token_file(self, token_file):
|
|
70
|
+
def set_token_file(self, token_file) -> bool:
|
|
58
71
|
self.token_file = token_file
|
|
72
|
+
return True
|
|
59
73
|
|
|
60
|
-
def set_verify_ssl(self, verify_ssl):
|
|
74
|
+
def set_verify_ssl(self, verify_ssl) -> bool:
|
|
61
75
|
self.verify_ssl = verify_ssl
|
|
76
|
+
return True
|
|
62
77
|
|
|
63
|
-
def get_api(self, token: str):
|
|
78
|
+
def get_api(self, token: str) -> [int, str]:
|
|
64
79
|
url = self.api_url if hasattr(self, 'api_url') and self.api_url else self.get_api_url()
|
|
65
80
|
|
|
66
81
|
# Aggiunta dei filtri come query string
|
|
@@ -95,12 +110,12 @@ class PDNDClient:
|
|
|
95
110
|
return status_code, body
|
|
96
111
|
|
|
97
112
|
# 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):
|
|
113
|
+
def get_status(self, url) -> [int, str]:
|
|
99
114
|
headers = {"Authorization": f"Bearer {self.token}"}
|
|
100
115
|
response = requests.get(url, headers=headers, verify=self.verify_ssl)
|
|
101
116
|
return response.status_code, response.text
|
|
102
117
|
|
|
103
|
-
def get_token(self):
|
|
118
|
+
def get_token(self) -> str:
|
|
104
119
|
return self.token
|
|
105
120
|
|
|
106
121
|
def is_token_valid(self, exp) -> bool:
|
|
@@ -112,28 +127,48 @@ class PDNDClient:
|
|
|
112
127
|
raise ValueError("L'exp deve essere una stringa o un oggetto datetime")
|
|
113
128
|
return time.time() < exp.timestamp()
|
|
114
129
|
|
|
115
|
-
def load_token(self, file: str = None):
|
|
130
|
+
def load_token(self, file: str = None) -> [str, str]:
|
|
116
131
|
file = file or self.token_file # Usa il file passato o quello di default
|
|
117
132
|
|
|
118
133
|
if not os.path.exists(file):
|
|
119
|
-
return None
|
|
134
|
+
return [None, None]
|
|
120
135
|
|
|
121
136
|
try:
|
|
122
137
|
with open(file, "r", encoding="utf-8") as f:
|
|
123
138
|
data = json.load(f)
|
|
124
139
|
except (json.JSONDecodeError, IOError):
|
|
125
|
-
return None
|
|
140
|
+
return [None, None]
|
|
126
141
|
|
|
127
142
|
if not data or "token" not in data or "exp" not in data:
|
|
128
|
-
return None
|
|
143
|
+
return [None, None]
|
|
129
144
|
|
|
145
|
+
self.token = data["token"]
|
|
130
146
|
self.token_exp = data["exp"]
|
|
131
147
|
return data["token"], data["exp"]
|
|
132
148
|
|
|
133
149
|
|
|
134
|
-
def save_token(self, token: str, exp: str, file: str = None):
|
|
150
|
+
def save_token(self, token: str, exp: str, file: str = None) -> bool:
|
|
151
|
+
if not token:
|
|
152
|
+
raise ValueError("Il token non può essere vuoto")
|
|
153
|
+
if not exp:
|
|
154
|
+
raise ValueError("L'exp non può essere vuoto")
|
|
155
|
+
if not isinstance(token, str):
|
|
156
|
+
raise ValueError("Il token deve essere una stringa")
|
|
157
|
+
if not isinstance(exp, str) and not isinstance(exp, datetime):
|
|
158
|
+
raise ValueError("L'exp deve essere una stringa o un oggetto datetime")
|
|
159
|
+
|
|
135
160
|
file = file or self.token_file # Usa il file passato o quello di default
|
|
136
161
|
exp = exp or self.token_exp # Usa l'exp passato o quello corrente
|
|
162
|
+
if not isinstance(exp, str):
|
|
163
|
+
raise ValueError("L'exp deve essere una stringa nel formato 'YYYY-MM-DD HH:MM:SS'")
|
|
164
|
+
try:
|
|
165
|
+
datetime.strptime(exp, "%Y-%m-%d %H:%M:%S") # Verifica che l'exp sia nel formato corretto
|
|
166
|
+
except ValueError:
|
|
167
|
+
raise ValueError("L'exp deve essere una stringa nel formato 'YYYY-MM-DD HH:MM:SS'")
|
|
168
|
+
|
|
169
|
+
if not os.path.exists(os.path.dirname(file)):
|
|
170
|
+
os.makedirs(os.path.dirname(file))
|
|
171
|
+
|
|
137
172
|
data = {
|
|
138
173
|
"token": token,
|
|
139
174
|
"exp": exp
|
|
@@ -141,7 +176,6 @@ class PDNDClient:
|
|
|
141
176
|
with open(file, "w", encoding="utf-8") as f:
|
|
142
177
|
json.dump(data, f, ensure_ascii=False)
|
|
143
178
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
179
|
+
self.token = token
|
|
180
|
+
self.token_exp = exp
|
|
181
|
+
return True
|
|
@@ -8,7 +8,7 @@ import os
|
|
|
8
8
|
# Il metodo get consente di recuperare valori specifici della configurazione, con un valore predefinito opzionale.
|
|
9
9
|
class Config:
|
|
10
10
|
# Questo metodo inizializza l'oggetto Config caricando la configurazione da un file JSON.
|
|
11
|
-
def __init__(self, config_path, env):
|
|
11
|
+
def __init__(self, config_path, env: "produzione"):
|
|
12
12
|
self.env = env
|
|
13
13
|
with open(config_path, "r") as f:
|
|
14
14
|
full_config = json.load(f)
|
|
@@ -17,5 +17,5 @@ class Config:
|
|
|
17
17
|
self.config = full_config[env]
|
|
18
18
|
|
|
19
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):
|
|
20
|
+
def get(self, key, default=None) -> str:
|
|
21
21
|
return self.config.get(key, default)
|
|
@@ -6,6 +6,7 @@ import base64
|
|
|
6
6
|
import requests
|
|
7
7
|
import jwt # PyJWT
|
|
8
8
|
import secrets
|
|
9
|
+
import os
|
|
9
10
|
from datetime import datetime, timezone
|
|
10
11
|
from jwt import exceptions as jwt_exceptions
|
|
11
12
|
|
|
@@ -22,23 +23,46 @@ class JWTGenerator:
|
|
|
22
23
|
self.client_id = config.get("clientId")
|
|
23
24
|
self.endpoint = config.get("endpoint")
|
|
24
25
|
self.env = config.get("env", "produzione")
|
|
26
|
+
self.privKeyPath = config.get("privKeyPath")
|
|
27
|
+
self.issuer = self.config.get("issuer")
|
|
28
|
+
self.clientId = self.config.get("clientId")
|
|
29
|
+
self.kid = self.config.get("kid")
|
|
30
|
+
self.purposeId = self.config.get("purposeId")
|
|
25
31
|
self.token_exp = None
|
|
26
32
|
self.endpoint = "https://auth.interop.pagopa.it/token.oauth2"
|
|
27
33
|
self.aud = "auth.interop.pagopa.it/client-assertion"
|
|
28
34
|
|
|
29
|
-
def set_debug(self, debug):
|
|
30
|
-
self.debug = debug
|
|
31
35
|
|
|
36
|
+
def set_debug(self, debug) -> bool:
|
|
37
|
+
self.debug = debug
|
|
38
|
+
return True
|
|
32
39
|
|
|
33
|
-
def set_env(self, env):
|
|
40
|
+
def set_env(self, env: "produzione") -> bool:
|
|
34
41
|
self.env = env
|
|
35
42
|
if self.env == "collaudo":
|
|
36
43
|
self.endpoint = "https://auth.uat.interop.pagopa.it/token.oauth2"
|
|
37
44
|
self.aud = "auth.uat.interop.pagopa.it/client-assertion"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
def request_token(self) -> str:
|
|
48
|
+
if not self.client_id:
|
|
49
|
+
raise ValueError("Client ID non specificato nella configurazione.")
|
|
50
|
+
if not self.privKeyPath:
|
|
51
|
+
raise ValueError("Percorso della chiave privata non specificato nella configurazione.")
|
|
52
|
+
if not os.path.exists(self.privKeyPath):
|
|
53
|
+
raise FileNotFoundError(f"File della chiave privata non trovato: {self.privKeyPath}")
|
|
54
|
+
if not self.endpoint:
|
|
55
|
+
raise ValueError("Endpoint non specificato nella configurazione.")
|
|
56
|
+
if not self.issuer:
|
|
57
|
+
raise ValueError("Issuer non specificato nella configurazione.")
|
|
58
|
+
if not self.clientId:
|
|
59
|
+
raise ValueError("Client ID non specificato nella configurazione.")
|
|
60
|
+
if not self.kid:
|
|
61
|
+
raise ValueError("KID non specificato nella configurazione.")
|
|
62
|
+
if not self.purposeId:
|
|
63
|
+
raise ValueError("Purpose ID non specificato nella configurazione.")
|
|
64
|
+
|
|
65
|
+
with open(self.privKeyPath, "rb") as key_file:
|
|
42
66
|
private_key = key_file.read()
|
|
43
67
|
|
|
44
68
|
issued_at = int(time.time())
|
|
@@ -46,17 +70,17 @@ class JWTGenerator:
|
|
|
46
70
|
jti = secrets.token_hex(16)
|
|
47
71
|
|
|
48
72
|
payload = {
|
|
49
|
-
"iss": self.
|
|
50
|
-
"sub": self.
|
|
73
|
+
"iss": self.issuer,
|
|
74
|
+
"sub": self.clientId,
|
|
51
75
|
"aud": self.aud,
|
|
52
|
-
"purposeId": self.
|
|
76
|
+
"purposeId": self.purposeId,
|
|
53
77
|
"jti": jti,
|
|
54
78
|
"iat": issued_at,
|
|
55
79
|
"exp": expiration_time
|
|
56
80
|
}
|
|
57
81
|
|
|
58
82
|
headers = {
|
|
59
|
-
"kid": self.
|
|
83
|
+
"kid": self.kid,
|
|
60
84
|
"alg": "RS256",
|
|
61
85
|
"typ": "JWT"
|
|
62
86
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pdnd-python-client
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Client Python per autenticazione e interazione con le API della Piattaforma Digitale Nazionale Dati (PDND).
|
|
5
5
|
Author-email: Francesco Loreti <francesco.loreti@isprambiente.it>
|
|
6
6
|
License-Expression: MIT
|
{pdnd_python_client-0.1.0 → pdnd_python_client-0.1.2}/pdnd_python_client.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{pdnd_python_client-0.1.0 → pdnd_python_client-0.1.2}/pdnd_python_client.egg-info/requires.txt
RENAMED
|
File without changes
|
{pdnd_python_client-0.1.0 → pdnd_python_client-0.1.2}/pdnd_python_client.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
|
|
2
2
|
import pytest
|
|
3
3
|
from unittest.mock import patch, Mock
|
|
4
|
+
from pdnd_client.config import Config
|
|
5
|
+
from pdnd_client.jwt_generator import JWTGenerator
|
|
4
6
|
from pdnd_client.client import PDNDClient
|
|
5
7
|
|
|
6
|
-
|
|
7
8
|
# Il codice seguente è una suite di test per la classe PDNDClient,
|
|
8
9
|
# che verifica il funzionamento dei metodi get_status e get_api.
|
|
9
10
|
# Questi test utilizzano la libreria unittest.mock per simulare le risposte delle richieste HTTP
|