sentinel-python-client 2.0.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.
- sentinel/__init__.py +69 -0
- sentinel/_async_client.py +56 -0
- sentinel/_client.py +56 -0
- sentinel/_exceptions.py +54 -0
- sentinel/_http.py +192 -0
- sentinel/_replay.py +32 -0
- sentinel/_signature.py +119 -0
- sentinel/models/__init__.py +24 -0
- sentinel/models/license.py +63 -0
- sentinel/models/page.py +15 -0
- sentinel/models/requests.py +184 -0
- sentinel/models/validation.py +102 -0
- sentinel/services/__init__.py +1 -0
- sentinel/services/_async_license.py +172 -0
- sentinel/services/_async_operations.py +80 -0
- sentinel/services/_license.py +193 -0
- sentinel/services/_operations.py +74 -0
- sentinel/util/__init__.py +5 -0
- sentinel/util/fingerprint.py +383 -0
- sentinel/util/public_ip.py +29 -0
- sentinel_python_client-2.0.0.dist-info/METADATA +41 -0
- sentinel_python_client-2.0.0.dist-info/RECORD +24 -0
- sentinel_python_client-2.0.0.dist-info/WHEEL +4 -0
- sentinel_python_client-2.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, fields
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from sentinel.models.license import SubUser
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _ClearValue:
|
|
12
|
+
"""Sentinel indicating a field should be cleared in the API."""
|
|
13
|
+
|
|
14
|
+
_instance: _ClearValue | None = None
|
|
15
|
+
|
|
16
|
+
def __new__(cls) -> _ClearValue:
|
|
17
|
+
if cls._instance is None:
|
|
18
|
+
cls._instance = super().__new__(cls)
|
|
19
|
+
return cls._instance
|
|
20
|
+
|
|
21
|
+
def __repr__(self) -> str:
|
|
22
|
+
return "CLEAR"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
CLEAR: Any = _ClearValue()
|
|
26
|
+
"""Pass as a field value in :class:`UpdateLicenseRequest` to clear the stored value.
|
|
27
|
+
Sends the appropriate empty/reset value per field type (empty string for text fields,
|
|
28
|
+
empty collection for collection fields, epoch for expiration)."""
|
|
29
|
+
|
|
30
|
+
CLEAR_EXPIRATION = datetime(1970, 1, 1, tzinfo=UTC)
|
|
31
|
+
"""Pass as ``expiration`` in :class:`UpdateLicenseRequest` to clear a license's expiration
|
|
32
|
+
(i.e. set it to never expire). The API interprets the Unix epoch as "no expiration"."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class CreateLicenseRequest:
|
|
37
|
+
product: str
|
|
38
|
+
key: str | None = None
|
|
39
|
+
tier: str | None = None
|
|
40
|
+
expiration: datetime | None = None
|
|
41
|
+
max_servers: int | None = None
|
|
42
|
+
max_ips: int | None = None
|
|
43
|
+
note: str | None = None
|
|
44
|
+
connections: dict[str, str] | None = None
|
|
45
|
+
additional_entitlements: set[str] | None = None
|
|
46
|
+
|
|
47
|
+
def to_body(self) -> dict[str, Any]:
|
|
48
|
+
body: dict[str, Any] = {"product": self.product}
|
|
49
|
+
if self.key is not None:
|
|
50
|
+
body["key"] = self.key
|
|
51
|
+
if self.tier is not None:
|
|
52
|
+
body["tier"] = self.tier
|
|
53
|
+
if self.expiration is not None:
|
|
54
|
+
body["expiration"] = self.expiration.isoformat()
|
|
55
|
+
if self.max_servers is not None:
|
|
56
|
+
body["maxServers"] = self.max_servers
|
|
57
|
+
if self.max_ips is not None:
|
|
58
|
+
body["maxIps"] = self.max_ips
|
|
59
|
+
if self.note is not None:
|
|
60
|
+
body["note"] = self.note
|
|
61
|
+
if self.connections is not None:
|
|
62
|
+
body["connections"] = self.connections
|
|
63
|
+
if self.additional_entitlements is not None:
|
|
64
|
+
body["additionalEntitlements"] = list(self.additional_entitlements)
|
|
65
|
+
return body
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
_CLEAR_JSON_VALUES: dict[str, Any] = {
|
|
69
|
+
"connections": {},
|
|
70
|
+
"subUsers": [],
|
|
71
|
+
"servers": [],
|
|
72
|
+
"ips": [],
|
|
73
|
+
"additionalEntitlements": [],
|
|
74
|
+
"expiration": datetime(1970, 1, 1, tzinfo=UTC).isoformat(),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
_FIELD_TO_JSON: dict[str, str] = {
|
|
78
|
+
"product": "product",
|
|
79
|
+
"tier": "tier",
|
|
80
|
+
"expiration": "expiration",
|
|
81
|
+
"max_servers": "maxServers",
|
|
82
|
+
"max_ips": "maxIps",
|
|
83
|
+
"note": "note",
|
|
84
|
+
"blacklist_reason": "blacklistReason",
|
|
85
|
+
"connections": "connections",
|
|
86
|
+
"sub_users": "subUsers",
|
|
87
|
+
"servers": "servers",
|
|
88
|
+
"ips": "ips",
|
|
89
|
+
"additional_entitlements": "additionalEntitlements",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@dataclass
|
|
94
|
+
class UpdateLicenseRequest:
|
|
95
|
+
product: str | None = None
|
|
96
|
+
tier: str | None = None
|
|
97
|
+
expiration: datetime | None = None
|
|
98
|
+
max_servers: int | None = None
|
|
99
|
+
max_ips: int | None = None
|
|
100
|
+
note: str | None = None
|
|
101
|
+
blacklist_reason: str | None = None
|
|
102
|
+
connections: dict[str, str] | None = None
|
|
103
|
+
sub_users: list[SubUser] | None = None
|
|
104
|
+
servers: set[str] | None = None
|
|
105
|
+
ips: set[str] | None = None
|
|
106
|
+
additional_entitlements: set[str] | None = None
|
|
107
|
+
|
|
108
|
+
def to_body(self) -> dict[str, Any]:
|
|
109
|
+
body: dict[str, Any] = {}
|
|
110
|
+
for f in fields(self):
|
|
111
|
+
value = getattr(self, f.name)
|
|
112
|
+
json_key = _FIELD_TO_JSON.get(f.name, f.name)
|
|
113
|
+
if isinstance(value, _ClearValue):
|
|
114
|
+
body[json_key] = _CLEAR_JSON_VALUES.get(json_key, "")
|
|
115
|
+
elif value is not None:
|
|
116
|
+
if isinstance(value, datetime):
|
|
117
|
+
body[json_key] = value.isoformat()
|
|
118
|
+
elif isinstance(value, set):
|
|
119
|
+
body[json_key] = list(value)
|
|
120
|
+
elif isinstance(value, list):
|
|
121
|
+
body[json_key] = [
|
|
122
|
+
item.model_dump() if hasattr(item, "model_dump") else item
|
|
123
|
+
for item in value
|
|
124
|
+
]
|
|
125
|
+
else:
|
|
126
|
+
body[json_key] = value
|
|
127
|
+
return body
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass(frozen=True)
|
|
131
|
+
class ListLicensesRequest:
|
|
132
|
+
product: str | None = None
|
|
133
|
+
status: str | None = None
|
|
134
|
+
query: str | None = None
|
|
135
|
+
platform: str | None = None
|
|
136
|
+
value: str | None = None
|
|
137
|
+
server: str | None = None
|
|
138
|
+
ip: str | None = None
|
|
139
|
+
page: int = 0
|
|
140
|
+
size: int = 50
|
|
141
|
+
|
|
142
|
+
def to_query_params(self) -> dict[str, str]:
|
|
143
|
+
params: dict[str, str] = {}
|
|
144
|
+
if self.product is not None:
|
|
145
|
+
params["product"] = self.product
|
|
146
|
+
if self.status is not None:
|
|
147
|
+
params["status"] = self.status
|
|
148
|
+
if self.query is not None:
|
|
149
|
+
params["query"] = self.query
|
|
150
|
+
if self.platform is not None:
|
|
151
|
+
params["platform"] = self.platform
|
|
152
|
+
if self.value is not None:
|
|
153
|
+
params["value"] = self.value
|
|
154
|
+
if self.server is not None:
|
|
155
|
+
params["server"] = self.server
|
|
156
|
+
if self.ip is not None:
|
|
157
|
+
params["ip"] = self.ip
|
|
158
|
+
params["page"] = str(self.page)
|
|
159
|
+
params["size"] = str(self.size)
|
|
160
|
+
return params
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass(frozen=True)
|
|
164
|
+
class ValidationRequest:
|
|
165
|
+
product: str
|
|
166
|
+
key: str | None = None
|
|
167
|
+
server: str | None = None
|
|
168
|
+
ip: str | None = None
|
|
169
|
+
connection_platform: str | None = None
|
|
170
|
+
connection_value: str | None = None
|
|
171
|
+
|
|
172
|
+
def to_body(self) -> dict[str, str]:
|
|
173
|
+
body: dict[str, str] = {"product": self.product}
|
|
174
|
+
if self.key is not None:
|
|
175
|
+
body["key"] = self.key
|
|
176
|
+
if self.server is not None:
|
|
177
|
+
body["server"] = self.server
|
|
178
|
+
if self.ip is not None:
|
|
179
|
+
body["ip"] = self.ip
|
|
180
|
+
if self.connection_platform is not None:
|
|
181
|
+
body["connectionPlatform"] = self.connection_platform
|
|
182
|
+
if self.connection_value is not None:
|
|
183
|
+
body["connectionValue"] = self.connection_value
|
|
184
|
+
return body
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from enum import StrEnum
|
|
6
|
+
|
|
7
|
+
from sentinel._exceptions import LicenseValidationError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ValidationResultType(StrEnum):
|
|
11
|
+
SUCCESS = "SUCCESS"
|
|
12
|
+
INVALID_PRODUCT = "INVALID_PRODUCT"
|
|
13
|
+
INVALID_LICENSE = "INVALID_LICENSE"
|
|
14
|
+
INVALID_PLATFORM = "INVALID_PLATFORM"
|
|
15
|
+
EXPIRED_LICENSE = "EXPIRED_LICENSE"
|
|
16
|
+
BLACKLISTED_LICENSE = "BLACKLISTED_LICENSE"
|
|
17
|
+
CONNECTION_MISMATCH = "CONNECTION_MISMATCH"
|
|
18
|
+
EXCESSIVE_SERVERS = "EXCESSIVE_SERVERS"
|
|
19
|
+
EXCESSIVE_IPS = "EXCESSIVE_IPS"
|
|
20
|
+
UNKNOWN = "UNKNOWN"
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def from_string(value: str | None) -> ValidationResultType:
|
|
24
|
+
if value is None:
|
|
25
|
+
return ValidationResultType.UNKNOWN
|
|
26
|
+
try:
|
|
27
|
+
return ValidationResultType(value)
|
|
28
|
+
except ValueError:
|
|
29
|
+
return ValidationResultType.UNKNOWN
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class ValidationDetails:
|
|
34
|
+
expiration: datetime | None
|
|
35
|
+
server_count: int
|
|
36
|
+
max_servers: int
|
|
37
|
+
ip_count: int
|
|
38
|
+
max_ips: int
|
|
39
|
+
tier: str | None
|
|
40
|
+
entitlements: set[str]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class FailureDetails:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class BlacklistFailureDetails(FailureDetails):
|
|
50
|
+
timestamp: datetime
|
|
51
|
+
reason: str | None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(frozen=True)
|
|
55
|
+
class ExcessiveServersFailureDetails(FailureDetails):
|
|
56
|
+
max_servers: int
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class ExcessiveIpsFailureDetails(FailureDetails):
|
|
61
|
+
max_ips: int
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class ValidationResult:
|
|
66
|
+
type: ValidationResultType
|
|
67
|
+
message: str
|
|
68
|
+
is_valid: bool
|
|
69
|
+
details: ValidationDetails | None = None
|
|
70
|
+
failure_details: FailureDetails | None = None
|
|
71
|
+
|
|
72
|
+
@staticmethod
|
|
73
|
+
def success(details: ValidationDetails, message: str) -> ValidationResult:
|
|
74
|
+
return ValidationResult(
|
|
75
|
+
type=ValidationResultType.SUCCESS,
|
|
76
|
+
message=message,
|
|
77
|
+
is_valid=True,
|
|
78
|
+
details=details,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
@staticmethod
|
|
82
|
+
def failure(
|
|
83
|
+
type: ValidationResultType,
|
|
84
|
+
message: str,
|
|
85
|
+
failure_details: FailureDetails | None = None,
|
|
86
|
+
) -> ValidationResult:
|
|
87
|
+
return ValidationResult(
|
|
88
|
+
type=type,
|
|
89
|
+
message=message,
|
|
90
|
+
is_valid=False,
|
|
91
|
+
failure_details=failure_details,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def require_valid(self) -> ValidationDetails:
|
|
95
|
+
if not self.is_valid:
|
|
96
|
+
raise LicenseValidationError(
|
|
97
|
+
type=self.type,
|
|
98
|
+
message=self.message,
|
|
99
|
+
failure_details=self.failure_details,
|
|
100
|
+
)
|
|
101
|
+
assert self.details is not None
|
|
102
|
+
return self.details
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Sentinel API services."""
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
|
|
5
|
+
from sentinel._exceptions import SentinelApiError
|
|
6
|
+
from sentinel._http import ApiResponse, AsyncSentinelHttpClient
|
|
7
|
+
from sentinel._replay import ReplayProtector
|
|
8
|
+
from sentinel._signature import SignatureVerifier
|
|
9
|
+
from sentinel.models.license import License
|
|
10
|
+
from sentinel.models.page import Page
|
|
11
|
+
from sentinel.models.requests import (
|
|
12
|
+
CreateLicenseRequest,
|
|
13
|
+
ListLicensesRequest,
|
|
14
|
+
UpdateLicenseRequest,
|
|
15
|
+
ValidationRequest,
|
|
16
|
+
)
|
|
17
|
+
from sentinel.models.validation import (
|
|
18
|
+
ValidationDetails,
|
|
19
|
+
ValidationResult,
|
|
20
|
+
ValidationResultType,
|
|
21
|
+
)
|
|
22
|
+
from sentinel.services._async_operations import (
|
|
23
|
+
AsyncLicenseConnectionOperations,
|
|
24
|
+
AsyncLicenseIpOperations,
|
|
25
|
+
AsyncLicenseServerOperations,
|
|
26
|
+
AsyncLicenseSubUserOperations,
|
|
27
|
+
)
|
|
28
|
+
from sentinel.services._license import _parse_failure_details
|
|
29
|
+
|
|
30
|
+
_BASE_PATH = "/api/v2/licenses"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AsyncLicenseService:
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
http_client: AsyncSentinelHttpClient,
|
|
37
|
+
signature_verifier: SignatureVerifier | None = None,
|
|
38
|
+
replay_protector: ReplayProtector | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
self._http = http_client
|
|
41
|
+
self._sig = signature_verifier
|
|
42
|
+
self._replay = replay_protector
|
|
43
|
+
self.connections = AsyncLicenseConnectionOperations(http_client)
|
|
44
|
+
self.servers = AsyncLicenseServerOperations(http_client)
|
|
45
|
+
self.ips = AsyncLicenseIpOperations(http_client)
|
|
46
|
+
self.sub_users = AsyncLicenseSubUserOperations(http_client)
|
|
47
|
+
|
|
48
|
+
async def validate(self, request: ValidationRequest) -> ValidationResult:
|
|
49
|
+
body = request.to_body()
|
|
50
|
+
if "server" not in body:
|
|
51
|
+
import asyncio
|
|
52
|
+
|
|
53
|
+
from sentinel.util.fingerprint import generate_fingerprint
|
|
54
|
+
|
|
55
|
+
body["server"] = await asyncio.to_thread(generate_fingerprint)
|
|
56
|
+
|
|
57
|
+
resp = await self._http.request(
|
|
58
|
+
"POST", f"{_BASE_PATH}/validate", json_body=body, allowed_statuses={403}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if resp.http_status == 200:
|
|
62
|
+
return self._parse_validation_success(resp)
|
|
63
|
+
if resp.http_status == 403:
|
|
64
|
+
return self._parse_validation_failure(resp)
|
|
65
|
+
raise SentinelApiError(
|
|
66
|
+
http_status=resp.http_status,
|
|
67
|
+
type=resp.type,
|
|
68
|
+
message=resp.message or "Unknown error",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
async def create(self, request: CreateLicenseRequest) -> License:
|
|
72
|
+
resp = await self._http.request("POST", _BASE_PATH, json_body=request.to_body())
|
|
73
|
+
return License.model_validate(resp.require_result()["license"])
|
|
74
|
+
|
|
75
|
+
async def get(self, key: str) -> License:
|
|
76
|
+
resp = await self._http.request("GET", f"{_BASE_PATH}/{key}")
|
|
77
|
+
return License.model_validate(resp.require_result()["license"])
|
|
78
|
+
|
|
79
|
+
async def list(self, request: ListLicensesRequest) -> Page[License]:
|
|
80
|
+
resp = await self._http.request("GET", _BASE_PATH, query_params=request.to_query_params())
|
|
81
|
+
page_data = resp.require_result()["page"]
|
|
82
|
+
licenses = [License.model_validate(item) for item in page_data["content"]]
|
|
83
|
+
meta = page_data["page"]
|
|
84
|
+
return Page(
|
|
85
|
+
content=licenses,
|
|
86
|
+
size=meta["size"],
|
|
87
|
+
number=meta["number"],
|
|
88
|
+
total_elements=meta["totalElements"],
|
|
89
|
+
total_pages=meta["totalPages"],
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
async def update(self, key: str, request: UpdateLicenseRequest) -> License:
|
|
93
|
+
resp = await self._http.request(
|
|
94
|
+
"PATCH", f"{_BASE_PATH}/{key}", json_body=request.to_body()
|
|
95
|
+
)
|
|
96
|
+
return License.model_validate(resp.require_result()["license"])
|
|
97
|
+
|
|
98
|
+
async def delete(self, key: str) -> None:
|
|
99
|
+
await self._http.request("DELETE", f"{_BASE_PATH}/{key}")
|
|
100
|
+
|
|
101
|
+
async def regenerate_key(self, key: str, new_key: str | None = None) -> License:
|
|
102
|
+
params = {"newKey": new_key} if new_key else None
|
|
103
|
+
resp = await self._http.request(
|
|
104
|
+
"POST", f"{_BASE_PATH}/{key}/regenerate-key", query_params=params
|
|
105
|
+
)
|
|
106
|
+
return License.model_validate(resp.require_result()["license"])
|
|
107
|
+
|
|
108
|
+
def _parse_validation_success(self, resp: ApiResponse) -> ValidationResult:
|
|
109
|
+
validation = resp.require_result()["validation"]
|
|
110
|
+
nonce = validation["nonce"]
|
|
111
|
+
timestamp = validation["timestamp"]
|
|
112
|
+
signature = validation.get("signature")
|
|
113
|
+
details_data = validation["details"]
|
|
114
|
+
|
|
115
|
+
expiration_str = details_data.get("expiration")
|
|
116
|
+
expiration = datetime.fromisoformat(expiration_str) if expiration_str else None
|
|
117
|
+
server_count = details_data["serverCount"]
|
|
118
|
+
max_servers = details_data["maxServers"]
|
|
119
|
+
ip_count = details_data["ipCount"]
|
|
120
|
+
max_ips = details_data["maxIps"]
|
|
121
|
+
tier = details_data.get("tier")
|
|
122
|
+
|
|
123
|
+
raw_entitlements = details_data.get("entitlements")
|
|
124
|
+
entitlements_list: list[str] | None = None
|
|
125
|
+
entitlements_set: set[str] = set()
|
|
126
|
+
if raw_entitlements is not None:
|
|
127
|
+
entitlements_list = list(raw_entitlements)
|
|
128
|
+
entitlements_set = set(raw_entitlements)
|
|
129
|
+
|
|
130
|
+
details = ValidationDetails(
|
|
131
|
+
expiration=expiration,
|
|
132
|
+
server_count=server_count,
|
|
133
|
+
max_servers=max_servers,
|
|
134
|
+
ip_count=ip_count,
|
|
135
|
+
max_ips=max_ips,
|
|
136
|
+
tier=tier,
|
|
137
|
+
entitlements=entitlements_set,
|
|
138
|
+
)
|
|
139
|
+
result = ValidationResult.success(details=details, message=resp.message or "")
|
|
140
|
+
|
|
141
|
+
if self._sig is not None:
|
|
142
|
+
self._sig.verify(
|
|
143
|
+
signature_base64=signature,
|
|
144
|
+
nonce=nonce,
|
|
145
|
+
timestamp=timestamp,
|
|
146
|
+
expiration=expiration_str,
|
|
147
|
+
server_count=server_count,
|
|
148
|
+
max_servers=max_servers,
|
|
149
|
+
ip_count=ip_count,
|
|
150
|
+
max_ips=max_ips,
|
|
151
|
+
tier=tier,
|
|
152
|
+
entitlements=entitlements_list,
|
|
153
|
+
)
|
|
154
|
+
if self._replay is not None:
|
|
155
|
+
self._replay.check(nonce, timestamp)
|
|
156
|
+
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
def _parse_validation_failure(self, resp: ApiResponse) -> ValidationResult:
|
|
160
|
+
result_type = ValidationResultType.from_string(resp.type)
|
|
161
|
+
if result_type == ValidationResultType.UNKNOWN:
|
|
162
|
+
raise SentinelApiError(
|
|
163
|
+
http_status=resp.http_status,
|
|
164
|
+
type=resp.type,
|
|
165
|
+
message=resp.message or "Unknown error",
|
|
166
|
+
)
|
|
167
|
+
failure_details = _parse_failure_details(result_type, resp.result)
|
|
168
|
+
return ValidationResult.failure(
|
|
169
|
+
type=result_type,
|
|
170
|
+
message=resp.message or "",
|
|
171
|
+
failure_details=failure_details,
|
|
172
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from sentinel._http import AsyncSentinelHttpClient
|
|
4
|
+
from sentinel.models.license import License, SubUser
|
|
5
|
+
|
|
6
|
+
_BASE_PATH = "/api/v2/licenses"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AsyncLicenseConnectionOperations:
|
|
10
|
+
def __init__(self, http_client: AsyncSentinelHttpClient) -> None:
|
|
11
|
+
self._http = http_client
|
|
12
|
+
|
|
13
|
+
async def add(self, key: str, connections: dict[str, str]) -> License:
|
|
14
|
+
resp = await self._http.request(
|
|
15
|
+
"POST", f"{_BASE_PATH}/{key}/connections", json_body=connections
|
|
16
|
+
)
|
|
17
|
+
return License.model_validate(resp.require_result()["license"])
|
|
18
|
+
|
|
19
|
+
async def remove(self, key: str, platforms: set[str]) -> License:
|
|
20
|
+
resp = await self._http.request(
|
|
21
|
+
"DELETE",
|
|
22
|
+
f"{_BASE_PATH}/{key}/connections",
|
|
23
|
+
multi_query_params={"platforms": sorted(platforms)},
|
|
24
|
+
)
|
|
25
|
+
return License.model_validate(resp.require_result()["license"])
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AsyncLicenseServerOperations:
|
|
29
|
+
def __init__(self, http_client: AsyncSentinelHttpClient) -> None:
|
|
30
|
+
self._http = http_client
|
|
31
|
+
|
|
32
|
+
async def add(self, key: str, identifiers: set[str]) -> License:
|
|
33
|
+
resp = await self._http.request(
|
|
34
|
+
"POST", f"{_BASE_PATH}/{key}/servers", json_body=sorted(identifiers)
|
|
35
|
+
)
|
|
36
|
+
return License.model_validate(resp.require_result()["license"])
|
|
37
|
+
|
|
38
|
+
async def remove(self, key: str, identifiers: set[str]) -> License:
|
|
39
|
+
resp = await self._http.request(
|
|
40
|
+
"DELETE",
|
|
41
|
+
f"{_BASE_PATH}/{key}/servers",
|
|
42
|
+
multi_query_params={"servers": sorted(identifiers)},
|
|
43
|
+
)
|
|
44
|
+
return License.model_validate(resp.require_result()["license"])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AsyncLicenseIpOperations:
|
|
48
|
+
def __init__(self, http_client: AsyncSentinelHttpClient) -> None:
|
|
49
|
+
self._http = http_client
|
|
50
|
+
|
|
51
|
+
async def add(self, key: str, addresses: set[str]) -> License:
|
|
52
|
+
resp = await self._http.request(
|
|
53
|
+
"POST", f"{_BASE_PATH}/{key}/ips", json_body=sorted(addresses)
|
|
54
|
+
)
|
|
55
|
+
return License.model_validate(resp.require_result()["license"])
|
|
56
|
+
|
|
57
|
+
async def remove(self, key: str, addresses: set[str]) -> License:
|
|
58
|
+
resp = await self._http.request(
|
|
59
|
+
"DELETE",
|
|
60
|
+
f"{_BASE_PATH}/{key}/ips",
|
|
61
|
+
multi_query_params={"ips": sorted(addresses)},
|
|
62
|
+
)
|
|
63
|
+
return License.model_validate(resp.require_result()["license"])
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class AsyncLicenseSubUserOperations:
|
|
67
|
+
def __init__(self, http_client: AsyncSentinelHttpClient) -> None:
|
|
68
|
+
self._http = http_client
|
|
69
|
+
|
|
70
|
+
async def add(self, key: str, sub_users: list[SubUser]) -> License:
|
|
71
|
+
body = [su.model_dump() for su in sub_users]
|
|
72
|
+
resp = await self._http.request("POST", f"{_BASE_PATH}/{key}/sub-users", json_body=body)
|
|
73
|
+
return License.model_validate(resp.require_result()["license"])
|
|
74
|
+
|
|
75
|
+
async def remove(self, key: str, sub_users: list[SubUser]) -> License:
|
|
76
|
+
body = [su.model_dump() for su in sub_users]
|
|
77
|
+
resp = await self._http.request(
|
|
78
|
+
"POST", f"{_BASE_PATH}/{key}/sub-users/remove", json_body=body
|
|
79
|
+
)
|
|
80
|
+
return License.model_validate(resp.require_result()["license"])
|