aisoc-sdk 4.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.
aisoc_sdk/__init__.py ADDED
@@ -0,0 +1,52 @@
1
+ """aisoc-sdk — Python client for AiSOC.
2
+
3
+ Usage::
4
+
5
+ from aisoc_sdk import AiSOCClient
6
+
7
+ async with AiSOCClient(base_url="https://soc.example.com", token="aisoc_...") as client:
8
+ alerts = await client.alerts.list(severity="critical")
9
+ """
10
+
11
+ from .client import AiSOCClient, AiSOCError
12
+ from .models import (
13
+ Alert,
14
+ AlertFilters,
15
+ AlertSeverity,
16
+ AlertStatus,
17
+ ApiKey,
18
+ ApiKeyCreateRequest,
19
+ ApiKeyCreateResponse,
20
+ Case,
21
+ CaseFilters,
22
+ CasePriority,
23
+ CaseStatus,
24
+ Connector,
25
+ DetectionRule,
26
+ Page,
27
+ Playbook,
28
+ PlaybookRun,
29
+ )
30
+
31
+ __all__ = [
32
+ "AiSOCClient",
33
+ "AiSOCError",
34
+ "Alert",
35
+ "AlertFilters",
36
+ "AlertSeverity",
37
+ "AlertStatus",
38
+ "ApiKey",
39
+ "ApiKeyCreateRequest",
40
+ "ApiKeyCreateResponse",
41
+ "Case",
42
+ "CaseFilters",
43
+ "CasePriority",
44
+ "CaseStatus",
45
+ "Connector",
46
+ "DetectionRule",
47
+ "Page",
48
+ "Playbook",
49
+ "PlaybookRun",
50
+ ]
51
+
52
+ __version__ = "4.0.0"
aisoc_sdk/client.py ADDED
@@ -0,0 +1,292 @@
1
+ """AiSOCClient — async httpx-based client for the AiSOC REST API.
2
+
3
+ Usage::
4
+
5
+ async with AiSOCClient(base_url="https://soc.example.com", token="aisoc_...") as c:
6
+ page = await c.alerts.list(severity="critical")
7
+ case = await c.cases.create(title="Incident", priority="high")
8
+ run = await c.playbooks.run("isolate-host", trigger_data={"host": "srv-42"})
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any, Optional, Type, TypeVar
14
+
15
+ import httpx
16
+ from pydantic import TypeAdapter
17
+
18
+ from .models import (
19
+ Alert,
20
+ AlertFilters,
21
+ ApiKey,
22
+ ApiKeyCreateRequest,
23
+ ApiKeyCreateResponse,
24
+ Case,
25
+ CaseFilters,
26
+ Connector,
27
+ DetectionRule,
28
+ Page,
29
+ Playbook,
30
+ PlaybookRun,
31
+ )
32
+
33
+ T = TypeVar("T")
34
+
35
+
36
+ # ─── Error ────────────────────────────────────────────────────────────────────
37
+
38
+
39
+ class AiSOCError(Exception):
40
+ """Raised when the AiSOC API returns a non-2xx response."""
41
+
42
+ def __init__(self, status_code: int, detail: str) -> None:
43
+ self.status_code = status_code
44
+ self.detail = detail
45
+ super().__init__(f"AiSOC API {status_code}: {detail}")
46
+
47
+
48
+ # ─── Base resource client ─────────────────────────────────────────────────────
49
+
50
+
51
+ class _ResourceClient:
52
+ def __init__(self, http: httpx.AsyncClient) -> None:
53
+ self._http = http
54
+
55
+ async def _get(
56
+ self,
57
+ path: str,
58
+ params: Optional[dict[str, Any]] = None,
59
+ model: Optional[Type[T]] = None,
60
+ ) -> Any:
61
+ r = await self._http.get(path, params=self._clean(params))
62
+ self._raise(r)
63
+ if model is not None:
64
+ return TypeAdapter(model).validate_python(r.json())
65
+ return r.json()
66
+
67
+ async def _post(self, path: str, body: Any, model: Optional[Type[T]] = None) -> Any:
68
+ r = await self._http.post(path, json=body)
69
+ self._raise(r)
70
+ if model is not None:
71
+ return TypeAdapter(model).validate_python(r.json())
72
+ return r.json()
73
+
74
+ async def _patch(self, path: str, body: Any, model: Optional[Type[T]] = None) -> Any:
75
+ r = await self._http.patch(path, json=body)
76
+ self._raise(r)
77
+ if model is not None:
78
+ return TypeAdapter(model).validate_python(r.json())
79
+ return r.json()
80
+
81
+ async def _put(self, path: str, body: Any, model: Optional[Type[T]] = None) -> Any:
82
+ r = await self._http.put(path, json=body)
83
+ self._raise(r)
84
+ if model is not None:
85
+ return TypeAdapter(model).validate_python(r.json())
86
+ return r.json()
87
+
88
+ async def _delete(self, path: str) -> None:
89
+ r = await self._http.delete(path)
90
+ self._raise(r)
91
+
92
+ @staticmethod
93
+ def _clean(params: Optional[dict[str, Any]]) -> dict[str, Any]:
94
+ if params is None:
95
+ return {}
96
+ return {k: v for k, v in params.items() if v is not None}
97
+
98
+ @staticmethod
99
+ def _raise(r: httpx.Response) -> None:
100
+ if not r.is_success:
101
+ try:
102
+ detail = r.json().get("detail", r.text)
103
+ except Exception:
104
+ detail = r.text
105
+ raise AiSOCError(r.status_code, detail)
106
+
107
+
108
+ # ─── Resource sub-clients ─────────────────────────────────────────────────────
109
+
110
+
111
+ class AlertsClient(_ResourceClient):
112
+ async def list(self, filters: Optional[AlertFilters] = None, **kwargs: Any) -> Page[Alert]:
113
+ params = filters.model_dump(exclude_none=True) if filters else self._clean(kwargs)
114
+ return await self._get("/api/v1/alerts", params, Page[Alert])
115
+
116
+ async def get(self, alert_id: str) -> Alert:
117
+ return await self._get(f"/api/v1/alerts/{alert_id}", model=Alert)
118
+
119
+ async def update(self, alert_id: str, **data: Any) -> Alert:
120
+ return await self._patch(f"/api/v1/alerts/{alert_id}", data, Alert)
121
+
122
+
123
+ class CasesClient(_ResourceClient):
124
+ async def list(self, filters: Optional[CaseFilters] = None, **kwargs: Any) -> Page[Case]:
125
+ params = filters.model_dump(exclude_none=True) if filters else self._clean(kwargs)
126
+ return await self._get("/api/v1/cases", params, Page[Case])
127
+
128
+ async def get(self, case_id: str) -> Case:
129
+ return await self._get(f"/api/v1/cases/{case_id}", model=Case)
130
+
131
+ async def create(self, **data: Any) -> Case:
132
+ return await self._post("/api/v1/cases", data, Case)
133
+
134
+ async def update(self, case_id: str, **data: Any) -> Case:
135
+ return await self._patch(f"/api/v1/cases/{case_id}", data, Case)
136
+
137
+ # There is no `delete`. The API serves no DELETE on a case — a case is
138
+ # closed by patching its status, and the method that used to be here
139
+ # called a route `services/api` has never declared.
140
+
141
+
142
+ class DetectionsClient(_ResourceClient):
143
+ # The route is `/detection/rules`, singular, and this client asked for
144
+ # `/detections` — so every method here answered 404 against a real
145
+ # deployment while its mocked test passed.
146
+ async def list(self, page: int = 1, page_size: int = 20) -> Page[DetectionRule]:
147
+ return await self._get(
148
+ "/api/v1/detection/rules", {"page": page, "page_size": page_size}, Page[DetectionRule]
149
+ )
150
+
151
+ async def get(self, rule_id: str) -> DetectionRule:
152
+ return await self._get(f"/api/v1/detection/rules/{rule_id}", model=DetectionRule)
153
+
154
+
155
+ class ConnectorsClient(_ResourceClient):
156
+ async def list(self, page: int = 1, page_size: int = 20) -> Page[Connector]:
157
+ return await self._get(
158
+ "/api/v1/connectors", {"page": page, "page_size": page_size}, Page[Connector]
159
+ )
160
+
161
+ async def get(self, connector_id: str) -> Connector:
162
+ return await self._get(f"/api/v1/connectors/{connector_id}", model=Connector)
163
+
164
+
165
+ class PlaybooksClient(_ResourceClient):
166
+ async def list(self, page: int = 1, page_size: int = 20) -> Page[Playbook]:
167
+ return await self._get(
168
+ "/api/v1/playbooks", {"page": page, "page_size": page_size}, Page[Playbook]
169
+ )
170
+
171
+ async def get(self, playbook_id: str) -> Playbook:
172
+ return await self._get(f"/api/v1/playbooks/{playbook_id}", model=Playbook)
173
+
174
+ async def create(self, **data: Any) -> Playbook:
175
+ return await self._post("/api/v1/playbooks", data, Playbook)
176
+
177
+ # PUT, not PATCH: `playbooks.py` declares `@router.put("/{playbook_id}")`
178
+ # and no patch route, so the previous verb returned 405.
179
+ async def update(self, playbook_id: str, **data: Any) -> Playbook:
180
+ return await self._put(f"/api/v1/playbooks/{playbook_id}", data, Playbook)
181
+
182
+ async def delete(self, playbook_id: str) -> None:
183
+ return await self._delete(f"/api/v1/playbooks/{playbook_id}")
184
+
185
+ async def run(
186
+ self,
187
+ playbook_id: str,
188
+ trigger_data: Optional[dict[str, Any]] = None,
189
+ ) -> PlaybookRun:
190
+ return await self._post(
191
+ f"/api/v1/playbooks/{playbook_id}/run",
192
+ {"trigger_data": trigger_data or {}},
193
+ PlaybookRun,
194
+ )
195
+
196
+ async def get_run(self, run_id: str) -> PlaybookRun:
197
+ return await self._get(f"/api/v1/playbooks/runs/{run_id}", model=PlaybookRun)
198
+
199
+
200
+ class ApiKeysClient(_ResourceClient):
201
+ async def list(self) -> Page[ApiKey]:
202
+ return await self._get("/api/v1/api-keys", model=Page[ApiKey])
203
+
204
+ async def create(self, req: ApiKeyCreateRequest) -> ApiKeyCreateResponse:
205
+ return await self._post(
206
+ "/api/v1/api-keys",
207
+ req.model_dump(exclude_none=True),
208
+ ApiKeyCreateResponse,
209
+ )
210
+
211
+ async def revoke(self, key_id: str) -> None:
212
+ return await self._delete(f"/api/v1/api-keys/{key_id}")
213
+
214
+
215
+ # ─── Main client ─────────────────────────────────────────────────────────────
216
+
217
+
218
+ class AiSOCClient:
219
+ """Async Python client for the AiSOC REST API.
220
+
221
+ Must be used as an async context manager::
222
+
223
+ async with AiSOCClient(base_url="...", token="...") as client:
224
+ alerts = await client.alerts.list()
225
+
226
+ Or manage the lifecycle manually::
227
+
228
+ client = AiSOCClient(base_url="...", token="...")
229
+ await client.__aenter__()
230
+ try:
231
+ ...
232
+ finally:
233
+ await client.__aexit__(None, None, None)
234
+ """
235
+
236
+ def __init__(
237
+ self,
238
+ base_url: str,
239
+ token: str,
240
+ *,
241
+ timeout: float = 30.0,
242
+ headers: Optional[dict[str, str]] = None,
243
+ ) -> None:
244
+ self._base_url = base_url.rstrip("/")
245
+ self._token = token
246
+ self._timeout = timeout
247
+ self._extra_headers = headers or {}
248
+ self._http: Optional[httpx.AsyncClient] = None
249
+
250
+ # Placeholders — initialised in __aenter__
251
+ self.alerts: AlertsClient
252
+ self.cases: CasesClient
253
+ self.detections: DetectionsClient
254
+ self.connectors: ConnectorsClient
255
+ self.playbooks: PlaybooksClient
256
+ self.api_keys: ApiKeysClient
257
+
258
+ async def __aenter__(self) -> "AiSOCClient":
259
+ self._http = httpx.AsyncClient(
260
+ base_url=self._base_url,
261
+ headers={
262
+ "Authorization": f"Bearer {self._token}",
263
+ "Content-Type": "application/json",
264
+ **self._extra_headers,
265
+ },
266
+ timeout=self._timeout,
267
+ )
268
+ self.alerts = AlertsClient(self._http)
269
+ self.cases = CasesClient(self._http)
270
+ self.detections = DetectionsClient(self._http)
271
+ self.connectors = ConnectorsClient(self._http)
272
+ self.playbooks = PlaybooksClient(self._http)
273
+ self.api_keys = ApiKeysClient(self._http)
274
+ return self
275
+
276
+ async def __aexit__(self, *_: Any) -> None:
277
+ if self._http is not None:
278
+ await self._http.aclose()
279
+ self._http = None
280
+
281
+ async def graphql(
282
+ self,
283
+ query: str,
284
+ variables: Optional[dict[str, Any]] = None,
285
+ ) -> dict[str, Any]:
286
+ """Execute a GraphQL query against the /graphql endpoint."""
287
+ if self._http is None:
288
+ raise RuntimeError("Use AiSOCClient as an async context manager")
289
+ r = await self._http.post("/graphql", json={"query": query, "variables": variables})
290
+ if not r.is_success:
291
+ raise AiSOCError(r.status_code, r.text)
292
+ return r.json() # type: ignore[return-value]
aisoc_sdk/models.py ADDED
@@ -0,0 +1,208 @@
1
+ """Pydantic models mirroring the AiSOC OpenAPI schema."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from enum import Enum
7
+ from typing import Any, Generic, List, Optional, TypeVar
8
+
9
+ from pydantic import BaseModel, ConfigDict
10
+
11
+
12
+ # ── Enums ─────────────────────────────────────────────────────────────────────
13
+
14
+
15
+ class AlertSeverity(str, Enum):
16
+ CRITICAL = "critical"
17
+ HIGH = "high"
18
+ MEDIUM = "medium"
19
+ LOW = "low"
20
+ INFO = "info"
21
+
22
+
23
+ class AlertStatus(str, Enum):
24
+ OPEN = "open"
25
+ IN_PROGRESS = "in_progress"
26
+ CLOSED = "closed"
27
+ FALSE_POSITIVE = "false_positive"
28
+
29
+
30
+ class CasePriority(str, Enum):
31
+ CRITICAL = "critical"
32
+ HIGH = "high"
33
+ MEDIUM = "medium"
34
+ LOW = "low"
35
+
36
+
37
+ class CaseStatus(str, Enum):
38
+ OPEN = "open"
39
+ INVESTIGATING = "investigating"
40
+ RESOLVED = "resolved"
41
+ CLOSED = "closed"
42
+
43
+
44
+ # ── Core models ───────────────────────────────────────────────────────────────
45
+
46
+ _M = ConfigDict(populate_by_name=True, from_attributes=True)
47
+
48
+
49
+ class Alert(BaseModel):
50
+ model_config = _M
51
+
52
+ id: str
53
+ tenant_id: str
54
+ title: str
55
+ severity: AlertSeverity
56
+ status: AlertStatus
57
+ source: str
58
+ source_ref: Optional[str] = None
59
+ mitre_tactics: List[str] = []
60
+ ai_score: Optional[float] = None
61
+ case_id: Optional[str] = None
62
+ created_at: datetime
63
+ updated_at: datetime
64
+
65
+
66
+ class Case(BaseModel):
67
+ model_config = _M
68
+
69
+ id: str
70
+ tenant_id: str
71
+ case_number: str
72
+ title: str
73
+ status: CaseStatus
74
+ priority: CasePriority
75
+ assignee: Optional[str] = None
76
+ mitre_tactics: List[str] = []
77
+ alert_ids: List[str] = []
78
+ created_at: datetime
79
+ updated_at: datetime
80
+
81
+
82
+ class DetectionRule(BaseModel):
83
+ model_config = _M
84
+
85
+ id: str
86
+ tenant_id: str
87
+ name: str
88
+ description: Optional[str] = None
89
+ rule_language: str
90
+ severity: AlertSeverity
91
+ enabled: bool
92
+ created_at: datetime
93
+ updated_at: datetime
94
+
95
+
96
+ class Connector(BaseModel):
97
+ model_config = _M
98
+
99
+ id: str
100
+ tenant_id: str
101
+ name: str
102
+ connector_type: str
103
+ is_enabled: bool
104
+ health_status: str
105
+ events_ingested: int = 0
106
+ created_at: datetime
107
+ updated_at: datetime
108
+
109
+
110
+ class PlaybookStep(BaseModel):
111
+ model_config = _M
112
+
113
+ id: str
114
+ name: str
115
+ type: str
116
+ action: Optional[str] = None
117
+ parameters: Optional[dict[str, Any]] = None
118
+ next_steps: List[str] = []
119
+
120
+
121
+ class Playbook(BaseModel):
122
+ model_config = _M
123
+
124
+ id: str
125
+ name: str
126
+ description: Optional[str] = None
127
+ version: str
128
+ steps: List[PlaybookStep] = []
129
+ trigger_conditions: Optional[dict[str, Any]] = None
130
+ created_at: datetime
131
+ updated_at: datetime
132
+
133
+
134
+ class PlaybookRun(BaseModel):
135
+ model_config = _M
136
+
137
+ run_id: str
138
+ playbook_id: str
139
+ status: str
140
+ started_at: datetime
141
+ completed_at: Optional[datetime] = None
142
+ trigger_data: Optional[dict[str, Any]] = None
143
+ step_results: Optional[dict[str, Any]] = None
144
+
145
+
146
+ class ApiKey(BaseModel):
147
+ model_config = _M
148
+
149
+ id: str
150
+ name: str
151
+ prefix: str
152
+ scopes: List[str]
153
+ expires_at: Optional[datetime] = None
154
+ last_used_at: Optional[datetime] = None
155
+ created_at: datetime
156
+
157
+
158
+ # ── Pagination ────────────────────────────────────────────────────────────────
159
+
160
+ T = TypeVar("T")
161
+
162
+
163
+ class Page(BaseModel, Generic[T]):
164
+ model_config = _M
165
+
166
+ items: List[T]
167
+ total: int
168
+ page: int
169
+ page_size: int
170
+
171
+
172
+ # ── Request / response helpers ────────────────────────────────────────────────
173
+
174
+
175
+ class AlertFilters(BaseModel):
176
+ model_config = _M
177
+
178
+ severity: Optional[AlertSeverity] = None
179
+ status: Optional[AlertStatus] = None
180
+ case_id: Optional[str] = None
181
+ search: Optional[str] = None
182
+ page: int = 1
183
+ page_size: int = 20
184
+
185
+
186
+ class CaseFilters(BaseModel):
187
+ model_config = _M
188
+
189
+ status: Optional[CaseStatus] = None
190
+ priority: Optional[CasePriority] = None
191
+ assignee: Optional[str] = None
192
+ page: int = 1
193
+ page_size: int = 20
194
+
195
+
196
+ class ApiKeyCreateRequest(BaseModel):
197
+ model_config = _M
198
+
199
+ name: str
200
+ scopes: List[str]
201
+ expires_at: Optional[datetime] = None
202
+
203
+
204
+ class ApiKeyCreateResponse(BaseModel):
205
+ model_config = _M
206
+
207
+ key: ApiKey
208
+ raw_key: str
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.5
2
+ Name: aisoc-sdk
3
+ Version: 4.0.0
4
+ Summary: Python client SDK for AiSOC — typed httpx client
5
+ Project-URL: Homepage, https://github.com/beenuar/AiSOC
6
+ Project-URL: Documentation, https://beenuar.github.io/AiSOC
7
+ Project-URL: Repository, https://github.com/beenuar/AiSOC
8
+ Project-URL: Issues, https://github.com/beenuar/AiSOC/issues
9
+ Author-email: AiSOC Contributors <oss@aisoc.io>
10
+ License: MIT
11
+ Keywords: aisoc,client,sdk,security,soc
12
+ Requires-Python: >=3.10
13
+ Requires-Dist: httpx>=0.27.0
14
+ Requires-Dist: pydantic>=2.0.0
15
+ Provides-Extra: dev
16
+ Requires-Dist: mypy<3,>=2.3.1; extra == 'dev'
17
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
18
+ Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
19
+ Requires-Dist: pytest>=8.0; extra == 'dev'
20
+ Requires-Dist: ruff<0.17,>=0.16.8; extra == 'dev'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # aisoc-sdk
24
+
25
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](../../LICENSE)
26
+ [![PyPI release](https://img.shields.io/badge/pypi-not%20yet%20published-f59e0b)](https://github.com/beenuar/AiSOC/blob/main/CHANGELOG.md)
27
+
28
+ Async Python client SDK for [AiSOC](https://github.com/beenuar/AiSOC).
29
+
30
+ > **Status — monorepo today, not yet on PyPI.** `pip install aisoc-sdk` does not resolve; install from the monorepo source path below. The import path (`aisoc_sdk`) and API surface stay identical once it ships.
31
+
32
+ ## Installation
33
+
34
+ ```bash
35
+ # Today (from this monorepo):
36
+ git clone https://github.com/beenuar/AiSOC.git
37
+ cd AiSOC && pip install -e packages/sdk-py
38
+
39
+ # Not yet on PyPI — the upload is blocked on registry credentials,
40
+ # which is an account action rather than a code change. Until then, install
41
+ # from source with the command above.
42
+ # pip install aisoc-sdk
43
+ ```
44
+
45
+ ## Quick start
46
+
47
+ ```python
48
+ import asyncio
49
+ from aisoc_sdk import AiSOCClient
50
+
51
+
52
+ async def main():
53
+ async with AiSOCClient(
54
+ base_url="https://your-aisoc.example.com",
55
+ token="aisoc_...",
56
+ ) as client:
57
+ # List critical open alerts
58
+ alerts = await client.alerts.list(severity="critical", status="open")
59
+ print(f"Found {alerts.total} critical alerts")
60
+
61
+ # Create a case
62
+ case = await client.cases.create(
63
+ title="Suspicious lateral movement",
64
+ priority="high",
65
+ )
66
+
67
+ # Trigger a playbook
68
+ run = await client.playbooks.run(
69
+ "isolate-host",
70
+ trigger_data={"host_id": "srv-prod-42", "case_id": case.id},
71
+ )
72
+ print("Playbook run:", run.run_id)
73
+
74
+
75
+ asyncio.run(main())
76
+ ```
77
+
78
+ ## GraphQL
79
+
80
+ ```python
81
+ async with AiSOCClient(base_url="...", token="...") as client:
82
+ result = await client.graphql("""
83
+ query {
84
+ alerts(pageSize: 10, status: "open") {
85
+ items { id title severity }
86
+ }
87
+ }
88
+ """)
89
+ ```
90
+
91
+ ## API reference
92
+
93
+ All resource methods are `async` and return typed Pydantic models.
94
+
95
+ | Attribute | Methods |
96
+ |---|---|
97
+ | `client.alerts` | `list(filters?)`, `get(id)`, `update(id, **data)` |
98
+ | `client.cases` | `list(filters?)`, `get(id)`, `create(**data)`, `update(id, **data)`, `delete(id)` |
99
+ | `client.detections` | `list(page, page_size)`, `get(id)` |
100
+ | `client.connectors` | `list(page, page_size)`, `get(id)` |
101
+ | `client.playbooks` | `list(page, page_size)`, `get(id)`, `create(**data)`, `update(id, **data)`, `delete(id)`, `run(id, trigger_data?)`, `get_run(run_id)` |
102
+ | `client.api_keys` | `list()`, `create(req)`, `revoke(id)` |
103
+
104
+ ## Development
105
+
106
+ ```bash
107
+ pip install -e ".[dev]"
108
+ pytest
109
+ ```
@@ -0,0 +1,6 @@
1
+ aisoc_sdk/__init__.py,sha256=cF4BWhjS-orfVyWEXsKUQRBjdoo6MrDXXcaURQoINWQ,955
2
+ aisoc_sdk/client.py,sha256=VBp10g4i6qKOa_zMXqrBbvhcR9TEkuPfYG_ckGwaCoU,10630
3
+ aisoc_sdk/models.py,sha256=iWCoAr3ZRL3HuZ2NPxbxwcvYbSSUyTz-rLybpl-94G0,4677
4
+ aisoc_sdk-4.0.0.dist-info/METADATA,sha256=h0pX-Pou7jm_ZhajJ52-mltlu0RGe4dSfn1br_mSubI,3485
5
+ aisoc_sdk-4.0.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
6
+ aisoc_sdk-4.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any