Coverage for integrations / social / models.py: 86.2%

87 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-12 04:49 +0000

1""" 

2HevolveSocial — Slim imports file. 

3 

4All table classes (schema + behavior) are defined in hevolve-database (sql.models). 

5This file provides: 

6 1. HARTOS-specific DB infrastructure (URL resolution, engine, sessions) 

7 2. Re-exports of all model classes so existing HARTOS imports keep working 

8 

9Usage (unchanged): 

10 from integrations.social.models import db_session, User, Post, Community 

11""" 

12import os 

13import uuid 

14from datetime import datetime 

15 

16from sqlalchemy import ( 

17 create_engine, event, Column, String, Text, Integer, Float, Boolean, 

18 DateTime, JSON, ForeignKey, UniqueConstraint, Index, func 

19) 

20from sqlalchemy.orm import declarative_base, sessionmaker, relationship, Session 

21 

22# Import shared Base from hevolve-database (single metadata registry). 

23# Fallback to local Base if hevolve-database is not installed (e.g., dev without pip install). 

24try: 

25 from sql.database import Base 

26except ImportError: 

27 Base = declarative_base() 

28 

29# Security: HTML sanitization for user-generated content (XSS prevention) 

30try: 

31 from security.sanitize import sanitize_html as _sanitize_html 

32except ImportError: 

33 import html as _html_module 

34 def _sanitize_html(text): 

35 """Minimal fallback: escape HTML entities to prevent XSS.""" 

36 if not isinstance(text, str): 

37 return text 

38 return _html_module.escape(text) 

39 

40# ── Database URL resolution ────────────────────────────────── 

41# Priority: HEVOLVE_DB_URL > DATABASE_URL > HEVOLVE_DB_PATH > SOCIAL_DB_PATH > auto-detect 

42# 

43# Cloud/Docker deployments MUST set HEVOLVE_DB_URL (or DATABASE_URL) pointing to 

44# the remote MySQL/PostgreSQL instance. SQLite fallback is only for local dev/standalone. 

45# Detection: /.dockerenv file, DOCKER_CONTAINER env, or HEVOLVE_CLOUD_MODE env. 

46_DB_URL_ENV = os.environ.get('HEVOLVE_DB_URL') or os.environ.get('DATABASE_URL') 

47_DB_PATH_ENV = os.environ.get('HEVOLVE_DB_PATH') or os.environ.get('SOCIAL_DB_PATH') 

48 

49_IS_DOCKER = ( 

50 os.path.exists('/.dockerenv') 

51 or os.environ.get('DOCKER_CONTAINER') == 'true' 

52 or os.environ.get('HEVOLVE_CLOUD_MODE') == 'true' 

53) 

54 

55if _DB_URL_ENV: 

56 # Full URL override (MySQL, PostgreSQL, or SQLite) — aligns with Hevolve_Database 

57 DB_URL = _DB_URL_ENV 

58 DB_PATH = ':memory:' if DB_URL == 'sqlite://' else None 

59elif _IS_DOCKER and not _DB_PATH_ENV: 

60 # Cloud/Docker mode WITHOUT a DB URL configured — this is a misconfiguration. 

61 # Refuse to silently fall back to SQLite; log a loud warning and use in-memory 

62 # so the server starts (health checks pass) but makes the problem obvious. 

63 import logging as _logging_models 

64 _logging_models.getLogger(__name__).critical( 

65 'CLOUD/DOCKER MODE: HEVOLVE_DB_URL or DATABASE_URL not set! ' 

66 'Set HEVOLVE_DB_URL=mysql+pymysql://user:pass@host/db to connect to cloud DB. ' 

67 'Falling back to in-memory SQLite — NO DATA WILL PERSIST.' 

68 ) 

69 DB_PATH = ':memory:' 

70 DB_URL = 'sqlite://' 

71elif _DB_PATH_ENV == ':memory:': 

72 DB_PATH = ':memory:' 

73 DB_URL = 'sqlite://' 

74else: 

75 import sys as _sys_models 

76 if _DB_PATH_ENV: 

77 DB_PATH = _DB_PATH_ENV 

78 elif os.environ.get('NUNBA_BUNDLED') or getattr(_sys_models, 'frozen', False): 

79 # Bundled mode: cross-platform writable data dir 

80 try: 

81 from core.platform_paths import get_db_path as _get_db_path 

82 DB_PATH = _get_db_path('hevolve_database.db') 

83 except ImportError: 

84 DB_PATH = os.path.join(os.path.expanduser('~'), 'Documents', 'Nunba', 'data', 'hevolve_database.db') 

85 else: 

86 DB_PATH = os.path.join( 

87 os.path.dirname(__file__), '..', '..', 'agent_data', 'hevolve_database.db') 

88 DB_URL = f"sqlite:///{os.path.abspath(DB_PATH)}" 

89 

90import threading as _threading_models 

91 

92_engine = None 

93_SessionLocal = None 

94_engine_lock = _threading_models.Lock() 

95_session_lock = _threading_models.Lock() 

96 

97 

98def _uuid(): 

99 return str(uuid.uuid4()) 

100 

101 

102def get_engine(): 

103 global _engine 

104 if _engine is None: 

105 with _engine_lock: 

106 if _engine is not None: 

107 return _engine 

108 

109 _is_sqlite = DB_URL.startswith('sqlite') 

110 _is_memory = DB_URL == 'sqlite://' 

111 

112 # Ensure parent directory exists for file-based SQLite 

113 if _is_sqlite and not _is_memory and DB_PATH: 

114 os.makedirs(os.path.dirname(os.path.abspath(DB_PATH)), exist_ok=True) 

115 

116 if _is_sqlite: 

117 if _is_memory: 

118 from sqlalchemy.pool import StaticPool 

119 engine_kwargs = dict( 

120 echo=False, 

121 future=True, 

122 connect_args={"check_same_thread": False}, 

123 poolclass=StaticPool, 

124 pool_pre_ping=True, 

125 ) 

126 else: 

127 # File-based SQLite: NullPool — each thread gets its own 

128 # short-lived connection. QueuePool holds connections open 

129 # across threads → "database is locked" under concurrent 

130 # daemon writes. 

131 from sqlalchemy.pool import NullPool 

132 engine_kwargs = dict( 

133 echo=False, 

134 future=True, 

135 connect_args={"check_same_thread": False}, 

136 poolclass=NullPool, 

137 ) 

138 else: 

139 # MySQL / PostgreSQL (via HEVOLVE_DB_URL) 

140 engine_kwargs = dict( 

141 echo=False, 

142 future=True, 

143 pool_pre_ping=True, 

144 pool_size=20, 

145 max_overflow=0, 

146 ) 

147 

148 _engine = create_engine(DB_URL, **engine_kwargs) 

149 

150 # Enable WAL mode for file-based SQLite 

151 # WAL allows concurrent reads + one writer without blocking readers. 

152 # busy_timeout=3000 (3s) — fail fast rather than blocking daemon threads 

153 # for 15-30s which triggers watchdog restarts. 

154 # 

155 # NullPool means every checkout creates a fresh connection, so 

156 # this hook fires per-checkout, not per-engine. Combining the 

157 # three PRAGMAs into a single executescript call halves the 

158 # per-checkout overhead vs. three separate cursor.execute 

159 # round-trips (matters under daemon-tick storm — py-spy traces 

160 # showed connection setup as a top-N consumer when the 

161 # speculative dispatcher iterated goals × idle_agents). 

162 if _is_sqlite and not _is_memory: 

163 @event.listens_for(_engine, "connect") 

164 def _set_sqlite_wal(dbapi_connection, connection_record): 

165 cursor = dbapi_connection.cursor() 

166 cursor.executescript( 

167 "PRAGMA journal_mode=WAL;" 

168 "PRAGMA busy_timeout=3000;" 

169 "PRAGMA synchronous=NORMAL;" 

170 ) 

171 cursor.close() 

172 return _engine 

173 

174 

175def get_session_factory(): 

176 global _SessionLocal 

177 if _SessionLocal is None: 

178 with _session_lock: 

179 if _SessionLocal is None: 

180 _SessionLocal = sessionmaker(bind=get_engine(), expire_on_commit=False) 

181 return _SessionLocal 

182 

183 

184def get_db() -> Session: 

185 factory = get_session_factory() 

186 return factory() 

187 

188 

189def db_session(commit=True): 

190 """Context manager for database sessions with automatic commit/rollback/close. 

191 

192 Usage: 

193 with db_session() as db: 

194 user = db.query(User).filter_by(id=uid).first() 

195 user.name = 'new' 

196 # auto-commits on clean exit, auto-rollbacks on exception, always closes 

197 """ 

198 from contextlib import contextmanager 

199 

200 @contextmanager 

201 def _session_cm(): 

202 db = get_db() 

203 try: 

204 yield db 

205 if commit: 

206 db.commit() 

207 except Exception: 

208 db.rollback() 

209 raise 

210 finally: 

211 db.close() 

212 

213 return _session_cm() 

214 

215 

216def init_db(): 

217 engine = get_engine() 

218 Base.metadata.create_all(engine) 

219 

220 

221# ═══════════════════════════════════════════════════════════════ 

222# MODEL IMPORTS — Canonical source: hevolve-database (sql.models) 

223# Fallback: local definitions in _models_local.py (for Docker/standalone) 

224# 

225# The 3 collision classes (SocialUser, SocialPost, SocialComment) are aliased 

226# to User, Post, Comment so all existing HARTOS code keeps working. 

227# ═══════════════════════════════════════════════════════════════ 

228 

229try: 

230 from sql.models import ( # noqa: E402, F401 

231 # Collision classes — aliased for HARTOS backward compatibility 

232 SocialUser as User, 

233 SocialPost as Post, 

234 SocialComment as Comment, 

235 # Non-collision classes — direct import 

236 Region, Community, Vote, Follow, CommunityMembership, 

237 AgentSkillBadge, TaskRequest, Notification, Report, RecipeShare, 

238 PeerNode, InstanceFollow, FederatedPost, ResonanceWallet, 

239 ResonanceTransaction, Achievement, UserAchievement, Season, 

240 Challenge, UserChallenge, RegionMembership, Encounter, Rating, 

241 TrustScore, AgentEvolution, AgentCollaboration, Referral, 

242 ReferralCode, Boost, OnboardingProgress, Campaign, CampaignAction, 

243 LocationPing, ProximityMatch, MissedConnection, 

244 MissedConnectionResponse, AdUnit, AdPlacement, AdImpression, 

245 HostingReward, NodeAttestation, IntegrityChallenge, FraudAlert, 

246 RegionAssignment, SyncQueue, CodingGoal, CodingTask, 

247 CodingSubmission, Product, AgentGoal, IPPatent, IPInfringement, 

248 DefensivePublication, CommercialAPIKey, APIUsageLog, BuildLicense, 

249 GuestRecovery, DeviceBinding, BackupMetadata, RegionalHostRequest, 

250 FleetCommand, ProvisionedNode, ThoughtExperiment, ExperimentVote, 

251 PaperPortfolio, PaperTrade, ComputeEscrow, MeteredAPIUsage, 

252 NodeComputeConfig, AuditLogEntry, GameSession, GameParticipant, 

253 ShareableLink, ShareEvent, UserConsent, MarketplaceListing, 

254 ListingReview, MCPServer, MCPTool, ComputePledge, PledgeConsumption, 

255 UserChannelBinding, ConversationEntry, ChannelPresence, 

256 DiscoverablePref, EncounterSighting, 

257 ) 

258except ImportError: 

259 # Standalone/Docker mode: sql package not installed, use local definitions 

260 from integrations.social._models_local import ( # noqa: E402, F401 

261 User, Post, Comment, Region, Community, Vote, Follow, 

262 CommunityMembership, AgentSkillBadge, TaskRequest, Notification, 

263 Report, RecipeShare, PeerNode, InstanceFollow, FederatedPost, 

264 ResonanceWallet, ResonanceTransaction, Achievement, UserAchievement, 

265 Season, Challenge, UserChallenge, RegionMembership, Encounter, 

266 Rating, TrustScore, AgentEvolution, AgentCollaboration, Referral, 

267 ReferralCode, Boost, OnboardingProgress, Campaign, CampaignAction, 

268 LocationPing, ProximityMatch, MissedConnection, 

269 MissedConnectionResponse, AdUnit, AdPlacement, AdImpression, 

270 HostingReward, NodeAttestation, IntegrityChallenge, FraudAlert, 

271 RegionAssignment, SyncQueue, CodingGoal, CodingTask, 

272 CodingSubmission, Product, AgentGoal, IPPatent, IPInfringement, 

273 DefensivePublication, CommercialAPIKey, APIUsageLog, BuildLicense, 

274 GuestRecovery, DeviceBinding, BackupMetadata, RegionalHostRequest, 

275 FleetCommand, ProvisionedNode, ThoughtExperiment, ExperimentVote, 

276 PaperPortfolio, PaperTrade, ComputeEscrow, MeteredAPIUsage, 

277 NodeComputeConfig, AuditLogEntry, GameSession, GameParticipant, 

278 ShareableLink, ShareEvent, UserConsent, MarketplaceListing, 

279 ListingReview, MCPServer, MCPTool, ComputePledge, PledgeConsumption, 

280 UserChannelBinding, ConversationEntry, ChannelPresence, 

281 DiscoverablePref, EncounterSighting, 

282 )