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
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 04:49 +0000
1"""
2HevolveSocial — Slim imports file.
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
9Usage (unchanged):
10 from integrations.social.models import db_session, User, Post, Community
11"""
12import os
13import uuid
14from datetime import datetime
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
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()
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)
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')
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)
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)}"
90import threading as _threading_models
92_engine = None
93_SessionLocal = None
94_engine_lock = _threading_models.Lock()
95_session_lock = _threading_models.Lock()
98def _uuid():
99 return str(uuid.uuid4())
102def get_engine():
103 global _engine
104 if _engine is None:
105 with _engine_lock:
106 if _engine is not None:
107 return _engine
109 _is_sqlite = DB_URL.startswith('sqlite')
110 _is_memory = DB_URL == 'sqlite://'
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)
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 )
148 _engine = create_engine(DB_URL, **engine_kwargs)
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
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
184def get_db() -> Session:
185 factory = get_session_factory()
186 return factory()
189def db_session(commit=True):
190 """Context manager for database sessions with automatic commit/rollback/close.
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
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()
213 return _session_cm()
216def init_db():
217 engine = get_engine()
218 Base.metadata.create_all(engine)
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# ═══════════════════════════════════════════════════════════════
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 )