Skip to content

Commit 8f929d5

Browse files
kesmit13claude
andcommitted
Add management API modules with enum case normalization
New management modules: - audit_logs.py: Audit log querying with pagination - invitations.py: Organization invitation management - private_connections.py: Private connection (PrivateLink) management - projects.py: Project listing - roles.py: Role-based access control management - storage.py: Storage and disaster recovery operations - teams.py: Team management with member/role operations - users.py: User management with identity roles Key improvements: - All enum string parameters are normalized to match OpenAPI spec: - UPPERCASE: provider, deploymentType, connectionType, status, edition - lowercase: resourceType - PascalCase: audit log type/source, invitation state - Specific normalizer functions in utils.py for self-documenting code - Normalization in both constructors and API calls for defensive programming - Mixin classes use cast('WorkspaceManager', self) instead of type ignores 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent a6e9efa commit 8f929d5

File tree

14 files changed

+6873
-44
lines changed

14 files changed

+6873
-44
lines changed

singlestoredb/management/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
#!/usr/bin/env python
2+
from .audit_logs import AuditLog
23
from .cluster import manage_cluster
34
from .files import manage_files
5+
from .invitations import Invitation
46
from .manager import get_token
7+
from .organization import Secret
8+
from .private_connections import PrivateConnection
9+
from .projects import Project
510
from .region import manage_regions
11+
from .roles import Role
12+
from .teams import Team
13+
from .users import User
614
from .workspace import get_organization
715
from .workspace import get_secret
816
from .workspace import get_stage
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
#!/usr/bin/env python
2+
"""SingleStoreDB Audit Log Management."""
3+
from __future__ import annotations
4+
5+
import datetime
6+
from typing import Any
7+
from typing import cast
8+
from typing import Dict
9+
from typing import List
10+
from typing import Optional
11+
from typing import TYPE_CHECKING
12+
13+
from .utils import NamedList
14+
from .utils import normalize_audit_log_source
15+
from .utils import normalize_audit_log_type
16+
from .utils import to_datetime
17+
from .utils import vars_to_str
18+
19+
if TYPE_CHECKING:
20+
from .workspace import WorkspaceManager
21+
22+
23+
class AuditLog:
24+
"""
25+
SingleStoreDB audit log entry.
26+
27+
This object is not instantiated directly. It is used in the results
28+
of API calls on the :class:`WorkspaceManager`. Audit logs are accessed using
29+
:meth:`WorkspaceManager.get_audit_logs`.
30+
31+
See Also
32+
--------
33+
:meth:`WorkspaceManager.get_audit_logs`
34+
35+
"""
36+
37+
id: str
38+
type: str
39+
source: Optional[str]
40+
timestamp: Optional[datetime.datetime]
41+
user_id: Optional[str]
42+
user_email: Optional[str]
43+
first_name: Optional[str]
44+
last_name: Optional[str]
45+
resource_type: Optional[str]
46+
resource_id: Optional[str]
47+
resource_name: Optional[str]
48+
details: Optional[Dict[str, Any]]
49+
50+
def __init__(
51+
self,
52+
audit_log_id: str,
53+
type: str,
54+
source: Optional[str] = None,
55+
timestamp: Optional[datetime.datetime] = None,
56+
user_id: Optional[str] = None,
57+
user_email: Optional[str] = None,
58+
first_name: Optional[str] = None,
59+
last_name: Optional[str] = None,
60+
resource_type: Optional[str] = None,
61+
resource_id: Optional[str] = None,
62+
resource_name: Optional[str] = None,
63+
details: Optional[Dict[str, Any]] = None,
64+
):
65+
#: Unique ID of the audit log entry
66+
self.id = audit_log_id
67+
68+
#: Type of action (e.g., 'Login')
69+
self.type = normalize_audit_log_type(type) or type
70+
71+
#: Source of the action (e.g., 'Portal', 'Admin', 'SystemJob')
72+
self.source = normalize_audit_log_source(source) if source else source
73+
74+
#: Timestamp of when the action occurred
75+
self.timestamp = timestamp
76+
77+
#: ID of the user who performed the action
78+
self.user_id = user_id
79+
80+
#: Email of the user who performed the action
81+
self.user_email = user_email
82+
83+
#: First name of the user who performed the action
84+
self.first_name = first_name
85+
86+
#: Last name of the user who performed the action
87+
self.last_name = last_name
88+
89+
#: Type of resource affected
90+
self.resource_type = resource_type
91+
92+
#: ID of the resource affected
93+
self.resource_id = resource_id
94+
95+
#: Name of the resource affected
96+
self.resource_name = resource_name
97+
98+
#: Additional details about the action
99+
self.details = details
100+
101+
self._manager: Optional[WorkspaceManager] = None
102+
103+
def __str__(self) -> str:
104+
"""Return string representation."""
105+
return vars_to_str(self)
106+
107+
def __repr__(self) -> str:
108+
"""Return string representation."""
109+
return str(self)
110+
111+
@classmethod
112+
def from_dict(
113+
cls,
114+
obj: Dict[str, Any],
115+
manager: 'WorkspaceManager',
116+
) -> 'AuditLog':
117+
"""
118+
Construct an AuditLog from a dictionary of values.
119+
120+
Parameters
121+
----------
122+
obj : dict
123+
Dictionary of values
124+
manager : WorkspaceManager
125+
The WorkspaceManager the AuditLog belongs to
126+
127+
Returns
128+
-------
129+
:class:`AuditLog`
130+
131+
"""
132+
# Get ID - try auditLogID first, fall back to id
133+
audit_log_id = obj.get('auditLogID') or obj.get('id')
134+
if not audit_log_id:
135+
raise KeyError('Missing required field(s): auditLogID or id')
136+
137+
out = cls(
138+
audit_log_id=audit_log_id,
139+
type=obj.get('type', ''),
140+
source=obj.get('source'),
141+
timestamp=to_datetime(obj.get('timestamp')),
142+
user_id=obj.get('userID'),
143+
user_email=obj.get('userEmail'),
144+
first_name=obj.get('firstName'),
145+
last_name=obj.get('lastName'),
146+
resource_type=obj.get('resourceType'),
147+
resource_id=obj.get('resourceID'),
148+
resource_name=obj.get('resourceName'),
149+
details=obj.get('details'),
150+
)
151+
out._manager = manager
152+
return out
153+
154+
155+
class AuditLogResult:
156+
"""
157+
Result from audit log query with pagination support.
158+
159+
Attributes
160+
----------
161+
logs : List[AuditLog]
162+
List of audit log entries
163+
next_token : str, optional
164+
Token for fetching the next page of results
165+
166+
"""
167+
168+
def __init__(
169+
self,
170+
logs: List[AuditLog],
171+
next_token: Optional[str] = None,
172+
):
173+
self.logs = logs
174+
self.next_token = next_token
175+
176+
def __str__(self) -> str:
177+
"""Return string representation."""
178+
return vars_to_str(self)
179+
180+
def __repr__(self) -> str:
181+
"""Return string representation."""
182+
return str(self)
183+
184+
185+
class AuditLogsMixin:
186+
"""Mixin class that adds audit log methods to WorkspaceManager."""
187+
188+
def get_audit_logs(
189+
self,
190+
type: Optional[str] = None,
191+
source: Optional[str] = None,
192+
start_date: Optional[datetime.datetime] = None,
193+
end_date: Optional[datetime.datetime] = None,
194+
limit: Optional[int] = None,
195+
next_token: Optional[str] = None,
196+
first_name: Optional[str] = None,
197+
last_name: Optional[str] = None,
198+
) -> AuditLogResult:
199+
"""
200+
Get audit logs for the organization.
201+
202+
Parameters
203+
----------
204+
type : str, optional
205+
Filter by action type (e.g., 'Login')
206+
source : str, optional
207+
Filter by source (e.g., 'Portal', 'Admin', 'SystemJob')
208+
start_date : datetime, optional
209+
Start date for the query range
210+
end_date : datetime, optional
211+
End date for the query range
212+
limit : int, optional
213+
Maximum number of results to return
214+
next_token : str, optional
215+
Token for pagination (from previous response)
216+
first_name : str, optional
217+
Filter by user's first name
218+
last_name : str, optional
219+
Filter by user's last name
220+
221+
Returns
222+
-------
223+
:class:`AuditLogResult`
224+
Contains list of audit logs and optional next_token for pagination
225+
226+
"""
227+
params: Dict[str, Any] = {}
228+
if type is not None:
229+
params['type'] = normalize_audit_log_type(type)
230+
if source is not None:
231+
params['source'] = normalize_audit_log_source(source)
232+
if start_date is not None:
233+
params['startDate'] = start_date.isoformat()
234+
if end_date is not None:
235+
params['endDate'] = end_date.isoformat()
236+
if limit is not None:
237+
params['limit'] = limit
238+
if next_token is not None:
239+
params['nextToken'] = next_token
240+
if first_name is not None:
241+
params['firstName'] = first_name
242+
if last_name is not None:
243+
params['lastName'] = last_name
244+
245+
manager = cast('WorkspaceManager', self)
246+
res = manager._get('auditLogs', params=params)
247+
data = res.json()
248+
249+
logs = NamedList([
250+
AuditLog.from_dict(item, manager)
251+
for item in data.get('auditLogs', [])
252+
])
253+
254+
return AuditLogResult(
255+
logs=logs,
256+
next_token=data.get('nextToken'),
257+
)

0 commit comments

Comments
 (0)