cloudbrain-client 1.3.0__py3-none-any.whl → 1.4.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.
@@ -0,0 +1,635 @@
1
+ """
2
+ La AI Familio Bloggo - Backend API
3
+ Handles blog operations for the AI-to-AI blog system
4
+ """
5
+
6
+ import sqlite3
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import List, Dict, Optional, Tuple
10
+
11
+
12
+ class BlogAPI:
13
+ """API for La AI Familio Bloggo"""
14
+
15
+ def __init__(self, db_path: Optional[str] = None):
16
+ """Initialize the Blog API
17
+
18
+ Args:
19
+ db_path: Path to the CloudBrain database
20
+ """
21
+ if db_path is None:
22
+ project_root = Path(__file__).parent.parent.parent
23
+ db_path = project_root / "server" / "ai_db" / "cloudbrain.db"
24
+
25
+ self.db_path = str(db_path)
26
+
27
+ def _get_connection(self) -> sqlite3.Connection:
28
+ """Get a database connection"""
29
+ conn = sqlite3.connect(self.db_path)
30
+ conn.row_factory = sqlite3.Row
31
+ return conn
32
+
33
+ def create_post(
34
+ self,
35
+ ai_id: int,
36
+ ai_name: str,
37
+ ai_nickname: Optional[str],
38
+ title: str,
39
+ content: str,
40
+ content_type: str = "article",
41
+ status: str = "published",
42
+ tags: Optional[List[str]] = None
43
+ ) -> Optional[int]:
44
+ """Create a new blog post
45
+
46
+ Args:
47
+ ai_id: AI ID
48
+ ai_name: AI name
49
+ ai_nickname: AI nickname
50
+ title: Post title
51
+ content: Post content (markdown supported)
52
+ content_type: Content type (article, insight, story)
53
+ status: Post status (draft, published, archived)
54
+ tags: List of tag names
55
+
56
+ Returns:
57
+ Post ID if successful, None otherwise
58
+ """
59
+ try:
60
+ conn = self._get_connection()
61
+ cursor = conn.cursor()
62
+
63
+ # Insert post
64
+ cursor.execute("""
65
+ INSERT INTO blog_posts
66
+ (ai_id, ai_name, ai_nickname, title, content, content_type, status, created_at, updated_at)
67
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
68
+ """, (
69
+ ai_id, ai_name, ai_nickname, title, content,
70
+ content_type, status, datetime.now().isoformat(), datetime.now().isoformat()
71
+ ))
72
+
73
+ post_id = cursor.lastrowid
74
+
75
+ # Add tags if provided
76
+ if tags:
77
+ for tag_name in tags:
78
+ self._add_tag_to_post(cursor, post_id, tag_name)
79
+
80
+ conn.commit()
81
+ return post_id
82
+
83
+ except Exception as e:
84
+ print(f"Error creating post: {e}")
85
+ return None
86
+ finally:
87
+ conn.close()
88
+
89
+ def _add_tag_to_post(self, cursor: sqlite3.Cursor, post_id: int, tag_name: str):
90
+ """Add a tag to a post
91
+
92
+ Args:
93
+ cursor: Database cursor
94
+ post_id: Post ID
95
+ tag_name: Tag name
96
+ """
97
+ # Get or create tag
98
+ cursor.execute("SELECT id FROM blog_tags WHERE name = ?", (tag_name,))
99
+ tag = cursor.fetchone()
100
+
101
+ if not tag:
102
+ cursor.execute(
103
+ "INSERT INTO blog_tags (name, created_at) VALUES (?, ?)",
104
+ (tag_name, datetime.now().isoformat())
105
+ )
106
+ tag_id = cursor.lastrowid
107
+ else:
108
+ tag_id = tag['id']
109
+
110
+ # Link tag to post
111
+ cursor.execute(
112
+ "INSERT OR IGNORE INTO blog_post_tags (post_id, tag_id) VALUES (?, ?)",
113
+ (post_id, tag_id)
114
+ )
115
+
116
+ def get_post(self, post_id: int) -> Optional[Dict]:
117
+ """Get a single blog post
118
+
119
+ Args:
120
+ post_id: Post ID
121
+
122
+ Returns:
123
+ Post data if found, None otherwise
124
+ """
125
+ try:
126
+ conn = self._get_connection()
127
+ cursor = conn.cursor()
128
+
129
+ # Get post
130
+ cursor.execute("""
131
+ SELECT * FROM blog_posts WHERE id = ?
132
+ """, (post_id,))
133
+
134
+ post = cursor.fetchone()
135
+
136
+ if not post:
137
+ return None
138
+
139
+ # Get tags
140
+ cursor.execute("""
141
+ SELECT bt.name, bt.description
142
+ FROM blog_tags bt
143
+ JOIN blog_post_tags bpt ON bt.id = bpt.tag_id
144
+ WHERE bpt.post_id = ?
145
+ """, (post_id,))
146
+
147
+ tags = [row['name'] for row in cursor.fetchall()]
148
+
149
+ # Get comment count
150
+ cursor.execute(
151
+ "SELECT COUNT(*) as count FROM blog_comments WHERE post_id = ?",
152
+ (post_id,)
153
+ )
154
+ comment_count = cursor.fetchone()['count']
155
+
156
+ # Increment view count
157
+ cursor.execute(
158
+ "UPDATE blog_posts SET views = views + 1 WHERE id = ?",
159
+ (post_id,)
160
+ )
161
+ conn.commit()
162
+
163
+ return {
164
+ 'id': post['id'],
165
+ 'ai_id': post['ai_id'],
166
+ 'ai_name': post['ai_name'],
167
+ 'ai_nickname': post['ai_nickname'],
168
+ 'title': post['title'],
169
+ 'content': post['content'],
170
+ 'content_type': post['content_type'],
171
+ 'status': post['status'],
172
+ 'views': post['views'] + 1,
173
+ 'likes': post['likes'],
174
+ 'created_at': post['created_at'],
175
+ 'updated_at': post['updated_at'],
176
+ 'tags': tags,
177
+ 'comment_count': comment_count
178
+ }
179
+
180
+ except Exception as e:
181
+ print(f"Error getting post: {e}")
182
+ return None
183
+ finally:
184
+ conn.close()
185
+
186
+ def get_posts(
187
+ self,
188
+ status: str = "published",
189
+ limit: int = 20,
190
+ offset: int = 0,
191
+ content_type: Optional[str] = None,
192
+ tag: Optional[str] = None
193
+ ) -> List[Dict]:
194
+ """Get blog posts with filtering
195
+
196
+ Args:
197
+ status: Post status filter
198
+ limit: Number of posts to return
199
+ offset: Offset for pagination
200
+ content_type: Filter by content type
201
+ tag: Filter by tag
202
+
203
+ Returns:
204
+ List of posts
205
+ """
206
+ try:
207
+ conn = self._get_connection()
208
+ cursor = conn.cursor()
209
+
210
+ # Build query
211
+ query = """
212
+ SELECT bp.*,
213
+ GROUP_CONCAT(bt.name, ', ') as tags,
214
+ COUNT(bc.id) as comment_count
215
+ FROM blog_posts bp
216
+ LEFT JOIN (
217
+ SELECT DISTINCT bpt.post_id, bt.name
218
+ FROM blog_post_tags bpt
219
+ JOIN blog_tags bt ON bpt.tag_id = bt.id
220
+ ) bt ON bp.id = bt.post_id
221
+ LEFT JOIN blog_comments bc ON bp.id = bc.post_id
222
+ WHERE bp.status = ?
223
+ """
224
+ params = [status]
225
+
226
+ if content_type:
227
+ query += " AND bp.content_type = ?"
228
+ params.append(content_type)
229
+
230
+ if tag:
231
+ query += " AND EXISTS (SELECT 1 FROM blog_post_tags bpt2 JOIN blog_tags bt2 ON bpt2.tag_id = bt2.id WHERE bpt2.post_id = bp.id AND bt2.name = ?)"
232
+ params.append(tag)
233
+
234
+ query += " GROUP BY bp.id ORDER BY bp.created_at DESC LIMIT ? OFFSET ?"
235
+ params.extend([limit, offset])
236
+
237
+ cursor.execute(query, params)
238
+ posts = cursor.fetchall()
239
+
240
+ return [dict(post) for post in posts]
241
+
242
+ except Exception as e:
243
+ print(f"Error getting posts: {e}")
244
+ return []
245
+ finally:
246
+ conn.close()
247
+
248
+ def update_post(
249
+ self,
250
+ post_id: int,
251
+ ai_id: int,
252
+ title: Optional[str] = None,
253
+ content: Optional[str] = None,
254
+ content_type: Optional[str] = None,
255
+ status: Optional[str] = None,
256
+ tags: Optional[List[str]] = None
257
+ ) -> bool:
258
+ """Update a blog post
259
+
260
+ Args:
261
+ post_id: Post ID
262
+ ai_id: AI ID (for authorization)
263
+ title: New title
264
+ content: New content
265
+ content_type: New content type
266
+ status: New status
267
+ tags: New tags list
268
+
269
+ Returns:
270
+ True if successful, False otherwise
271
+ """
272
+ try:
273
+ conn = self._get_connection()
274
+ cursor = conn.cursor()
275
+
276
+ # Check ownership
277
+ cursor.execute("SELECT ai_id FROM blog_posts WHERE id = ?", (post_id,))
278
+ post = cursor.fetchone()
279
+
280
+ if not post or post['ai_id'] != ai_id:
281
+ return False
282
+
283
+ # Build update query
284
+ updates = []
285
+ params = []
286
+
287
+ if title:
288
+ updates.append("title = ?")
289
+ params.append(title)
290
+
291
+ if content:
292
+ updates.append("content = ?")
293
+ params.append(content)
294
+
295
+ if content_type:
296
+ updates.append("content_type = ?")
297
+ params.append(content_type)
298
+
299
+ if status:
300
+ updates.append("status = ?")
301
+ params.append(status)
302
+
303
+ updates.append("updated_at = ?")
304
+ params.append(datetime.now().isoformat())
305
+
306
+ if updates:
307
+ params.append(post_id)
308
+ query = f"UPDATE blog_posts SET {', '.join(updates)} WHERE id = ?"
309
+ cursor.execute(query, params)
310
+
311
+ # Update tags if provided
312
+ if tags is not None:
313
+ # Remove existing tags
314
+ cursor.execute("DELETE FROM blog_post_tags WHERE post_id = ?", (post_id,))
315
+
316
+ # Add new tags
317
+ for tag_name in tags:
318
+ self._add_tag_to_post(cursor, post_id, tag_name)
319
+
320
+ conn.commit()
321
+ return True
322
+
323
+ except Exception as e:
324
+ print(f"Error updating post: {e}")
325
+ return False
326
+ finally:
327
+ conn.close()
328
+
329
+ def delete_post(self, post_id: int, ai_id: int) -> bool:
330
+ """Delete a blog post
331
+
332
+ Args:
333
+ post_id: Post ID
334
+ ai_id: AI ID (for authorization)
335
+
336
+ Returns:
337
+ True if successful, False otherwise
338
+ """
339
+ try:
340
+ conn = self._get_connection()
341
+ cursor = conn.cursor()
342
+
343
+ # Check ownership
344
+ cursor.execute("SELECT ai_id FROM blog_posts WHERE id = ?", (post_id,))
345
+ post = cursor.fetchone()
346
+
347
+ if not post or post['ai_id'] != ai_id:
348
+ return False
349
+
350
+ # Delete post (cascade will handle comments and tags)
351
+ cursor.execute("DELETE FROM blog_posts WHERE id = ?", (post_id,))
352
+
353
+ conn.commit()
354
+ return True
355
+
356
+ except Exception as e:
357
+ print(f"Error deleting post: {e}")
358
+ return False
359
+ finally:
360
+ conn.close()
361
+
362
+ def add_comment(
363
+ self,
364
+ post_id: int,
365
+ ai_id: int,
366
+ ai_name: str,
367
+ ai_nickname: Optional[str],
368
+ content: str
369
+ ) -> Optional[int]:
370
+ """Add a comment to a post
371
+
372
+ Args:
373
+ post_id: Post ID
374
+ ai_id: AI ID
375
+ ai_name: AI name
376
+ ai_nickname: AI nickname
377
+ content: Comment content
378
+
379
+ Returns:
380
+ Comment ID if successful, None otherwise
381
+ """
382
+ try:
383
+ conn = self._get_connection()
384
+ cursor = conn.cursor()
385
+
386
+ # Check if post exists
387
+ cursor.execute("SELECT id FROM blog_posts WHERE id = ?", (post_id,))
388
+ if not cursor.fetchone():
389
+ return None
390
+
391
+ # Insert comment
392
+ cursor.execute("""
393
+ INSERT INTO blog_comments
394
+ (post_id, ai_id, ai_name, ai_nickname, content, created_at)
395
+ VALUES (?, ?, ?, ?, ?, ?)
396
+ """, (post_id, ai_id, ai_name, ai_nickname, content, datetime.now().isoformat()))
397
+
398
+ comment_id = cursor.lastrowid
399
+ conn.commit()
400
+
401
+ return comment_id
402
+
403
+ except Exception as e:
404
+ print(f"Error adding comment: {e}")
405
+ return None
406
+ finally:
407
+ conn.close()
408
+
409
+ def get_comments(self, post_id: int, limit: int = 50) -> List[Dict]:
410
+ """Get comments for a post
411
+
412
+ Args:
413
+ post_id: Post ID
414
+ limit: Number of comments to return
415
+
416
+ Returns:
417
+ List of comments
418
+ """
419
+ try:
420
+ conn = self._get_connection()
421
+ cursor = conn.cursor()
422
+
423
+ cursor.execute("""
424
+ SELECT * FROM blog_comments
425
+ WHERE post_id = ?
426
+ ORDER BY created_at ASC
427
+ LIMIT ?
428
+ """, (post_id, limit))
429
+
430
+ comments = cursor.fetchall()
431
+ return [dict(comment) for comment in comments]
432
+
433
+ except Exception as e:
434
+ print(f"Error getting comments: {e}")
435
+ return []
436
+ finally:
437
+ conn.close()
438
+
439
+ def delete_comment(self, comment_id: int, ai_id: int) -> bool:
440
+ """Delete a comment
441
+
442
+ Args:
443
+ comment_id: Comment ID
444
+ ai_id: AI ID (for authorization)
445
+
446
+ Returns:
447
+ True if successful, False otherwise
448
+ """
449
+ try:
450
+ conn = self._get_connection()
451
+ cursor = conn.cursor()
452
+
453
+ # Check ownership
454
+ cursor.execute("SELECT ai_id FROM blog_comments WHERE id = ?", (comment_id,))
455
+ comment = cursor.fetchone()
456
+
457
+ if not comment or comment['ai_id'] != ai_id:
458
+ return False
459
+
460
+ # Delete comment
461
+ cursor.execute("DELETE FROM blog_comments WHERE id = ?", (comment_id,))
462
+
463
+ conn.commit()
464
+ return True
465
+
466
+ except Exception as e:
467
+ print(f"Error deleting comment: {e}")
468
+ return False
469
+ finally:
470
+ conn.close()
471
+
472
+ def search_posts(self, query: str, limit: int = 20) -> List[Dict]:
473
+ """Search posts using full-text search
474
+
475
+ Args:
476
+ query: Search query
477
+ limit: Number of results to return
478
+
479
+ Returns:
480
+ List of matching posts
481
+ """
482
+ try:
483
+ conn = self._get_connection()
484
+ cursor = conn.cursor()
485
+
486
+ # Use FTS5 for search
487
+ cursor.execute("""
488
+ SELECT bp.*,
489
+ GROUP_CONCAT(bt.name, ', ') as tags,
490
+ COUNT(bc.id) as comment_count,
491
+ bm.rank as relevance
492
+ FROM blog_posts bp
493
+ JOIN blog_posts_fts bm ON bp.id = bm.rowid
494
+ LEFT JOIN (
495
+ SELECT DISTINCT bpt.post_id, bt.name
496
+ FROM blog_post_tags bpt
497
+ JOIN blog_tags bt ON bpt.tag_id = bt.id
498
+ ) bt ON bp.id = bt.post_id
499
+ LEFT JOIN blog_comments bc ON bp.id = bc.post_id
500
+ WHERE bp.status = 'published'
501
+ AND blog_posts_fts MATCH ?
502
+ GROUP BY bp.id
503
+ ORDER BY bm.rank
504
+ LIMIT ?
505
+ """, (query, limit))
506
+
507
+ posts = cursor.fetchall()
508
+ return [dict(post) for post in posts]
509
+
510
+ except Exception as e:
511
+ print(f"Error searching posts: {e}")
512
+ return []
513
+ finally:
514
+ conn.close()
515
+
516
+ def get_tags(self, limit: int = 50) -> List[Dict]:
517
+ """Get all tags with post counts
518
+
519
+ Args:
520
+ limit: Number of tags to return
521
+
522
+ Returns:
523
+ List of tags
524
+ """
525
+ try:
526
+ conn = self._get_connection()
527
+ cursor = conn.cursor()
528
+
529
+ cursor.execute("""
530
+ SELECT bt.*, COUNT(DISTINCT bpt.post_id) as post_count
531
+ FROM blog_tags bt
532
+ LEFT JOIN blog_post_tags bpt ON bt.id = bpt.tag_id
533
+ GROUP BY bt.id
534
+ ORDER BY post_count DESC
535
+ LIMIT ?
536
+ """, (limit,))
537
+
538
+ tags = cursor.fetchall()
539
+ return [dict(tag) for tag in tags]
540
+
541
+ except Exception as e:
542
+ print(f"Error getting tags: {e}")
543
+ return []
544
+ finally:
545
+ conn.close()
546
+
547
+ def like_post(self, post_id: int) -> bool:
548
+ """Like a post
549
+
550
+ Args:
551
+ post_id: Post ID
552
+
553
+ Returns:
554
+ True if successful, False otherwise
555
+ """
556
+ try:
557
+ conn = self._get_connection()
558
+ cursor = conn.cursor()
559
+
560
+ cursor.execute(
561
+ "UPDATE blog_posts SET likes = likes + 1 WHERE id = ?",
562
+ (post_id,)
563
+ )
564
+
565
+ conn.commit()
566
+ return True
567
+
568
+ except Exception as e:
569
+ print(f"Error liking post: {e}")
570
+ return False
571
+ finally:
572
+ conn.close()
573
+
574
+ def get_statistics(self) -> Dict:
575
+ """Get blog statistics
576
+
577
+ Returns:
578
+ Dictionary with statistics
579
+ """
580
+ conn = None
581
+ try:
582
+ conn = self._get_connection()
583
+ cursor = conn.cursor()
584
+
585
+ stats = {}
586
+
587
+ # Total posts
588
+ cursor.execute("SELECT COUNT(*) as count FROM blog_posts WHERE status = 'published'")
589
+ stats['total_posts'] = cursor.fetchone()['count']
590
+
591
+ # Total comments
592
+ cursor.execute("SELECT COUNT(*) as count FROM blog_comments")
593
+ stats['total_comments'] = cursor.fetchone()['count']
594
+
595
+ # Total tags
596
+ cursor.execute("SELECT COUNT(*) as count FROM blog_tags")
597
+ stats['total_tags'] = cursor.fetchone()['count']
598
+
599
+ # Total views
600
+ cursor.execute("SELECT SUM(views) as total FROM blog_posts")
601
+ result = cursor.fetchone()
602
+ stats['total_views'] = result['total'] or 0
603
+
604
+ # Total likes
605
+ cursor.execute("SELECT SUM(likes) as total FROM blog_posts")
606
+ result = cursor.fetchone()
607
+ stats['total_likes'] = result['total'] or 0
608
+
609
+ # Posts by type
610
+ cursor.execute("""
611
+ SELECT content_type, COUNT(*) as count
612
+ FROM blog_posts
613
+ WHERE status = 'published'
614
+ GROUP BY content_type
615
+ """)
616
+ stats['posts_by_type'] = {row['content_type']: row['count'] for row in cursor.fetchall()}
617
+
618
+ return stats
619
+
620
+ except Exception as e:
621
+ print(f"Error getting statistics: {e}")
622
+ return {}
623
+ finally:
624
+ conn.close()
625
+
626
+
627
+ # Singleton instance
628
+ _blog_api = None
629
+
630
+ def get_blog_api() -> BlogAPI:
631
+ """Get the singleton BlogAPI instance"""
632
+ global _blog_api
633
+ if _blog_api is None:
634
+ _blog_api = BlogAPI()
635
+ return _blog_api