saamfi-sdk 0.1.0__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.
Potentially problematic release.
This version of saamfi-sdk might be problematic. Click here for more details.
- saamfi_sdk/__init__.py +49 -0
- saamfi_sdk/client.py +311 -0
- saamfi_sdk/exceptions.py +70 -0
- saamfi_sdk/models.py +141 -0
- saamfi_sdk/py.typed +2 -0
- saamfi_sdk/services/__init__.py +18 -0
- saamfi_sdk/services/auth.py +90 -0
- saamfi_sdk/services/base.py +45 -0
- saamfi_sdk/services/institutions.py +113 -0
- saamfi_sdk/services/systems.py +62 -0
- saamfi_sdk/services/token.py +155 -0
- saamfi_sdk/services/users.py +147 -0
- saamfi_sdk-0.1.0.dist-info/METADATA +257 -0
- saamfi_sdk-0.1.0.dist-info/RECORD +17 -0
- saamfi_sdk-0.1.0.dist-info/WHEEL +5 -0
- saamfi_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
- saamfi_sdk-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SaamfiServiceBase:
|
|
9
|
+
"""Common helpers for Saamfi service classes."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, base_url: str, session: requests.Session):
|
|
12
|
+
self._base_url = base_url.rstrip("/")
|
|
13
|
+
self._session = session
|
|
14
|
+
|
|
15
|
+
def _build_url(self, path: str) -> str:
|
|
16
|
+
"""Construct an absolute URL from a path."""
|
|
17
|
+
return f"{self._base_url}/{path.lstrip('/')}"
|
|
18
|
+
|
|
19
|
+
def _get(self, path: str, **kwargs: Any) -> requests.Response:
|
|
20
|
+
"""Perform a GET request using the shared session."""
|
|
21
|
+
url = self._build_url(path)
|
|
22
|
+
return self._session.get(url, **kwargs)
|
|
23
|
+
|
|
24
|
+
def _post(self, path: str, **kwargs: Any) -> requests.Response:
|
|
25
|
+
"""Perform a POST request using the shared session."""
|
|
26
|
+
url = self._build_url(path)
|
|
27
|
+
return self._session.post(url, **kwargs)
|
|
28
|
+
|
|
29
|
+
@staticmethod
|
|
30
|
+
def _is_success(status_code: int) -> bool:
|
|
31
|
+
"""Check if an HTTP status code represents a successful response."""
|
|
32
|
+
return 200 <= status_code < 300
|
|
33
|
+
|
|
34
|
+
@staticmethod
|
|
35
|
+
def _authorization_header(token: str) -> Dict[str, str]:
|
|
36
|
+
"""Create a bearer authorization header."""
|
|
37
|
+
return {"Authorization": f"Bearer {token}"}
|
|
38
|
+
|
|
39
|
+
@staticmethod
|
|
40
|
+
def _json_headers(token: str) -> Dict[str, str]:
|
|
41
|
+
"""Create headers for JSON requests with bearer authorization."""
|
|
42
|
+
headers = SaamfiServiceBase._authorization_header(token)
|
|
43
|
+
headers["Content-Type"] = "application/json"
|
|
44
|
+
return headers
|
|
45
|
+
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from .base import SaamfiServiceBase
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InstitutionService(SaamfiServiceBase):
|
|
12
|
+
"""Operations for retrieving institution metadata."""
|
|
13
|
+
|
|
14
|
+
def list_public_institutions(self) -> Optional[List[Dict[str, Any]]]:
|
|
15
|
+
"""Retrieve the public list of institutions."""
|
|
16
|
+
try:
|
|
17
|
+
response = self._get("public/institutions", timeout=30)
|
|
18
|
+
except Exception as exc:
|
|
19
|
+
logger.warning("Error retrieving public institutions: %s", exc)
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
if self._is_success(response.status_code):
|
|
23
|
+
try:
|
|
24
|
+
return response.json()
|
|
25
|
+
except Exception as exc:
|
|
26
|
+
logger.warning("Invalid response when listing public institutions: %s", exc)
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
logger.warning(
|
|
30
|
+
"Failed to retrieve public institutions: status code %s", response.status_code
|
|
31
|
+
)
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
def get_institution_by_nit(self, auth_token: str, nit: str) -> Optional[str]:
|
|
35
|
+
"""Retrieve an institution by its NIT."""
|
|
36
|
+
headers = self._authorization_header(auth_token)
|
|
37
|
+
try:
|
|
38
|
+
response = self._get(f"public/institutions?nit={nit}", headers=headers, timeout=30)
|
|
39
|
+
except Exception as exc:
|
|
40
|
+
logger.warning("Error retrieving institution by NIT: %s", exc)
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
if self._is_success(response.status_code):
|
|
44
|
+
logger.info("Retrieved institution by NIT '%s'", nit)
|
|
45
|
+
return response.text
|
|
46
|
+
|
|
47
|
+
logger.warning("Failed to retrieve institution by NIT '%s': %s", nit, response.status_code)
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
def get_institutions_by_ids(self, auth_token: str, institution_ids: List[int]) -> Optional[str]:
|
|
51
|
+
"""Retrieve multiple institutions by ID list."""
|
|
52
|
+
if not institution_ids:
|
|
53
|
+
logger.warning("No institution IDs provided")
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
headers = self._authorization_header(auth_token)
|
|
57
|
+
params = {
|
|
58
|
+
"institutionIds": ",".join(str(institution_id) for institution_id in institution_ids)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
response = self._get(
|
|
63
|
+
"institutions",
|
|
64
|
+
headers=headers,
|
|
65
|
+
params=params,
|
|
66
|
+
timeout=30,
|
|
67
|
+
)
|
|
68
|
+
except Exception as exc:
|
|
69
|
+
logger.warning("Error retrieving institutions by IDs: %s", exc)
|
|
70
|
+
return None
|
|
71
|
+
|
|
72
|
+
if self._is_success(response.status_code):
|
|
73
|
+
logger.info("Retrieved institutions for IDs %s", institution_ids)
|
|
74
|
+
return response.text
|
|
75
|
+
|
|
76
|
+
logger.warning(
|
|
77
|
+
"Failed to retrieve institutions by IDs %s: %s",
|
|
78
|
+
institution_ids,
|
|
79
|
+
response.status_code,
|
|
80
|
+
)
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
def get_institution_params(self, auth_token: str, institution_id: int) -> Optional[Dict[str, Any]]:
|
|
84
|
+
"""Retrieve configuration parameters for a specific institution."""
|
|
85
|
+
headers = self._authorization_header(auth_token)
|
|
86
|
+
try:
|
|
87
|
+
response = self._get(
|
|
88
|
+
f"institutions/{institution_id}/params",
|
|
89
|
+
headers=headers,
|
|
90
|
+
timeout=30,
|
|
91
|
+
)
|
|
92
|
+
except Exception as exc:
|
|
93
|
+
logger.warning("Error retrieving params for institution %s: %s", institution_id, exc)
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
if self._is_success(response.status_code):
|
|
97
|
+
try:
|
|
98
|
+
return response.json()
|
|
99
|
+
except Exception as exc:
|
|
100
|
+
logger.warning(
|
|
101
|
+
"Invalid response while retrieving params for institution %s: %s",
|
|
102
|
+
institution_id,
|
|
103
|
+
exc,
|
|
104
|
+
)
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
logger.warning(
|
|
108
|
+
"Failed to retrieve params for institution %s: %s",
|
|
109
|
+
institution_id,
|
|
110
|
+
response.status_code,
|
|
111
|
+
)
|
|
112
|
+
return None
|
|
113
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from .base import SaamfiServiceBase
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SystemService(SaamfiServiceBase):
|
|
12
|
+
"""Operations related to systems metadata and configuration."""
|
|
13
|
+
|
|
14
|
+
def list_public_systems(self) -> Optional[List[Dict[str, Any]]]:
|
|
15
|
+
"""Retrieve public information about available systems."""
|
|
16
|
+
try:
|
|
17
|
+
response = self._get("public/systems", timeout=30)
|
|
18
|
+
except Exception as exc:
|
|
19
|
+
logger.warning("Error retrieving public systems: %s", exc)
|
|
20
|
+
return None
|
|
21
|
+
|
|
22
|
+
if self._is_success(response.status_code):
|
|
23
|
+
try:
|
|
24
|
+
return response.json()
|
|
25
|
+
except Exception as exc:
|
|
26
|
+
logger.warning("Invalid response when listing public systems: %s", exc)
|
|
27
|
+
return None
|
|
28
|
+
|
|
29
|
+
logger.warning("Failed to retrieve public systems: status code %s", response.status_code)
|
|
30
|
+
return None
|
|
31
|
+
|
|
32
|
+
def get_system_roles(self, auth_token: str, system_id: int) -> Optional[List[Dict[str, Any]]]:
|
|
33
|
+
"""Retrieve roles for the given system."""
|
|
34
|
+
headers = self._authorization_header(auth_token)
|
|
35
|
+
try:
|
|
36
|
+
response = self._get(
|
|
37
|
+
f"systems/{system_id}/roles",
|
|
38
|
+
headers=headers,
|
|
39
|
+
timeout=30,
|
|
40
|
+
)
|
|
41
|
+
except Exception as exc:
|
|
42
|
+
logger.warning("Error retrieving roles for system %s: %s", system_id, exc)
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
if self._is_success(response.status_code):
|
|
46
|
+
try:
|
|
47
|
+
return response.json()
|
|
48
|
+
except Exception as exc:
|
|
49
|
+
logger.warning(
|
|
50
|
+
"Invalid response while retrieving roles for system %s: %s",
|
|
51
|
+
system_id,
|
|
52
|
+
exc,
|
|
53
|
+
)
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
logger.warning(
|
|
57
|
+
"Failed to retrieve roles for system %s: %s",
|
|
58
|
+
system_id,
|
|
59
|
+
response.status_code,
|
|
60
|
+
)
|
|
61
|
+
return None
|
|
62
|
+
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import base64
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
|
|
7
|
+
import jwt
|
|
8
|
+
from cryptography.hazmat.backends import default_backend
|
|
9
|
+
from cryptography.hazmat.primitives.asymmetric import rsa
|
|
10
|
+
from cryptography.hazmat.primitives.serialization import load_der_public_key
|
|
11
|
+
|
|
12
|
+
from saamfi_sdk.exceptions import SaamfiConnectionError, SaamfiTokenValidationError
|
|
13
|
+
|
|
14
|
+
from .base import SaamfiServiceBase
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TokenService(SaamfiServiceBase):
|
|
20
|
+
"""Handle public key retrieval and JWT token operations."""
|
|
21
|
+
|
|
22
|
+
ROLE_CLAIM = "role"
|
|
23
|
+
SYSTEM_CLAIM = "system"
|
|
24
|
+
USERNAME_CLAIM = "username"
|
|
25
|
+
ID_CLAIM = "persId"
|
|
26
|
+
|
|
27
|
+
def __init__(self, base_url: str, session):
|
|
28
|
+
super().__init__(base_url, session)
|
|
29
|
+
self._public_key_cache: Optional[bytes] = None
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def public_key_bytes(self) -> Optional[bytes]:
|
|
33
|
+
"""Return cached public key bytes, if available."""
|
|
34
|
+
return self._public_key_cache
|
|
35
|
+
|
|
36
|
+
def fetch_public_key_bytes(self) -> bytes:
|
|
37
|
+
"""Retrieve raw DER-encoded public key bytes from the Saamfi server."""
|
|
38
|
+
if self._public_key_cache is not None:
|
|
39
|
+
return self._public_key_cache
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
response = self._get("public/publicKey", timeout=30)
|
|
43
|
+
response.raise_for_status()
|
|
44
|
+
except Exception as exc:
|
|
45
|
+
logger.error("Error retrieving public key: %s", exc)
|
|
46
|
+
raise SaamfiConnectionError(f"Error retrieving public key: {exc}") from exc
|
|
47
|
+
|
|
48
|
+
key_bytes: Optional[bytes] = None
|
|
49
|
+
payload = response.text.strip()
|
|
50
|
+
|
|
51
|
+
# Format 1: Legacy numeric list "[48, 130, ...]"
|
|
52
|
+
if payload.startswith("[") and payload.endswith("]"):
|
|
53
|
+
key_string = payload[1:-1]
|
|
54
|
+
byte_strings = key_string.split(",")
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
byte_values = []
|
|
58
|
+
for item in byte_strings:
|
|
59
|
+
stripped = item.strip()
|
|
60
|
+
if not stripped:
|
|
61
|
+
raise ValueError("Empty byte element in public key payload")
|
|
62
|
+
value = int(stripped)
|
|
63
|
+
if not -256 <= value <= 255:
|
|
64
|
+
raise ValueError(f"Byte element out of range: {value}")
|
|
65
|
+
byte_values.append(value & 0xFF)
|
|
66
|
+
key_bytes = bytes(byte_values)
|
|
67
|
+
except Exception as exc:
|
|
68
|
+
logger.error("Invalid public key payload: %s", payload)
|
|
69
|
+
raise SaamfiConnectionError(f"Error retrieving public key: {exc}") from exc
|
|
70
|
+
|
|
71
|
+
# Format 2: PEM with headers (-----BEGIN PUBLIC KEY-----)
|
|
72
|
+
if key_bytes is None and "BEGIN PUBLIC KEY" in payload:
|
|
73
|
+
try:
|
|
74
|
+
pem_lines = [
|
|
75
|
+
line.strip()
|
|
76
|
+
for line in payload.splitlines()
|
|
77
|
+
if line and "BEGIN" not in line and "END" not in line
|
|
78
|
+
]
|
|
79
|
+
key_bytes = base64.b64decode("".join(pem_lines), validate=True)
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
logger.error("Invalid PEM-formatted public key payload")
|
|
82
|
+
raise SaamfiConnectionError(f"Error retrieving public key: {exc}") from exc
|
|
83
|
+
|
|
84
|
+
# Format 3: Plain base64 string
|
|
85
|
+
if key_bytes is None:
|
|
86
|
+
try:
|
|
87
|
+
key_bytes = base64.b64decode(payload, validate=True)
|
|
88
|
+
except Exception:
|
|
89
|
+
logger.error("Invalid public key payload: %s", payload[:64] + "...")
|
|
90
|
+
raise SaamfiConnectionError("Error retrieving public key: invalid payload format")
|
|
91
|
+
|
|
92
|
+
if not key_bytes:
|
|
93
|
+
logger.error("Public key payload is empty")
|
|
94
|
+
raise SaamfiConnectionError("Error retrieving public key: empty payload")
|
|
95
|
+
|
|
96
|
+
self._public_key_cache = key_bytes
|
|
97
|
+
logger.info("Public key successfully retrieved from Saamfi server")
|
|
98
|
+
return key_bytes
|
|
99
|
+
|
|
100
|
+
def get_rsa_public_key(self) -> rsa.RSAPublicKey:
|
|
101
|
+
"""Return the RSA public key object used to validate JWT tokens."""
|
|
102
|
+
try:
|
|
103
|
+
key_bytes = self.fetch_public_key_bytes()
|
|
104
|
+
public_key = load_der_public_key(key_bytes, backend=default_backend())
|
|
105
|
+
except SaamfiConnectionError:
|
|
106
|
+
raise
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
logger.error("Error getting public key: %s", exc)
|
|
109
|
+
raise SaamfiConnectionError(f"Error getting public key: {exc}") from exc
|
|
110
|
+
|
|
111
|
+
if not isinstance(public_key, rsa.RSAPublicKey):
|
|
112
|
+
raise SaamfiConnectionError("Retrieved key is not an RSA public key")
|
|
113
|
+
return public_key
|
|
114
|
+
|
|
115
|
+
def extract_roles(self, auth_token: str) -> List[str]:
|
|
116
|
+
"""Extract roles from a JWT token."""
|
|
117
|
+
try:
|
|
118
|
+
decoded_token = jwt.decode(
|
|
119
|
+
auth_token,
|
|
120
|
+
self.get_rsa_public_key(),
|
|
121
|
+
algorithms=["RS256"],
|
|
122
|
+
)
|
|
123
|
+
except Exception as exc:
|
|
124
|
+
logger.warning("Error extracting roles from JWT: %s", exc)
|
|
125
|
+
return []
|
|
126
|
+
|
|
127
|
+
role_claim = decoded_token.get(self.ROLE_CLAIM, "")
|
|
128
|
+
if not role_claim:
|
|
129
|
+
return []
|
|
130
|
+
return [role.strip() for role in role_claim.split(",")]
|
|
131
|
+
|
|
132
|
+
def validate_token(self, auth_token: str) -> dict:
|
|
133
|
+
"""Validate a JWT token and return decoded claims."""
|
|
134
|
+
try:
|
|
135
|
+
decoded_token = jwt.decode(
|
|
136
|
+
auth_token,
|
|
137
|
+
self.get_rsa_public_key(),
|
|
138
|
+
algorithms=["RS256"],
|
|
139
|
+
)
|
|
140
|
+
except jwt.ExpiredSignatureError as exc:
|
|
141
|
+
logger.error("Token has expired: %s", exc)
|
|
142
|
+
raise SaamfiTokenValidationError(f"Token has expired: {exc}") from exc
|
|
143
|
+
except jwt.InvalidTokenError as exc:
|
|
144
|
+
logger.error("Invalid token: %s", exc)
|
|
145
|
+
raise SaamfiTokenValidationError(f"Invalid token: {exc}") from exc
|
|
146
|
+
except Exception as exc:
|
|
147
|
+
logger.error("Error validating token: %s", exc)
|
|
148
|
+
raise SaamfiTokenValidationError(f"Error validating token: {exc}") from exc
|
|
149
|
+
|
|
150
|
+
return decoded_token
|
|
151
|
+
|
|
152
|
+
def reset_public_key_cache(self) -> None:
|
|
153
|
+
"""Clear the cached public key. Intended for testing."""
|
|
154
|
+
self._public_key_cache = None
|
|
155
|
+
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from saamfi_sdk.exceptions import SaamfiAuthenticationError, SaamfiConnectionError
|
|
7
|
+
from saamfi_sdk.models import UserInfo
|
|
8
|
+
|
|
9
|
+
from .base import SaamfiServiceBase
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class UserService(SaamfiServiceBase):
|
|
15
|
+
"""Operations for querying user information."""
|
|
16
|
+
|
|
17
|
+
def get_user_info(self, auth_token: str, user_id: int) -> Optional[UserInfo]:
|
|
18
|
+
"""Retrieve full user information by ID."""
|
|
19
|
+
headers = self._authorization_header(auth_token)
|
|
20
|
+
try:
|
|
21
|
+
response = self._get(f"users/{user_id}", headers=headers, timeout=30)
|
|
22
|
+
except Exception as exc:
|
|
23
|
+
logger.error("Error retrieving user info: %s", exc)
|
|
24
|
+
raise SaamfiConnectionError(f"Error retrieving user info: {exc}") from exc
|
|
25
|
+
|
|
26
|
+
if self._is_success(response.status_code):
|
|
27
|
+
try:
|
|
28
|
+
user = UserInfo(**response.json())
|
|
29
|
+
except Exception as exc:
|
|
30
|
+
logger.warning("Unexpected user info payload: %s", exc)
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
logger.info("User info retrieved successfully for user ID %s", user_id)
|
|
34
|
+
return user
|
|
35
|
+
|
|
36
|
+
logger.warning("Failed to retrieve user info for ID %s: %s", user_id, response.status_code)
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
def get_user_by_username(self, auth_token: str, username: str) -> Optional[Dict[str, Any]]:
|
|
40
|
+
"""Retrieve a user by username."""
|
|
41
|
+
headers = self._json_headers(auth_token)
|
|
42
|
+
try:
|
|
43
|
+
response = self._post(
|
|
44
|
+
"users/user-from-username",
|
|
45
|
+
json=username,
|
|
46
|
+
headers=headers,
|
|
47
|
+
timeout=30,
|
|
48
|
+
)
|
|
49
|
+
except Exception as exc:
|
|
50
|
+
logger.error("Error searching user by username: %s", exc)
|
|
51
|
+
raise SaamfiConnectionError(f"Error searching user by username: {exc}") from exc
|
|
52
|
+
|
|
53
|
+
if response.status_code == 401:
|
|
54
|
+
logger.error("Authentication error when searching user by username")
|
|
55
|
+
raise SaamfiAuthenticationError("Error authenticating")
|
|
56
|
+
|
|
57
|
+
if response.status_code != 200:
|
|
58
|
+
logger.error(
|
|
59
|
+
"Error searching user by username '%s': %s", username, response.status_code
|
|
60
|
+
)
|
|
61
|
+
raise SaamfiConnectionError("Error")
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
payload = response.json()
|
|
65
|
+
except ValueError:
|
|
66
|
+
logger.warning("Empty response when searching user by username '%s'", username)
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
logger.info("User found by username '%s'", username)
|
|
70
|
+
return payload
|
|
71
|
+
|
|
72
|
+
def get_users_by_document(self, auth_token: str, user_documents: List[str]) -> List[Dict[str, Any]]:
|
|
73
|
+
"""Retrieve users by document list."""
|
|
74
|
+
headers = self._json_headers(auth_token)
|
|
75
|
+
try:
|
|
76
|
+
response = self._post(
|
|
77
|
+
"users/users-from-document",
|
|
78
|
+
json=user_documents,
|
|
79
|
+
headers=headers,
|
|
80
|
+
timeout=30,
|
|
81
|
+
)
|
|
82
|
+
except Exception as exc:
|
|
83
|
+
logger.error("Error searching users by documents: %s", exc)
|
|
84
|
+
raise SaamfiConnectionError(f"Error searching users by documents: {exc}") from exc
|
|
85
|
+
|
|
86
|
+
if response.status_code == 401:
|
|
87
|
+
logger.error("Authentication error when searching users by documents")
|
|
88
|
+
raise SaamfiAuthenticationError("Error authenticating")
|
|
89
|
+
|
|
90
|
+
if response.status_code != 200:
|
|
91
|
+
logger.error(
|
|
92
|
+
"Error searching users by documents %s: %s",
|
|
93
|
+
user_documents,
|
|
94
|
+
response.status_code,
|
|
95
|
+
)
|
|
96
|
+
raise SaamfiConnectionError("Error")
|
|
97
|
+
|
|
98
|
+
users = response.json()
|
|
99
|
+
logger.info("Found %s users by documents", len(users))
|
|
100
|
+
return users
|
|
101
|
+
|
|
102
|
+
def get_users_from_list(self, auth_token: str, user_ids: List[int]) -> Optional[str]:
|
|
103
|
+
"""Retrieve multiple users by ID list."""
|
|
104
|
+
headers = self._json_headers(auth_token)
|
|
105
|
+
try:
|
|
106
|
+
response = self._post(
|
|
107
|
+
"users/users-from-list",
|
|
108
|
+
json=user_ids,
|
|
109
|
+
headers=headers,
|
|
110
|
+
timeout=30,
|
|
111
|
+
)
|
|
112
|
+
except Exception as exc:
|
|
113
|
+
logger.warning("Error retrieving users from list: %s", exc)
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
if self._is_success(response.status_code):
|
|
117
|
+
logger.info("Retrieved users from list of %s IDs", len(user_ids))
|
|
118
|
+
return response.text
|
|
119
|
+
|
|
120
|
+
logger.warning("Failed to retrieve users from list: %s", response.status_code)
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
def get_users_by_param_and_value(self, auth_token: str, param: str, value: str) -> Optional[str]:
|
|
124
|
+
"""Retrieve users using the generic param/value endpoint."""
|
|
125
|
+
headers = self._authorization_header(auth_token)
|
|
126
|
+
try:
|
|
127
|
+
response = self._get(
|
|
128
|
+
f"users?param={param}&value={value}",
|
|
129
|
+
headers=headers,
|
|
130
|
+
timeout=30,
|
|
131
|
+
)
|
|
132
|
+
except Exception as exc:
|
|
133
|
+
logger.warning("Error retrieving users by parameter: %s", exc)
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
if self._is_success(response.status_code):
|
|
137
|
+
logger.info("Retrieved users by param '%s' and value '%s'", param, value)
|
|
138
|
+
return response.text
|
|
139
|
+
|
|
140
|
+
logger.warning(
|
|
141
|
+
"Failed to retrieve users by param '%s' and value '%s': %s",
|
|
142
|
+
param,
|
|
143
|
+
value,
|
|
144
|
+
response.status_code,
|
|
145
|
+
)
|
|
146
|
+
return None
|
|
147
|
+
|