Skip to content

Commit 26d166d

Browse files
fix: add missing praisonai_platform.db module (fixes #1542) (#1547)
* fix: add missing praisonai_platform.db module (fixes #1542) - Create missing db/ directory with __init__.py, base.py, and models.py - Add all required database models: User, Workspace, Agent, Issue, etc. - Implement SQLAlchemy base configuration with init_db, get_session - Fix package imports with lazy loading in __init__.py - Resolves ModuleNotFoundError that prevented tests from running Tests now begin executing instead of failing at import. Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com> * fix: address critical P1 runtime issues found by reviewers - Fix User model field mismatch: change username/hashed_password to name/password_hash to match auth_service.py expectations - Fix get_session() signature incompatibility: update deps.py to properly use async generator pattern - Add missing foreign key constraints for user references (assignee_id, created_by_id, author_id, actor_id) - Fix timezone handling: add timezone=True to all DateTime columns to match _utcnow() - Make reset_engine async and properly dispose connections to prevent leaks - Add database_url parameter to get_engine for test flexibility - Remove unused imports (Engine, Boolean) - Fix wildcard import in db/__init__.py using explicit module import for side effects - Add __dir__ method to main __init__.py for better tab completion Fixes all P1 runtime issues identified by Greptile and CodeRabbit reviewers. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com> --------- Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com> Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com> Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com>
1 parent ac067f7 commit 26d166d

5 files changed

Lines changed: 330 additions & 4 deletions

File tree

src/praisonai-platform/praisonai_platform/__init__.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,21 @@
1919

2020
__version__ = "0.1.0"
2121

22-
from .api.app import create_app
23-
from .client.platform_client import PlatformClient
22+
23+
def __getattr__(name: str):
24+
"""Lazy import for platform components."""
25+
if name == "create_app":
26+
from .api.app import create_app
27+
return create_app
28+
elif name == "PlatformClient":
29+
from .client.platform_client import PlatformClient
30+
return PlatformClient
31+
raise AttributeError(f"module 'praisonai_platform' has no attribute '{name}'")
32+
33+
34+
def __dir__() -> list[str]:
35+
return sorted(list(globals().keys()) + ["create_app", "PlatformClient"])
36+
2437

2538
__all__ = [
2639
"__version__",

src/praisonai-platform/praisonai_platform/api/deps.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@
1616

1717
async def get_db() -> AsyncGenerator[AsyncSession, None]:
1818
"""Yield an async DB session per request."""
19-
factory = get_session()
20-
async with factory() as session:
19+
async for session in get_session():
2120
try:
2221
yield session
2322
await session.commit()
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""Database module for praisonai-platform."""
2+
3+
from .base import Base, get_engine, get_session, init_db, reset_engine
4+
from . import models # noqa: F401 # ensure ORM classes are registered with Base.metadata
5+
6+
__all__ = [
7+
"Base",
8+
"get_session",
9+
"init_db",
10+
"reset_engine",
11+
"get_engine",
12+
]
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""Database base configuration for praisonai-platform."""
2+
3+
import os
4+
from typing import AsyncGenerator
5+
6+
from sqlalchemy.ext.asyncio import (
7+
AsyncEngine,
8+
AsyncSession,
9+
async_sessionmaker,
10+
create_async_engine,
11+
)
12+
from sqlalchemy.orm import DeclarativeBase
13+
14+
15+
class Base(DeclarativeBase):
16+
"""Base class for all database models."""
17+
pass
18+
19+
20+
# Global engine instance
21+
_engine: AsyncEngine | None = None
22+
_session_factory: async_sessionmaker[AsyncSession] | None = None
23+
24+
25+
def get_engine(database_url: str | None = None) -> AsyncEngine:
26+
"""Get (and lazily create) the cached async database engine.
27+
28+
If ``database_url`` is provided and no engine is cached yet, it overrides
29+
the ``DATABASE_URL`` env var / in-memory default. To switch URLs after an
30+
engine has already been created, call ``reset_engine()`` first.
31+
"""
32+
global _engine
33+
if _engine is None:
34+
if database_url is None:
35+
# Default to in-memory SQLite for testing
36+
database_url = os.environ.get(
37+
"DATABASE_URL",
38+
"sqlite+aiosqlite:///:memory:",
39+
)
40+
_engine = create_async_engine(
41+
database_url,
42+
echo=False,
43+
connect_args={"check_same_thread": False} if "sqlite" in database_url else {},
44+
)
45+
return _engine
46+
47+
48+
async def reset_engine() -> None:
49+
"""Reset the global engine (for testing).
50+
51+
Disposes the existing async engine (if any) before clearing
52+
cached references so connection pools don't leak across tests.
53+
"""
54+
global _engine, _session_factory
55+
if _engine is not None:
56+
await _engine.dispose()
57+
_engine = None
58+
_session_factory = None
59+
60+
61+
def get_session_factory() -> async_sessionmaker[AsyncSession]:
62+
"""Get the session factory."""
63+
global _session_factory
64+
if _session_factory is None:
65+
_session_factory = async_sessionmaker(
66+
bind=get_engine(),
67+
class_=AsyncSession,
68+
expire_on_commit=False,
69+
)
70+
return _session_factory
71+
72+
73+
async def get_session() -> AsyncGenerator[AsyncSession, None]:
74+
"""Get a database session."""
75+
factory = get_session_factory()
76+
async with factory() as session:
77+
yield session
78+
79+
80+
async def init_db() -> None:
81+
"""Initialize the database (create all tables)."""
82+
engine = get_engine()
83+
async with engine.begin() as conn:
84+
await conn.run_sync(Base.metadata.create_all)
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
"""Database models for praisonai-platform."""
2+
3+
import uuid
4+
from datetime import datetime, timezone
5+
from typing import Any, Dict, List, Optional
6+
7+
from sqlalchemy import (
8+
DateTime,
9+
ForeignKey,
10+
Integer,
11+
JSON,
12+
String,
13+
Text,
14+
UniqueConstraint,
15+
)
16+
from sqlalchemy.orm import Mapped, mapped_column, relationship
17+
18+
from .base import Base
19+
20+
21+
def _uuid() -> str:
22+
"""Generate a new UUID string."""
23+
return str(uuid.uuid4())
24+
25+
26+
def _utcnow() -> datetime:
27+
"""Get current UTC datetime."""
28+
return datetime.now(timezone.utc)
29+
30+
31+
class User(Base):
32+
"""User model for authentication."""
33+
__tablename__ = "users"
34+
35+
id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid)
36+
name: Mapped[str] = mapped_column(String, unique=True, nullable=False)
37+
email: Mapped[str] = mapped_column(String, unique=True, nullable=False)
38+
password_hash: Mapped[str] = mapped_column(String, nullable=False)
39+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
40+
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow)
41+
42+
# Relationships
43+
memberships: Mapped[List["Member"]] = relationship("Member", back_populates="user")
44+
45+
46+
class Workspace(Base):
47+
"""Workspace model for organizing work."""
48+
__tablename__ = "workspaces"
49+
50+
id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid)
51+
name: Mapped[str] = mapped_column(String, nullable=False)
52+
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
53+
settings: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, nullable=True, default=dict)
54+
issue_prefix: Mapped[str] = mapped_column(String, default="ISSUE")
55+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
56+
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow)
57+
58+
# Relationships
59+
members: Mapped[List["Member"]] = relationship("Member", back_populates="workspace")
60+
projects: Mapped[List["Project"]] = relationship("Project", back_populates="workspace")
61+
issues: Mapped[List["Issue"]] = relationship("Issue", back_populates="workspace")
62+
agents: Mapped[List["Agent"]] = relationship("Agent", back_populates="workspace")
63+
64+
65+
class Member(Base):
66+
"""Workspace membership model."""
67+
__tablename__ = "members"
68+
69+
id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid)
70+
user_id: Mapped[str] = mapped_column(String, ForeignKey("users.id"), nullable=False)
71+
workspace_id: Mapped[str] = mapped_column(String, ForeignKey("workspaces.id"), nullable=False)
72+
role: Mapped[str] = mapped_column(String, nullable=False, default="member") # owner, admin, member
73+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
74+
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow)
75+
76+
# Relationships
77+
user: Mapped["User"] = relationship("User", back_populates="memberships")
78+
workspace: Mapped["Workspace"] = relationship("Workspace", back_populates="members")
79+
80+
__table_args__ = (
81+
UniqueConstraint("user_id", "workspace_id", name="uq_member_user_workspace"),
82+
)
83+
84+
85+
class Project(Base):
86+
"""Project model for organizing issues."""
87+
__tablename__ = "projects"
88+
89+
id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid)
90+
workspace_id: Mapped[str] = mapped_column(String, ForeignKey("workspaces.id"), nullable=False)
91+
name: Mapped[str] = mapped_column(String, nullable=False)
92+
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
93+
settings: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, nullable=True, default=dict)
94+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
95+
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow)
96+
97+
# Relationships
98+
workspace: Mapped["Workspace"] = relationship("Workspace", back_populates="projects")
99+
issues: Mapped[List["Issue"]] = relationship("Issue", back_populates="project")
100+
101+
102+
class Issue(Base):
103+
"""Issue model for tracking work items."""
104+
__tablename__ = "issues"
105+
106+
id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid)
107+
workspace_id: Mapped[str] = mapped_column(String, ForeignKey("workspaces.id"), nullable=False)
108+
project_id: Mapped[Optional[str]] = mapped_column(String, ForeignKey("projects.id"), nullable=True)
109+
title: Mapped[str] = mapped_column(String, nullable=False)
110+
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
111+
status: Mapped[str] = mapped_column(String, nullable=False, default="backlog")
112+
priority: Mapped[str] = mapped_column(String, nullable=False, default="medium")
113+
assignee_id: Mapped[Optional[str]] = mapped_column(String, ForeignKey("users.id"), nullable=True)
114+
created_by_id: Mapped[Optional[str]] = mapped_column(String, ForeignKey("users.id"), nullable=True)
115+
issue_number: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
116+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
117+
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow)
118+
119+
# Relationships
120+
workspace: Mapped["Workspace"] = relationship("Workspace", back_populates="issues")
121+
project: Mapped[Optional["Project"]] = relationship("Project", back_populates="issues")
122+
comments: Mapped[List["Comment"]] = relationship("Comment", back_populates="issue")
123+
labels: Mapped[List["IssueLabelLink"]] = relationship("IssueLabelLink", back_populates="issue")
124+
125+
126+
class Comment(Base):
127+
"""Comment model for issue discussions."""
128+
__tablename__ = "comments"
129+
130+
id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid)
131+
issue_id: Mapped[str] = mapped_column(String, ForeignKey("issues.id"), nullable=False)
132+
author_id: Mapped[Optional[str]] = mapped_column(String, ForeignKey("users.id"), nullable=True)
133+
content: Mapped[str] = mapped_column(Text, nullable=False)
134+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
135+
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow)
136+
137+
# Relationships
138+
issue: Mapped["Issue"] = relationship("Issue", back_populates="comments")
139+
140+
141+
class Agent(Base):
142+
"""Agent model for AI agents in workspace."""
143+
__tablename__ = "agents"
144+
145+
id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid)
146+
workspace_id: Mapped[str] = mapped_column(String, ForeignKey("workspaces.id"), nullable=False)
147+
name: Mapped[str] = mapped_column(String, nullable=False)
148+
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
149+
status: Mapped[str] = mapped_column(String, nullable=False, default="idle")
150+
config: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, nullable=True, default=dict)
151+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
152+
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow)
153+
154+
# Relationships
155+
workspace: Mapped["Workspace"] = relationship("Workspace", back_populates="agents")
156+
157+
158+
class IssueLabel(Base):
159+
"""Label definitions for issues."""
160+
__tablename__ = "issue_labels"
161+
162+
id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid)
163+
workspace_id: Mapped[str] = mapped_column(String, ForeignKey("workspaces.id"), nullable=False)
164+
name: Mapped[str] = mapped_column(String, nullable=False)
165+
color: Mapped[str] = mapped_column(String, nullable=False, default="#000000")
166+
description: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
167+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
168+
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow)
169+
170+
# Relationships
171+
issues: Mapped[List["IssueLabelLink"]] = relationship("IssueLabelLink", back_populates="label")
172+
173+
174+
class IssueLabelLink(Base):
175+
"""Many-to-many relationship between issues and labels."""
176+
__tablename__ = "issue_label_links"
177+
178+
id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid)
179+
issue_id: Mapped[str] = mapped_column(String, ForeignKey("issues.id"), nullable=False)
180+
label_id: Mapped[str] = mapped_column(String, ForeignKey("issue_labels.id"), nullable=False)
181+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
182+
183+
# Relationships
184+
issue: Mapped["Issue"] = relationship("Issue", back_populates="labels")
185+
label: Mapped["IssueLabel"] = relationship("IssueLabel", back_populates="issues")
186+
187+
__table_args__ = (
188+
UniqueConstraint("issue_id", "label_id", name="uq_issue_label"),
189+
)
190+
191+
192+
class IssueDependency(Base):
193+
"""Issue dependency relationships."""
194+
__tablename__ = "issue_dependencies"
195+
196+
id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid)
197+
issue_id: Mapped[str] = mapped_column(String, ForeignKey("issues.id"), nullable=False)
198+
depends_on_id: Mapped[str] = mapped_column(String, ForeignKey("issues.id"), nullable=False)
199+
dependency_type: Mapped[str] = mapped_column(String, nullable=False) # blocks, blocked_by, related
200+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)
201+
202+
__table_args__ = (
203+
UniqueConstraint("issue_id", "depends_on_id", "dependency_type", name="uq_issue_dependency"),
204+
)
205+
206+
207+
class ActivityLog(Base):
208+
"""Activity log for tracking changes."""
209+
__tablename__ = "activity_logs"
210+
211+
id: Mapped[str] = mapped_column(String, primary_key=True, default=_uuid)
212+
workspace_id: Mapped[str] = mapped_column(String, ForeignKey("workspaces.id"), nullable=False)
213+
entity_type: Mapped[str] = mapped_column(String, nullable=False) # issue, project, workspace, etc.
214+
entity_id: Mapped[str] = mapped_column(String, nullable=False)
215+
action: Mapped[str] = mapped_column(String, nullable=False) # created, updated, deleted, etc.
216+
actor_id: Mapped[Optional[str]] = mapped_column(String, ForeignKey("users.id"), nullable=True)
217+
details: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, nullable=True, default=dict)
218+
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=_utcnow)

0 commit comments

Comments
 (0)