socratic-knowledge 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.
- socratic_knowledge/__init__.py +26 -0
- socratic_knowledge/access/__init__.py +9 -0
- socratic_knowledge/access/permissions.py +234 -0
- socratic_knowledge/access/rbac.py +37 -0
- socratic_knowledge/audit/__init__.py +10 -0
- socratic_knowledge/audit/events.py +91 -0
- socratic_knowledge/audit/logger.py +216 -0
- socratic_knowledge/collaboration/__init__.py +13 -0
- socratic_knowledge/collaboration/conflict.py +273 -0
- socratic_knowledge/collaboration/locks.py +186 -0
- socratic_knowledge/core/__init__.py +15 -0
- socratic_knowledge/core/collection.py +139 -0
- socratic_knowledge/core/knowledge_item.py +145 -0
- socratic_knowledge/core/tenant.py +89 -0
- socratic_knowledge/core/user.py +51 -0
- socratic_knowledge/core/version.py +99 -0
- socratic_knowledge/integrations/__init__.py +9 -0
- socratic_knowledge/integrations/langchain/__init__.py +5 -0
- socratic_knowledge/integrations/langchain/tools.py +228 -0
- socratic_knowledge/integrations/openclaw/__init__.py +5 -0
- socratic_knowledge/integrations/openclaw/skill.py +327 -0
- socratic_knowledge/manager.py +825 -0
- socratic_knowledge/retrieval/__init__.py +12 -0
- socratic_knowledge/retrieval/indexer.py +80 -0
- socratic_knowledge/retrieval/rag_integration.py +152 -0
- socratic_knowledge/retrieval/search.py +183 -0
- socratic_knowledge/storage/__init__.py +9 -0
- socratic_knowledge/storage/base.py +264 -0
- socratic_knowledge/storage/sqlite_store.py +606 -0
- socratic_knowledge/versioning/__init__.py +9 -0
- socratic_knowledge/versioning/history.py +126 -0
- socratic_knowledge/versioning/version_model.py +35 -0
- socratic_knowledge-0.1.0.dist-info/METADATA +340 -0
- socratic_knowledge-0.1.0.dist-info/RECORD +37 -0
- socratic_knowledge-0.1.0.dist-info/WHEEL +5 -0
- socratic_knowledge-0.1.0.dist-info/licenses/LICENSE +21 -0
- socratic_knowledge-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Socratic Knowledge - Enterprise knowledge management system.
|
|
3
|
+
|
|
4
|
+
Provides multi-tenant knowledge management with access control, versioning,
|
|
5
|
+
and RAG integration.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .core.collection import Collection
|
|
9
|
+
from .core.knowledge_item import KnowledgeItem
|
|
10
|
+
from .core.tenant import Tenant
|
|
11
|
+
from .core.user import User
|
|
12
|
+
from .core.version import Version
|
|
13
|
+
from .manager import KnowledgeManager
|
|
14
|
+
|
|
15
|
+
__version__ = "0.1.0"
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
# Core models
|
|
19
|
+
"KnowledgeItem",
|
|
20
|
+
"Collection",
|
|
21
|
+
"Tenant",
|
|
22
|
+
"User",
|
|
23
|
+
"Version",
|
|
24
|
+
# Manager
|
|
25
|
+
"KnowledgeManager",
|
|
26
|
+
]
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Permission checking and enforcement."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Optional, Set
|
|
4
|
+
|
|
5
|
+
from ..core.collection import Collection
|
|
6
|
+
from ..core.knowledge_item import KnowledgeItem
|
|
7
|
+
from .rbac import Permission, Role, ROLE_PERMISSIONS
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AccessControl:
|
|
11
|
+
"""
|
|
12
|
+
Role-based access control (RBAC) engine.
|
|
13
|
+
|
|
14
|
+
Checks if user has permission to perform action on resource.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def check_permission(
|
|
19
|
+
user_id: str,
|
|
20
|
+
item: KnowledgeItem,
|
|
21
|
+
permission: Permission,
|
|
22
|
+
) -> bool:
|
|
23
|
+
"""
|
|
24
|
+
Check if user has permission on item.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
user_id: User ID
|
|
28
|
+
item: Knowledge item
|
|
29
|
+
permission: Permission to check
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
bool: True if user has permission
|
|
33
|
+
"""
|
|
34
|
+
# Owner always has access
|
|
35
|
+
if item.owner_id == user_id:
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
# Check role-based permissions
|
|
39
|
+
if not item.permissions:
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
for role_str, user_ids in item.permissions.items():
|
|
43
|
+
if user_id in user_ids:
|
|
44
|
+
try:
|
|
45
|
+
role = Role[role_str.upper()]
|
|
46
|
+
except KeyError:
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
role_permissions = ROLE_PERMISSIONS.get(role, set())
|
|
50
|
+
if permission in role_permissions:
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def check_collection_permission(
|
|
57
|
+
user_id: str,
|
|
58
|
+
collection: Collection,
|
|
59
|
+
permission: Permission,
|
|
60
|
+
storage: Optional[object] = None,
|
|
61
|
+
) -> bool:
|
|
62
|
+
"""
|
|
63
|
+
Check permission with inheritance from parent collections.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
user_id: User ID
|
|
67
|
+
collection: Collection
|
|
68
|
+
permission: Permission to check
|
|
69
|
+
storage: Storage instance for fetching parents
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
bool: True if user has permission
|
|
73
|
+
"""
|
|
74
|
+
# Direct permission
|
|
75
|
+
if AccessControl._has_collection_permission(
|
|
76
|
+
user_id,
|
|
77
|
+
collection,
|
|
78
|
+
permission,
|
|
79
|
+
):
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
# Check parent hierarchy if inheritance enabled
|
|
83
|
+
if collection.inherit_permissions and collection.parent_id and storage:
|
|
84
|
+
parent = storage.get_collection(collection.parent_id)
|
|
85
|
+
if parent:
|
|
86
|
+
return AccessControl.check_collection_permission(
|
|
87
|
+
user_id,
|
|
88
|
+
parent,
|
|
89
|
+
permission,
|
|
90
|
+
storage,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
@staticmethod
|
|
96
|
+
def _has_collection_permission(
|
|
97
|
+
user_id: str,
|
|
98
|
+
collection: Collection,
|
|
99
|
+
permission: Permission,
|
|
100
|
+
) -> bool:
|
|
101
|
+
"""
|
|
102
|
+
Check direct permission on collection.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
user_id: User ID
|
|
106
|
+
collection: Collection
|
|
107
|
+
permission: Permission to check
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
bool: True if user has permission
|
|
111
|
+
"""
|
|
112
|
+
# Owner always has access
|
|
113
|
+
if collection.owner_id == user_id:
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
if not collection.permissions:
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
for role_str, user_ids in collection.permissions.items():
|
|
120
|
+
if user_id in user_ids:
|
|
121
|
+
try:
|
|
122
|
+
role = Role[role_str.upper()]
|
|
123
|
+
except KeyError:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
role_permissions = ROLE_PERMISSIONS.get(role, set())
|
|
127
|
+
if permission in role_permissions:
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def grant_permission(
|
|
134
|
+
item: KnowledgeItem,
|
|
135
|
+
user_id: str,
|
|
136
|
+
role: Role,
|
|
137
|
+
) -> None:
|
|
138
|
+
"""
|
|
139
|
+
Grant permission to user.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
item: Knowledge item
|
|
143
|
+
user_id: User to grant permission to
|
|
144
|
+
role: Role to grant
|
|
145
|
+
"""
|
|
146
|
+
if item.permissions is None:
|
|
147
|
+
item.permissions = {}
|
|
148
|
+
|
|
149
|
+
role_str = role.value
|
|
150
|
+
if role_str not in item.permissions:
|
|
151
|
+
item.permissions[role_str] = []
|
|
152
|
+
|
|
153
|
+
if user_id not in item.permissions[role_str]:
|
|
154
|
+
item.permissions[role_str].append(user_id)
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def revoke_permission(
|
|
158
|
+
item: KnowledgeItem,
|
|
159
|
+
user_id: str,
|
|
160
|
+
role: Optional[Role] = None,
|
|
161
|
+
) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Revoke permission from user.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
item: Knowledge item
|
|
167
|
+
user_id: User to revoke permission from
|
|
168
|
+
role: Specific role to revoke, or None to revoke all
|
|
169
|
+
"""
|
|
170
|
+
if not item.permissions:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
if role:
|
|
174
|
+
role_str = role.value
|
|
175
|
+
if role_str in item.permissions:
|
|
176
|
+
if user_id in item.permissions[role_str]:
|
|
177
|
+
item.permissions[role_str].remove(user_id)
|
|
178
|
+
else:
|
|
179
|
+
# Revoke all roles for user
|
|
180
|
+
for role_str in item.permissions:
|
|
181
|
+
if user_id in item.permissions[role_str]:
|
|
182
|
+
item.permissions[role_str].remove(user_id)
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def grant_collection_permission(
|
|
186
|
+
collection: Collection,
|
|
187
|
+
user_id: str,
|
|
188
|
+
role: Role,
|
|
189
|
+
) -> None:
|
|
190
|
+
"""
|
|
191
|
+
Grant permission on collection.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
collection: Collection
|
|
195
|
+
user_id: User to grant permission to
|
|
196
|
+
role: Role to grant
|
|
197
|
+
"""
|
|
198
|
+
if collection.permissions is None:
|
|
199
|
+
collection.permissions = {}
|
|
200
|
+
|
|
201
|
+
role_str = role.value
|
|
202
|
+
if role_str not in collection.permissions:
|
|
203
|
+
collection.permissions[role_str] = []
|
|
204
|
+
|
|
205
|
+
if user_id not in collection.permissions[role_str]:
|
|
206
|
+
collection.permissions[role_str].append(user_id)
|
|
207
|
+
|
|
208
|
+
@staticmethod
|
|
209
|
+
def revoke_collection_permission(
|
|
210
|
+
collection: Collection,
|
|
211
|
+
user_id: str,
|
|
212
|
+
role: Optional[Role] = None,
|
|
213
|
+
) -> None:
|
|
214
|
+
"""
|
|
215
|
+
Revoke permission on collection.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
collection: Collection
|
|
219
|
+
user_id: User to revoke permission from
|
|
220
|
+
role: Specific role to revoke, or None to revoke all
|
|
221
|
+
"""
|
|
222
|
+
if not collection.permissions:
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
if role:
|
|
226
|
+
role_str = role.value
|
|
227
|
+
if role_str in collection.permissions:
|
|
228
|
+
if user_id in collection.permissions[role_str]:
|
|
229
|
+
collection.permissions[role_str].remove(user_id)
|
|
230
|
+
else:
|
|
231
|
+
# Revoke all roles for user
|
|
232
|
+
for role_str in collection.permissions:
|
|
233
|
+
if user_id in collection.permissions[role_str]:
|
|
234
|
+
collection.permissions[role_str].remove(user_id)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Role-Based Access Control (RBAC) implementation."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Set
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Permission(Enum):
|
|
8
|
+
"""Fine-grained permissions."""
|
|
9
|
+
|
|
10
|
+
READ = "read"
|
|
11
|
+
WRITE = "write"
|
|
12
|
+
DELETE = "delete"
|
|
13
|
+
SHARE = "share"
|
|
14
|
+
ADMIN = "admin"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Role(Enum):
|
|
18
|
+
"""Predefined roles with permission sets."""
|
|
19
|
+
|
|
20
|
+
VIEWER = "viewer"
|
|
21
|
+
EDITOR = "editor"
|
|
22
|
+
ADMIN = "admin"
|
|
23
|
+
OWNER = "owner"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
ROLE_PERMISSIONS: dict[Role, Set[Permission]] = {
|
|
27
|
+
Role.VIEWER: {Permission.READ},
|
|
28
|
+
Role.EDITOR: {Permission.READ, Permission.WRITE},
|
|
29
|
+
Role.ADMIN: {Permission.READ, Permission.WRITE, Permission.DELETE, Permission.SHARE},
|
|
30
|
+
Role.OWNER: {
|
|
31
|
+
Permission.READ,
|
|
32
|
+
Permission.WRITE,
|
|
33
|
+
Permission.DELETE,
|
|
34
|
+
Permission.SHARE,
|
|
35
|
+
Permission.ADMIN,
|
|
36
|
+
},
|
|
37
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Audit event types and models."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AuditEventType(Enum):
|
|
10
|
+
"""Types of audit events."""
|
|
11
|
+
|
|
12
|
+
# Item operations
|
|
13
|
+
ITEM_CREATED = "item_created"
|
|
14
|
+
ITEM_UPDATED = "item_updated"
|
|
15
|
+
ITEM_DELETED = "item_deleted"
|
|
16
|
+
ITEM_VIEWED = "item_viewed"
|
|
17
|
+
|
|
18
|
+
# Collection operations
|
|
19
|
+
COLLECTION_CREATED = "collection_created"
|
|
20
|
+
COLLECTION_UPDATED = "collection_updated"
|
|
21
|
+
COLLECTION_DELETED = "collection_deleted"
|
|
22
|
+
|
|
23
|
+
# Tenant operations
|
|
24
|
+
TENANT_CREATED = "tenant_created"
|
|
25
|
+
TENANT_UPDATED = "tenant_updated"
|
|
26
|
+
|
|
27
|
+
# Access control
|
|
28
|
+
PERMISSION_GRANTED = "permission_granted"
|
|
29
|
+
PERMISSION_REVOKED = "permission_revoked"
|
|
30
|
+
|
|
31
|
+
# Version operations
|
|
32
|
+
VERSION_CREATED = "version_created"
|
|
33
|
+
ITEM_ROLLED_BACK = "item_rolled_back"
|
|
34
|
+
|
|
35
|
+
# Search operations
|
|
36
|
+
SEARCH_EXECUTED = "search_executed"
|
|
37
|
+
|
|
38
|
+
# Conflict operations
|
|
39
|
+
CONFLICT_DETECTED = "conflict_detected"
|
|
40
|
+
CONFLICT_RESOLVED = "conflict_resolved"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class AuditEvent:
|
|
45
|
+
"""Represents a single audit event."""
|
|
46
|
+
|
|
47
|
+
event_id: str
|
|
48
|
+
event_type: AuditEventType
|
|
49
|
+
tenant_id: str
|
|
50
|
+
user_id: str
|
|
51
|
+
resource_type: str # "item", "collection", "tenant", etc.
|
|
52
|
+
resource_id: str
|
|
53
|
+
action: str # "create", "update", "delete", etc.
|
|
54
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
55
|
+
changes: Dict[str, Any] = field(default_factory=dict)
|
|
56
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
57
|
+
ip_address: Optional[str] = None
|
|
58
|
+
user_agent: Optional[str] = None
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
61
|
+
"""Serialize to dictionary."""
|
|
62
|
+
return {
|
|
63
|
+
"event_id": self.event_id,
|
|
64
|
+
"event_type": self.event_type.value,
|
|
65
|
+
"tenant_id": self.tenant_id,
|
|
66
|
+
"user_id": self.user_id,
|
|
67
|
+
"resource_type": self.resource_type,
|
|
68
|
+
"resource_id": self.resource_id,
|
|
69
|
+
"action": self.action,
|
|
70
|
+
"timestamp": self.timestamp.isoformat(),
|
|
71
|
+
"changes": self.changes,
|
|
72
|
+
"metadata": self.metadata,
|
|
73
|
+
"ip_address": self.ip_address,
|
|
74
|
+
"user_agent": self.user_agent,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_dict(cls, data: Dict[str, Any]) -> "AuditEvent":
|
|
79
|
+
"""Deserialize from dictionary."""
|
|
80
|
+
event_type = data.get("event_type")
|
|
81
|
+
if isinstance(event_type, str):
|
|
82
|
+
data = {
|
|
83
|
+
**data,
|
|
84
|
+
"event_type": AuditEventType(event_type),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
timestamp = data.get("timestamp")
|
|
88
|
+
if isinstance(timestamp, str):
|
|
89
|
+
data = {**data, "timestamp": datetime.fromisoformat(timestamp)}
|
|
90
|
+
|
|
91
|
+
return cls(**data)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Audit logging for knowledge management operations."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
from uuid import uuid4
|
|
5
|
+
|
|
6
|
+
from .events import AuditEvent, AuditEventType
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AuditLogger:
|
|
10
|
+
"""
|
|
11
|
+
Audit logging system for tracking all operations.
|
|
12
|
+
|
|
13
|
+
Maintains a complete audit trail for compliance and debugging.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
"""Initialize audit logger."""
|
|
18
|
+
self._events: List[AuditEvent] = []
|
|
19
|
+
|
|
20
|
+
def log_event(
|
|
21
|
+
self,
|
|
22
|
+
event_type: AuditEventType,
|
|
23
|
+
tenant_id: str,
|
|
24
|
+
user_id: str,
|
|
25
|
+
resource_type: str,
|
|
26
|
+
resource_id: str,
|
|
27
|
+
action: str,
|
|
28
|
+
changes: Optional[Dict[str, Any]] = None,
|
|
29
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
30
|
+
ip_address: Optional[str] = None,
|
|
31
|
+
user_agent: Optional[str] = None,
|
|
32
|
+
) -> AuditEvent:
|
|
33
|
+
"""
|
|
34
|
+
Log an audit event.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
event_type: Type of event
|
|
38
|
+
tenant_id: Tenant ID
|
|
39
|
+
user_id: User performing action
|
|
40
|
+
resource_type: Type of resource (item, collection, etc.)
|
|
41
|
+
resource_id: ID of resource
|
|
42
|
+
action: Action performed (create, update, delete, etc.)
|
|
43
|
+
changes: Dictionary of changes made
|
|
44
|
+
metadata: Optional metadata
|
|
45
|
+
ip_address: Optional IP address of user
|
|
46
|
+
user_agent: Optional user agent string
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
AuditEvent: Logged event
|
|
50
|
+
"""
|
|
51
|
+
event = AuditEvent(
|
|
52
|
+
event_id=str(uuid4()),
|
|
53
|
+
event_type=event_type,
|
|
54
|
+
tenant_id=tenant_id,
|
|
55
|
+
user_id=user_id,
|
|
56
|
+
resource_type=resource_type,
|
|
57
|
+
resource_id=resource_id,
|
|
58
|
+
action=action,
|
|
59
|
+
changes=changes or {},
|
|
60
|
+
metadata=metadata or {},
|
|
61
|
+
ip_address=ip_address,
|
|
62
|
+
user_agent=user_agent,
|
|
63
|
+
)
|
|
64
|
+
self._events.append(event)
|
|
65
|
+
return event
|
|
66
|
+
|
|
67
|
+
def get_events(
|
|
68
|
+
self,
|
|
69
|
+
tenant_id: Optional[str] = None,
|
|
70
|
+
user_id: Optional[str] = None,
|
|
71
|
+
resource_id: Optional[str] = None,
|
|
72
|
+
event_type: Optional[AuditEventType] = None,
|
|
73
|
+
limit: int = 100,
|
|
74
|
+
offset: int = 0,
|
|
75
|
+
) -> List[AuditEvent]:
|
|
76
|
+
"""
|
|
77
|
+
Query audit events.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
tenant_id: Filter by tenant
|
|
81
|
+
user_id: Filter by user
|
|
82
|
+
resource_id: Filter by resource
|
|
83
|
+
event_type: Filter by event type
|
|
84
|
+
limit: Maximum events to return
|
|
85
|
+
offset: Number of events to skip
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
List[AuditEvent]: Matching events
|
|
89
|
+
"""
|
|
90
|
+
results = self._events
|
|
91
|
+
|
|
92
|
+
if tenant_id:
|
|
93
|
+
results = [e for e in results if e.tenant_id == tenant_id]
|
|
94
|
+
|
|
95
|
+
if user_id:
|
|
96
|
+
results = [e for e in results if e.user_id == user_id]
|
|
97
|
+
|
|
98
|
+
if resource_id:
|
|
99
|
+
results = [e for e in results if e.resource_id == resource_id]
|
|
100
|
+
|
|
101
|
+
if event_type:
|
|
102
|
+
results = [e for e in results if e.event_type == event_type]
|
|
103
|
+
|
|
104
|
+
# Return in reverse chronological order
|
|
105
|
+
results = list(reversed(results))
|
|
106
|
+
|
|
107
|
+
return results[offset : offset + limit]
|
|
108
|
+
|
|
109
|
+
def get_user_activity(
|
|
110
|
+
self,
|
|
111
|
+
tenant_id: str,
|
|
112
|
+
user_id: str,
|
|
113
|
+
limit: int = 50,
|
|
114
|
+
) -> List[AuditEvent]:
|
|
115
|
+
"""
|
|
116
|
+
Get activity for specific user.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
tenant_id: Tenant ID
|
|
120
|
+
user_id: User ID
|
|
121
|
+
limit: Maximum events to return
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
List[AuditEvent]: User's recent actions
|
|
125
|
+
"""
|
|
126
|
+
return self.get_events(
|
|
127
|
+
tenant_id=tenant_id,
|
|
128
|
+
user_id=user_id,
|
|
129
|
+
limit=limit,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def get_resource_history(
|
|
133
|
+
self,
|
|
134
|
+
resource_id: str,
|
|
135
|
+
limit: int = 50,
|
|
136
|
+
) -> List[AuditEvent]:
|
|
137
|
+
"""
|
|
138
|
+
Get change history for specific resource.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
resource_id: Resource ID
|
|
142
|
+
limit: Maximum events to return
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
List[AuditEvent]: Resource history
|
|
146
|
+
"""
|
|
147
|
+
return self.get_events(
|
|
148
|
+
resource_id=resource_id,
|
|
149
|
+
limit=limit,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def get_tenant_audit_log(
|
|
153
|
+
self,
|
|
154
|
+
tenant_id: str,
|
|
155
|
+
limit: int = 100,
|
|
156
|
+
) -> List[AuditEvent]:
|
|
157
|
+
"""
|
|
158
|
+
Get complete audit log for tenant.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
tenant_id: Tenant ID
|
|
162
|
+
limit: Maximum events to return
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
List[AuditEvent]: Tenant audit trail
|
|
166
|
+
"""
|
|
167
|
+
return self.get_events(
|
|
168
|
+
tenant_id=tenant_id,
|
|
169
|
+
limit=limit,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def clear_events(self, tenant_id: Optional[str] = None) -> int:
|
|
173
|
+
"""
|
|
174
|
+
Clear audit events.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
tenant_id: If specified, only clear events for this tenant
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
int: Number of events cleared
|
|
181
|
+
"""
|
|
182
|
+
if not tenant_id:
|
|
183
|
+
count = len(self._events)
|
|
184
|
+
self._events = []
|
|
185
|
+
return count
|
|
186
|
+
|
|
187
|
+
original_count = len(self._events)
|
|
188
|
+
self._events = [e for e in self._events if e.tenant_id != tenant_id]
|
|
189
|
+
return original_count - len(self._events)
|
|
190
|
+
|
|
191
|
+
def count_events(
|
|
192
|
+
self,
|
|
193
|
+
tenant_id: Optional[str] = None,
|
|
194
|
+
user_id: Optional[str] = None,
|
|
195
|
+
) -> int:
|
|
196
|
+
"""
|
|
197
|
+
Count audit events.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
tenant_id: Optional tenant filter
|
|
201
|
+
user_id: Optional user filter
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
int: Number of matching events
|
|
205
|
+
"""
|
|
206
|
+
count = len(self._events)
|
|
207
|
+
|
|
208
|
+
if tenant_id:
|
|
209
|
+
count = len([e for e in self._events if e.tenant_id == tenant_id])
|
|
210
|
+
|
|
211
|
+
if user_id:
|
|
212
|
+
count = len(
|
|
213
|
+
[e for e in self._events if e.tenant_id == tenant_id and e.user_id == user_id]
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return count
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Collaboration and concurrent edit management module."""
|
|
2
|
+
|
|
3
|
+
from .conflict import Conflict, ConflictDetector, ConflictResolutionStrategy, ConflictType
|
|
4
|
+
from .locks import LockToken, OptimisticLockManager
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"LockToken",
|
|
8
|
+
"OptimisticLockManager",
|
|
9
|
+
"Conflict",
|
|
10
|
+
"ConflictType",
|
|
11
|
+
"ConflictDetector",
|
|
12
|
+
"ConflictResolutionStrategy",
|
|
13
|
+
]
|