Initial SFERA platform baseline

This commit is contained in:
2026-05-16 19:03:49 +03:00
commit 3b845c8fce
282 changed files with 55045 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
# sfera-collaboration
In-memory collaboration primitives for SFERA.
Provides:
- users, workspaces, projects, and tasks;
- task-linked change sessions with explicit finish time;
- project/target scoped comments;
- activity feed events;
- project/target scoped ownership assignments.
+10
View File
@@ -0,0 +1,10 @@
[project]
name = "sfera-collaboration"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"pydantic>=2.0",
]
[tool.uv]
package = true
@@ -0,0 +1,196 @@
from __future__ import annotations
from datetime import datetime, timezone
from enum import Enum
from pydantic import BaseModel, Field
class TaskStatus(str, Enum):
OPEN = "OPEN"
IN_PROGRESS = "IN_PROGRESS"
DONE = "DONE"
CANCELED = "CANCELED"
class User(BaseModel):
user_id: str
display_name: str
email: str | None = None
class Workspace(BaseModel):
workspace_id: str
name: str
owner_user_id: str | None = None
class Project(BaseModel):
project_id: str
workspace_id: str
name: str
class Task(BaseModel):
task_id: str
project_id: str
title: str
status: TaskStatus = TaskStatus.OPEN
assignee_user_id: str | None = None
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class ChangeSession(BaseModel):
session_id: str
task_id: str
user_id: str
started_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
finished_at: datetime | None = None
class Comment(BaseModel):
comment_id: str
project_id: str
target_id: str
user_id: str
body: str
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
class ActivityEvent(BaseModel):
event_id: str
project_id: str
actor_user_id: str
verb: str
target_id: str
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
attributes: dict = Field(default_factory=dict)
class Ownership(BaseModel):
owner_user_id: str
project_id: str
target_id: str
role: str = "OWNER"
assigned_by: str | None = None
assigned_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
attributes: dict = Field(default_factory=dict)
class InMemoryCollaborationStore:
def __init__(self) -> None:
self.users: dict[str, User] = {}
self.workspaces: dict[str, Workspace] = {}
self.projects: dict[str, Project] = {}
self.tasks: dict[str, Task] = {}
self.sessions: dict[str, ChangeSession] = {}
self.comments: dict[str, Comment] = {}
self.activities: list[ActivityEvent] = []
self.ownership: dict[str, Ownership] = {}
def upsert_user(self, user: User) -> User:
self.users[user.user_id] = user
return user
def upsert_workspace(self, workspace: Workspace) -> Workspace:
self.workspaces[workspace.workspace_id] = workspace
return workspace
def upsert_project(self, project: Project) -> Project:
self.projects[project.project_id] = project
return project
def upsert_task(self, task: Task) -> Task:
self.tasks[task.task_id] = task
return task
def start_session(self, session: ChangeSession) -> ChangeSession:
if session.task_id not in self.tasks:
raise KeyError(f"Unknown task: {session.task_id}")
if session.user_id not in self.users:
raise KeyError(f"Unknown user: {session.user_id}")
self.sessions[session.session_id] = session
return session
def finish_session(self, session_id: str) -> ChangeSession:
session = self.sessions.get(session_id)
if session is None:
raise KeyError(f"Unknown session: {session_id}")
if session.finished_at is None:
session = session.model_copy(update={"finished_at": datetime.now(timezone.utc)})
self.sessions[session_id] = session
return session
def add_comment(self, comment: Comment) -> Comment:
if comment.user_id not in self.users:
raise KeyError(f"Unknown user: {comment.user_id}")
self.comments[comment.comment_id] = comment
return comment
def comments_for_project(self, project_id: str) -> list[Comment]:
return sorted(
[
comment
for comment in self.comments.values()
if comment.project_id == project_id
],
key=lambda item: item.created_at,
)
def comments_for_target(self, project_id: str, target_id: str) -> list[Comment]:
return [
comment
for comment in self.comments_for_project(project_id)
if comment.target_id == target_id
]
def add_activity(self, event: ActivityEvent) -> ActivityEvent:
self.activities.append(event)
return event
def assign_owner(self, ownership: Ownership) -> Ownership:
if ownership.owner_user_id not in self.users:
raise KeyError(f"Unknown user: {ownership.owner_user_id}")
self.ownership[self._ownership_key(ownership)] = ownership
return ownership
def owners_for_project(self, project_id: str) -> list[Ownership]:
return sorted(
[
ownership
for ownership in self.ownership.values()
if ownership.project_id == project_id
],
key=lambda item: (item.target_id, item.role, item.owner_user_id),
)
def owners_for_target(self, project_id: str, target_id: str) -> list[Ownership]:
return [
ownership
for ownership in self.owners_for_project(project_id)
if ownership.target_id == target_id
]
def activity_feed(self, project_id: str) -> list[ActivityEvent]:
return [
event
for event in sorted(self.activities, key=lambda item: item.created_at, reverse=True)
if event.project_id == project_id
]
def _ownership_key(self, ownership: Ownership) -> str:
return f"{ownership.project_id}:{ownership.target_id}:{ownership.role}:{ownership.owner_user_id}"
__all__ = [
"ActivityEvent",
"ChangeSession",
"Comment",
"InMemoryCollaborationStore",
"Ownership",
"Project",
"Task",
"TaskStatus",
"User",
"Workspace",
]
@@ -0,0 +1,62 @@
from collaboration import ChangeSession, Comment, InMemoryCollaborationStore, Ownership, Task, User
def test_start_session_requires_known_task_and_user():
store = InMemoryCollaborationStore()
store.upsert_user(User(user_id="user.1", display_name="Tester"))
store.upsert_task(Task(task_id="task.1", project_id="demo", title="Index project"))
session = store.start_session(
ChangeSession(session_id="session.1", task_id="task.1", user_id="user.1")
)
assert session.session_id == "session.1"
def test_finish_session_sets_finished_at_once():
store = InMemoryCollaborationStore()
store.upsert_user(User(user_id="user.1", display_name="Tester"))
store.upsert_task(Task(task_id="task.1", project_id="demo", title="Index project"))
store.start_session(ChangeSession(session_id="session.1", task_id="task.1", user_id="user.1"))
finished = store.finish_session("session.1")
finished_again = store.finish_session("session.1")
assert finished.finished_at is not None
assert finished_again.finished_at == finished.finished_at
def test_assign_owner_is_project_and_target_scoped():
store = InMemoryCollaborationStore()
store.upsert_user(User(user_id="user.1", display_name="Tester"))
ownership = store.assign_owner(
Ownership(
owner_user_id="user.1",
project_id="demo",
target_id="lineage.document.order",
role="RESPONSIBLE",
)
)
assert ownership.owner_user_id == "user.1"
assert store.owners_for_project("demo") == [ownership]
assert store.owners_for_target("demo", "lineage.document.order") == [ownership]
def test_add_comment_is_project_and_target_scoped():
store = InMemoryCollaborationStore()
store.upsert_user(User(user_id="user.1", display_name="Tester"))
comment = store.add_comment(
Comment(
comment_id="comment.1",
project_id="demo",
target_id="lineage.document.order",
user_id="user.1",
body="Check posting rules.",
)
)
assert store.comments_for_project("demo") == [comment]
assert store.comments_for_target("demo", "lineage.document.order") == [comment]