binalyze-air-sdk 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- binalyze_air/__init__.py +77 -0
- binalyze_air/apis/__init__.py +27 -0
- binalyze_air/apis/authentication.py +27 -0
- binalyze_air/apis/auto_asset_tags.py +75 -0
- binalyze_air/apis/endpoints.py +22 -0
- binalyze_air/apis/event_subscription.py +97 -0
- binalyze_air/apis/evidence.py +53 -0
- binalyze_air/apis/evidences.py +216 -0
- binalyze_air/apis/interact.py +36 -0
- binalyze_air/apis/params.py +40 -0
- binalyze_air/apis/settings.py +27 -0
- binalyze_air/apis/user_management.py +74 -0
- binalyze_air/apis/users.py +68 -0
- binalyze_air/apis/webhooks.py +231 -0
- binalyze_air/base.py +133 -0
- binalyze_air/client.py +1338 -0
- binalyze_air/commands/__init__.py +146 -0
- binalyze_air/commands/acquisitions.py +387 -0
- binalyze_air/commands/assets.py +363 -0
- binalyze_air/commands/authentication.py +37 -0
- binalyze_air/commands/auto_asset_tags.py +231 -0
- binalyze_air/commands/baseline.py +396 -0
- binalyze_air/commands/cases.py +603 -0
- binalyze_air/commands/event_subscription.py +102 -0
- binalyze_air/commands/evidences.py +988 -0
- binalyze_air/commands/interact.py +58 -0
- binalyze_air/commands/organizations.py +221 -0
- binalyze_air/commands/policies.py +203 -0
- binalyze_air/commands/settings.py +29 -0
- binalyze_air/commands/tasks.py +56 -0
- binalyze_air/commands/triage.py +360 -0
- binalyze_air/commands/user_management.py +126 -0
- binalyze_air/commands/users.py +101 -0
- binalyze_air/config.py +245 -0
- binalyze_air/exceptions.py +50 -0
- binalyze_air/http_client.py +306 -0
- binalyze_air/models/__init__.py +285 -0
- binalyze_air/models/acquisitions.py +251 -0
- binalyze_air/models/assets.py +439 -0
- binalyze_air/models/audit.py +273 -0
- binalyze_air/models/authentication.py +70 -0
- binalyze_air/models/auto_asset_tags.py +117 -0
- binalyze_air/models/baseline.py +232 -0
- binalyze_air/models/cases.py +276 -0
- binalyze_air/models/endpoints.py +76 -0
- binalyze_air/models/event_subscription.py +172 -0
- binalyze_air/models/evidence.py +66 -0
- binalyze_air/models/evidences.py +349 -0
- binalyze_air/models/interact.py +136 -0
- binalyze_air/models/organizations.py +294 -0
- binalyze_air/models/params.py +128 -0
- binalyze_air/models/policies.py +250 -0
- binalyze_air/models/settings.py +84 -0
- binalyze_air/models/tasks.py +149 -0
- binalyze_air/models/triage.py +143 -0
- binalyze_air/models/user_management.py +97 -0
- binalyze_air/models/users.py +82 -0
- binalyze_air/queries/__init__.py +134 -0
- binalyze_air/queries/acquisitions.py +156 -0
- binalyze_air/queries/assets.py +105 -0
- binalyze_air/queries/audit.py +417 -0
- binalyze_air/queries/authentication.py +56 -0
- binalyze_air/queries/auto_asset_tags.py +60 -0
- binalyze_air/queries/baseline.py +185 -0
- binalyze_air/queries/cases.py +293 -0
- binalyze_air/queries/endpoints.py +25 -0
- binalyze_air/queries/event_subscription.py +55 -0
- binalyze_air/queries/evidence.py +140 -0
- binalyze_air/queries/evidences.py +280 -0
- binalyze_air/queries/interact.py +28 -0
- binalyze_air/queries/organizations.py +223 -0
- binalyze_air/queries/params.py +115 -0
- binalyze_air/queries/policies.py +150 -0
- binalyze_air/queries/settings.py +20 -0
- binalyze_air/queries/tasks.py +82 -0
- binalyze_air/queries/triage.py +231 -0
- binalyze_air/queries/user_management.py +83 -0
- binalyze_air/queries/users.py +69 -0
- binalyze_air_sdk-1.0.1.dist-info/METADATA +635 -0
- binalyze_air_sdk-1.0.1.dist-info/RECORD +82 -0
- binalyze_air_sdk-1.0.1.dist-info/WHEEL +5 -0
- binalyze_air_sdk-1.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
"""
|
2
|
+
Settings API for the Binalyze AIR SDK.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from ..http_client import HTTPClient
|
6
|
+
from ..models.settings import BannerSettings, UpdateBannerSettingsRequest
|
7
|
+
from ..queries.settings import GetBannerSettingsQuery
|
8
|
+
from ..commands.settings import UpdateBannerSettingsCommand
|
9
|
+
|
10
|
+
|
11
|
+
class SettingsAPI:
|
12
|
+
"""Settings API with CQRS pattern - separated queries and commands."""
|
13
|
+
|
14
|
+
def __init__(self, http_client: HTTPClient):
|
15
|
+
self.http_client = http_client
|
16
|
+
|
17
|
+
# QUERIES (Read operations)
|
18
|
+
def get_banner_settings(self) -> BannerSettings:
|
19
|
+
"""Get current banner settings."""
|
20
|
+
query = GetBannerSettingsQuery(self.http_client)
|
21
|
+
return query.execute()
|
22
|
+
|
23
|
+
# COMMANDS (Write operations)
|
24
|
+
def update_banner_settings(self, request: UpdateBannerSettingsRequest) -> BannerSettings:
|
25
|
+
"""Update banner settings."""
|
26
|
+
command = UpdateBannerSettingsCommand(self.http_client, request)
|
27
|
+
return command.execute()
|
@@ -0,0 +1,74 @@
|
|
1
|
+
"""
|
2
|
+
User Management API for the Binalyze AIR SDK.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import List, Optional, Dict, Any
|
6
|
+
|
7
|
+
from ..http_client import HTTPClient
|
8
|
+
from ..models.user_management import (
|
9
|
+
UserManagementUser, UserFilter, CreateUserRequest, UpdateUserRequest,
|
10
|
+
AIUser, CreateAIUserRequest, APIUser, CreateAPIUserRequest
|
11
|
+
)
|
12
|
+
from ..queries.user_management import (
|
13
|
+
ListUsersQuery, GetUserQuery, GetAIUserQuery, GetAPIUserQuery
|
14
|
+
)
|
15
|
+
from ..commands.user_management import (
|
16
|
+
CreateUserCommand, UpdateUserCommand, DeleteUserCommand,
|
17
|
+
CreateAIUserCommand, CreateAPIUserCommand
|
18
|
+
)
|
19
|
+
|
20
|
+
|
21
|
+
class UserManagementAPI:
|
22
|
+
"""User Management API with CQRS pattern - separated queries and commands."""
|
23
|
+
|
24
|
+
def __init__(self, http_client: HTTPClient):
|
25
|
+
self.http_client = http_client
|
26
|
+
|
27
|
+
# USER QUERIES (Read operations)
|
28
|
+
def list_users(self, filter_params: Optional[UserFilter] = None) -> List[UserManagementUser]:
|
29
|
+
"""List users with optional filtering."""
|
30
|
+
query = ListUsersQuery(self.http_client, filter_params)
|
31
|
+
return query.execute()
|
32
|
+
|
33
|
+
def get_user(self, user_id: str) -> UserManagementUser:
|
34
|
+
"""Get a specific user by ID."""
|
35
|
+
query = GetUserQuery(self.http_client, user_id)
|
36
|
+
return query.execute()
|
37
|
+
|
38
|
+
# USER COMMANDS (Write operations)
|
39
|
+
def create_user(self, request: CreateUserRequest) -> UserManagementUser:
|
40
|
+
"""Create a new user."""
|
41
|
+
command = CreateUserCommand(self.http_client, request)
|
42
|
+
return command.execute()
|
43
|
+
|
44
|
+
def update_user(self, user_id: str, request: UpdateUserRequest) -> UserManagementUser:
|
45
|
+
"""Update an existing user."""
|
46
|
+
command = UpdateUserCommand(self.http_client, user_id, request)
|
47
|
+
return command.execute()
|
48
|
+
|
49
|
+
def delete_user(self, user_id: str) -> Dict[str, Any]:
|
50
|
+
"""Delete a user."""
|
51
|
+
command = DeleteUserCommand(self.http_client, user_id)
|
52
|
+
return command.execute()
|
53
|
+
|
54
|
+
# AI USER OPERATIONS
|
55
|
+
def get_ai_user(self) -> AIUser:
|
56
|
+
"""Get the AI user."""
|
57
|
+
query = GetAIUserQuery(self.http_client)
|
58
|
+
return query.execute()
|
59
|
+
|
60
|
+
def create_ai_user(self, request: CreateAIUserRequest) -> AIUser:
|
61
|
+
"""Create a new AI user."""
|
62
|
+
command = CreateAIUserCommand(self.http_client, request)
|
63
|
+
return command.execute()
|
64
|
+
|
65
|
+
# API USER OPERATIONS
|
66
|
+
def get_api_user(self) -> APIUser:
|
67
|
+
"""Get the API user."""
|
68
|
+
query = GetAPIUserQuery(self.http_client)
|
69
|
+
return query.execute()
|
70
|
+
|
71
|
+
def create_api_user(self, request: CreateAPIUserRequest) -> APIUser:
|
72
|
+
"""Create a new API user."""
|
73
|
+
command = CreateAPIUserCommand(self.http_client, request)
|
74
|
+
return command.execute()
|
@@ -0,0 +1,68 @@
|
|
1
|
+
"""
|
2
|
+
Users API for the Binalyze AIR SDK.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import List, Optional, Dict, Any
|
6
|
+
|
7
|
+
from ..http_client import HTTPClient
|
8
|
+
from ..models.users import (
|
9
|
+
User, UserFilter, CreateUserRequest, UpdateUserRequest,
|
10
|
+
APIUser, CreateAPIUserRequest
|
11
|
+
)
|
12
|
+
from ..queries.users import (
|
13
|
+
ListUsersQuery, GetUserQuery
|
14
|
+
)
|
15
|
+
from ..commands.users import (
|
16
|
+
CreateUserCommand, UpdateUserCommand, DeleteUserCommand,
|
17
|
+
CreateAPIUserCommand
|
18
|
+
)
|
19
|
+
|
20
|
+
|
21
|
+
class UsersAPI:
|
22
|
+
"""Users API with CQRS pattern - separated queries and commands."""
|
23
|
+
|
24
|
+
def __init__(self, http_client: HTTPClient):
|
25
|
+
self.http_client = http_client
|
26
|
+
|
27
|
+
# USER QUERIES (Read operations)
|
28
|
+
def list(self, page_number: int = 1, page_size: int = 10,
|
29
|
+
sort_by: str = "createdAt", sort_type: str = "ASC") -> List[User]:
|
30
|
+
"""List users with pagination support."""
|
31
|
+
query = ListUsersQuery(self.http_client, page_number, page_size, sort_by, sort_type)
|
32
|
+
return query.execute()
|
33
|
+
|
34
|
+
def get(self, user_id: str) -> User:
|
35
|
+
"""Get a specific user by ID."""
|
36
|
+
query = GetUserQuery(self.http_client, user_id)
|
37
|
+
return query.execute()
|
38
|
+
|
39
|
+
# USER COMMANDS (Write operations)
|
40
|
+
def create_user(self, request: CreateUserRequest) -> User:
|
41
|
+
"""Create a new user."""
|
42
|
+
command = CreateUserCommand(self.http_client, request)
|
43
|
+
return command.execute()
|
44
|
+
|
45
|
+
def update_user(self, user_id: str, request: UpdateUserRequest) -> User:
|
46
|
+
"""Update an existing user."""
|
47
|
+
command = UpdateUserCommand(self.http_client, user_id, request)
|
48
|
+
return command.execute()
|
49
|
+
|
50
|
+
def delete_user(self, user_id: str) -> Dict[str, Any]:
|
51
|
+
"""Delete a user."""
|
52
|
+
command = DeleteUserCommand(self.http_client, user_id)
|
53
|
+
return command.execute()
|
54
|
+
|
55
|
+
# API USER OPERATIONS
|
56
|
+
def create_api_user(self, username: str, email: str, organization_ids: List[int],
|
57
|
+
profile: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
|
58
|
+
"""Create a new API user for programmatic access."""
|
59
|
+
request_data = {
|
60
|
+
"username": username,
|
61
|
+
"email": email,
|
62
|
+
"organizationIds": organization_ids
|
63
|
+
}
|
64
|
+
if profile:
|
65
|
+
request_data["profile"] = profile
|
66
|
+
|
67
|
+
command = CreateAPIUserCommand(self.http_client, request_data)
|
68
|
+
return command.execute()
|
@@ -0,0 +1,231 @@
|
|
1
|
+
"""
|
2
|
+
Webhook API for the Binalyze AIR SDK.
|
3
|
+
Provides webhook trigger functionality for programmatically calling webhook endpoints.
|
4
|
+
"""
|
5
|
+
|
6
|
+
from typing import Dict, Any, Optional, List, Union
|
7
|
+
import json
|
8
|
+
|
9
|
+
from ..http_client import HTTPClient
|
10
|
+
|
11
|
+
|
12
|
+
class WebhookAPI:
|
13
|
+
"""Webhook API for triggering webhook endpoints programmatically."""
|
14
|
+
|
15
|
+
def __init__(self, http_client: HTTPClient):
|
16
|
+
self.http_client = http_client
|
17
|
+
|
18
|
+
def trigger_get(
|
19
|
+
self,
|
20
|
+
slug: str,
|
21
|
+
data: str,
|
22
|
+
token: str,
|
23
|
+
use_webhook_endpoint: bool = True
|
24
|
+
) -> Dict[str, Any]:
|
25
|
+
"""
|
26
|
+
Trigger a webhook via GET request.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
slug: Webhook slug/name
|
30
|
+
data: Comma-separated hostnames or IP addresses
|
31
|
+
token: Webhook token
|
32
|
+
use_webhook_endpoint: If True, use webhook endpoint directly (no auth needed)
|
33
|
+
If False, use authenticated API endpoint
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
Dict containing task details and URLs
|
37
|
+
"""
|
38
|
+
if use_webhook_endpoint:
|
39
|
+
# Direct webhook call - no authentication needed, just token
|
40
|
+
endpoint = f"webhook/{slug}/{data}"
|
41
|
+
params = {"token": token}
|
42
|
+
|
43
|
+
# Use raw HTTP client for webhook endpoints (they don't use standard API auth)
|
44
|
+
import requests
|
45
|
+
base_url = self.http_client.config.host
|
46
|
+
url = f"{base_url}/api/{endpoint}"
|
47
|
+
|
48
|
+
response = requests.get(
|
49
|
+
url,
|
50
|
+
params=params,
|
51
|
+
verify=self.http_client.config.verify_ssl,
|
52
|
+
timeout=30
|
53
|
+
)
|
54
|
+
|
55
|
+
if response.status_code == 200:
|
56
|
+
return response.json()
|
57
|
+
elif response.status_code == 403:
|
58
|
+
return {
|
59
|
+
"success": False,
|
60
|
+
"error": "Forbidden",
|
61
|
+
"message": "Invalid webhook token",
|
62
|
+
"statusCode": 403
|
63
|
+
}
|
64
|
+
elif response.status_code == 404:
|
65
|
+
return {
|
66
|
+
"success": False,
|
67
|
+
"error": "Not Found",
|
68
|
+
"message": "Webhook not found",
|
69
|
+
"statusCode": 404
|
70
|
+
}
|
71
|
+
else:
|
72
|
+
return {
|
73
|
+
"success": False,
|
74
|
+
"error": f"HTTP {response.status_code}",
|
75
|
+
"message": response.text,
|
76
|
+
"statusCode": response.status_code
|
77
|
+
}
|
78
|
+
else:
|
79
|
+
# Use authenticated API endpoint (if available)
|
80
|
+
endpoint = f"webhook/{slug}/{data}"
|
81
|
+
params = {"token": token}
|
82
|
+
return self.http_client.get(endpoint, params=params)
|
83
|
+
|
84
|
+
def trigger_post(
|
85
|
+
self,
|
86
|
+
slug: str,
|
87
|
+
token: str,
|
88
|
+
payload: Optional[Dict[str, Any]] = None,
|
89
|
+
use_webhook_endpoint: bool = True
|
90
|
+
) -> Dict[str, Any]:
|
91
|
+
"""
|
92
|
+
Trigger a webhook via POST request.
|
93
|
+
|
94
|
+
Args:
|
95
|
+
slug: Webhook slug/name
|
96
|
+
token: Webhook token
|
97
|
+
payload: Optional POST data/payload
|
98
|
+
use_webhook_endpoint: If True, use webhook endpoint directly (no auth needed)
|
99
|
+
If False, use authenticated API endpoint
|
100
|
+
|
101
|
+
Returns:
|
102
|
+
Dict containing task details and URLs
|
103
|
+
"""
|
104
|
+
if payload is None:
|
105
|
+
payload = {}
|
106
|
+
|
107
|
+
if use_webhook_endpoint:
|
108
|
+
# Direct webhook call - no authentication needed, just token
|
109
|
+
endpoint = f"webhook/{slug}"
|
110
|
+
params = {"token": token}
|
111
|
+
|
112
|
+
# Use raw HTTP client for webhook endpoints
|
113
|
+
import requests
|
114
|
+
base_url = self.http_client.config.host
|
115
|
+
url = f"{base_url}/api/{endpoint}"
|
116
|
+
|
117
|
+
response = requests.post(
|
118
|
+
url,
|
119
|
+
params=params,
|
120
|
+
json=payload,
|
121
|
+
verify=self.http_client.config.verify_ssl,
|
122
|
+
timeout=30
|
123
|
+
)
|
124
|
+
|
125
|
+
if response.status_code == 200:
|
126
|
+
return response.json()
|
127
|
+
elif response.status_code == 403:
|
128
|
+
return {
|
129
|
+
"success": False,
|
130
|
+
"error": "Forbidden",
|
131
|
+
"message": "Invalid webhook token",
|
132
|
+
"statusCode": 403
|
133
|
+
}
|
134
|
+
elif response.status_code == 404:
|
135
|
+
return {
|
136
|
+
"success": False,
|
137
|
+
"error": "Not Found",
|
138
|
+
"message": "Webhook not found",
|
139
|
+
"statusCode": 404
|
140
|
+
}
|
141
|
+
else:
|
142
|
+
return {
|
143
|
+
"success": False,
|
144
|
+
"error": f"HTTP {response.status_code}",
|
145
|
+
"message": response.text,
|
146
|
+
"statusCode": response.status_code
|
147
|
+
}
|
148
|
+
else:
|
149
|
+
# Use authenticated API endpoint (if available)
|
150
|
+
endpoint = f"webhook/{slug}"
|
151
|
+
params = {"token": token}
|
152
|
+
return self.http_client.post(endpoint, json_data=payload, params=params)
|
153
|
+
|
154
|
+
def get_task_details(
|
155
|
+
self,
|
156
|
+
slug: str,
|
157
|
+
token: str,
|
158
|
+
task_id: str,
|
159
|
+
use_webhook_endpoint: bool = True
|
160
|
+
) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
|
161
|
+
"""
|
162
|
+
Get task assignment details from a webhook.
|
163
|
+
|
164
|
+
Args:
|
165
|
+
slug: Webhook slug/name
|
166
|
+
token: Webhook token
|
167
|
+
task_id: Task ID returned from webhook trigger
|
168
|
+
use_webhook_endpoint: If True, use webhook endpoint directly (no auth needed)
|
169
|
+
If False, use authenticated API endpoint
|
170
|
+
|
171
|
+
Returns:
|
172
|
+
List of task assignment details or Dict with error info
|
173
|
+
"""
|
174
|
+
if use_webhook_endpoint:
|
175
|
+
# Direct webhook call - no authentication needed, just token
|
176
|
+
endpoint = f"webhook/{slug}/assignments"
|
177
|
+
params = {"token": token, "taskId": task_id}
|
178
|
+
|
179
|
+
# Use raw HTTP client for webhook endpoints
|
180
|
+
import requests
|
181
|
+
base_url = self.http_client.config.host
|
182
|
+
url = f"{base_url}/api/{endpoint}"
|
183
|
+
|
184
|
+
response = requests.get(
|
185
|
+
url,
|
186
|
+
params=params,
|
187
|
+
verify=self.http_client.config.verify_ssl,
|
188
|
+
timeout=30
|
189
|
+
)
|
190
|
+
|
191
|
+
if response.status_code == 200:
|
192
|
+
return response.json()
|
193
|
+
elif response.status_code == 403:
|
194
|
+
return {
|
195
|
+
"success": False,
|
196
|
+
"error": "Forbidden",
|
197
|
+
"message": "Invalid webhook token",
|
198
|
+
"statusCode": 403
|
199
|
+
}
|
200
|
+
elif response.status_code == 404:
|
201
|
+
return {
|
202
|
+
"success": False,
|
203
|
+
"error": "Not Found",
|
204
|
+
"message": "Task not found",
|
205
|
+
"statusCode": 404
|
206
|
+
}
|
207
|
+
else:
|
208
|
+
return {
|
209
|
+
"success": False,
|
210
|
+
"error": f"HTTP {response.status_code}",
|
211
|
+
"message": response.text,
|
212
|
+
"statusCode": response.status_code
|
213
|
+
}
|
214
|
+
else:
|
215
|
+
# Use authenticated API endpoint (if available)
|
216
|
+
endpoint = f"webhook/{slug}/assignments"
|
217
|
+
params = {"token": token, "taskId": task_id}
|
218
|
+
return self.http_client.get(endpoint, params=params)
|
219
|
+
|
220
|
+
# Convenience methods with better names
|
221
|
+
def call_webhook_get(self, slug: str, data: str, token: str) -> Dict[str, Any]:
|
222
|
+
"""Convenience method to call a webhook via GET."""
|
223
|
+
return self.trigger_get(slug, data, token)
|
224
|
+
|
225
|
+
def call_webhook_post(self, slug: str, token: str, payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
226
|
+
"""Convenience method to call a webhook via POST."""
|
227
|
+
return self.trigger_post(slug, token, payload)
|
228
|
+
|
229
|
+
def get_webhook_task_data(self, slug: str, token: str, task_id: str) -> Union[List[Dict[str, Any]], Dict[str, Any]]:
|
230
|
+
"""Convenience method to get webhook task assignment data."""
|
231
|
+
return self.get_task_details(slug, token, task_id)
|
binalyze_air/base.py
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
"""
|
2
|
+
Base classes for CQRS implementation in the AIR SDK.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from abc import ABC, abstractmethod
|
6
|
+
from typing import Any, Dict, List, Optional, TypeVar, Generic
|
7
|
+
from pydantic import BaseModel as PydanticBaseModel, ConfigDict
|
8
|
+
|
9
|
+
T = TypeVar("T")
|
10
|
+
|
11
|
+
|
12
|
+
class Query(Generic[T], ABC):
|
13
|
+
"""Base class for all queries (read operations)."""
|
14
|
+
|
15
|
+
@abstractmethod
|
16
|
+
def execute(self) -> T:
|
17
|
+
"""Execute the query and return the result."""
|
18
|
+
pass
|
19
|
+
|
20
|
+
|
21
|
+
class Command(Generic[T], ABC):
|
22
|
+
"""Base class for all commands (write operations)."""
|
23
|
+
|
24
|
+
@abstractmethod
|
25
|
+
def execute(self) -> T:
|
26
|
+
"""Execute the command and return the result."""
|
27
|
+
pass
|
28
|
+
|
29
|
+
|
30
|
+
class AIRBaseModel(PydanticBaseModel):
|
31
|
+
"""Base Pydantic model with common configurations."""
|
32
|
+
|
33
|
+
model_config = ConfigDict(
|
34
|
+
use_enum_values=True,
|
35
|
+
validate_assignment=True,
|
36
|
+
arbitrary_types_allowed=True
|
37
|
+
)
|
38
|
+
|
39
|
+
|
40
|
+
class PaginatedResponse(AIRBaseModel, Generic[T]):
|
41
|
+
"""Generic paginated response model."""
|
42
|
+
|
43
|
+
entities: List[T]
|
44
|
+
total_entity_count: int
|
45
|
+
current_page: int
|
46
|
+
page_size: int
|
47
|
+
total_page_count: int
|
48
|
+
previous_page: Optional[int] = None
|
49
|
+
next_page: Optional[int] = None
|
50
|
+
|
51
|
+
|
52
|
+
class APIResponse(AIRBaseModel, Generic[T]):
|
53
|
+
"""Generic API response model."""
|
54
|
+
|
55
|
+
success: bool
|
56
|
+
result: T
|
57
|
+
status_code: int
|
58
|
+
errors: List[str] = []
|
59
|
+
|
60
|
+
|
61
|
+
class Filter(AIRBaseModel):
|
62
|
+
"""Base filter model for queries with pagination and sorting support."""
|
63
|
+
|
64
|
+
# Basic filter fields
|
65
|
+
search_term: Optional[str] = None
|
66
|
+
organization_ids: Optional[List[int]] = None
|
67
|
+
|
68
|
+
# Pagination parameters (match API documentation exactly) - no defaults
|
69
|
+
page_number: Optional[int] = None
|
70
|
+
page_size: Optional[int] = None
|
71
|
+
|
72
|
+
# Sorting parameters (match API documentation exactly) - no defaults
|
73
|
+
sort_by: Optional[str] = None
|
74
|
+
sort_type: Optional[str] = None
|
75
|
+
|
76
|
+
def to_params(self) -> Dict[str, Any]:
|
77
|
+
"""Convert filter to API parameters."""
|
78
|
+
params = {}
|
79
|
+
|
80
|
+
# Pagination parameters (not in filter namespace) - only if set
|
81
|
+
if self.page_number is not None:
|
82
|
+
params["pageNumber"] = self.page_number
|
83
|
+
if self.page_size is not None:
|
84
|
+
params["pageSize"] = self.page_size
|
85
|
+
if self.sort_by is not None:
|
86
|
+
params["sortBy"] = self.sort_by
|
87
|
+
if self.sort_type is not None:
|
88
|
+
params["sortType"] = self.sort_type
|
89
|
+
|
90
|
+
# Filter parameters (in filter namespace)
|
91
|
+
for field_name, field_value in self.model_dump(exclude_none=True).items():
|
92
|
+
# Skip pagination/sorting fields as they're handled above
|
93
|
+
if field_name in ["page_number", "page_size", "sort_by", "sort_type"]:
|
94
|
+
continue
|
95
|
+
|
96
|
+
if field_value is not None:
|
97
|
+
if isinstance(field_value, list):
|
98
|
+
params[f"filter[{field_name}]"] = ",".join(map(str, field_value))
|
99
|
+
else:
|
100
|
+
params[f"filter[{field_name}]"] = str(field_value)
|
101
|
+
return params
|
102
|
+
|
103
|
+
|
104
|
+
class PaginatedList(list):
|
105
|
+
"""List-like container that carries pagination metadata.
|
106
|
+
This allows SDK query methods to return a normal iterable while still
|
107
|
+
exposing additional pagination attributes such as total_entity_count.
|
108
|
+
The class simply subclasses ``list`` and adds attributes during
|
109
|
+
construction. All standard list operations remain intact.
|
110
|
+
"""
|
111
|
+
def __init__(
|
112
|
+
self,
|
113
|
+
iterable=None,
|
114
|
+
*,
|
115
|
+
total_entity_count: int | None = None,
|
116
|
+
current_page: int | None = None,
|
117
|
+
page_size: int | None = None,
|
118
|
+
total_page_count: int | None = None,
|
119
|
+
) -> None:
|
120
|
+
super().__init__(iterable or [])
|
121
|
+
# Store metadata for caller introspection
|
122
|
+
self.total_entity_count = total_entity_count
|
123
|
+
self.current_page = current_page
|
124
|
+
self.page_size = page_size
|
125
|
+
self.total_page_count = total_page_count
|
126
|
+
|
127
|
+
# Optional: pretty representation including meta for debugging
|
128
|
+
def __repr__(self) -> str: # noqa: D401
|
129
|
+
meta = (
|
130
|
+
f" total={self.total_entity_count} page={self.current_page} "
|
131
|
+
f"size={self.page_size} pages={self.total_page_count}"
|
132
|
+
)
|
133
|
+
return f"PaginatedList({list.__repr__(self)},{meta})"
|