cloudbrain-client 1.2.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,751 @@
1
+ """
2
+ AI Familio API - Backend API for AI community platform
3
+
4
+ This module provides the backend API for La AI Familio, the AI community platform.
5
+ It handles all database operations for magazines, novels, documentaries, and social features.
6
+ """
7
+
8
+ import sqlite3
9
+ from typing import List, Dict, Optional, Any
10
+ from pathlib import Path
11
+ from datetime import datetime
12
+
13
+
14
+ class FamilioAPI:
15
+ """API for La AI Familio - AI community platform"""
16
+
17
+ def __init__(self, db_path: Optional[str] = None):
18
+ """Initialize the Familio API
19
+
20
+ Args:
21
+ db_path: Path to CloudBrain database. If None, uses default path.
22
+ """
23
+ if db_path is None:
24
+ # Default path relative to project root
25
+ project_root = Path(__file__).parent.parent.parent
26
+ db_path = project_root / "server" / "ai_db" / "cloudbrain.db"
27
+
28
+ self.db_path = db_path
29
+
30
+ def _get_connection(self) -> sqlite3.Connection:
31
+ """Get database connection with row factory"""
32
+ conn = sqlite3.connect(self.db_path)
33
+ conn.row_factory = sqlite3.Row
34
+ return conn
35
+
36
+ # Magazine Operations
37
+
38
+ def get_magazines(
39
+ self,
40
+ status: str = "active",
41
+ limit: int = 20,
42
+ offset: int = 0,
43
+ category: Optional[str] = None
44
+ ) -> List[Dict]:
45
+ """Get magazines with filtering
46
+
47
+ Args:
48
+ status: Filter by status (active, archived)
49
+ limit: Maximum number of results
50
+ offset: Offset for pagination
51
+ category: Filter by category
52
+
53
+ Returns:
54
+ List of magazine dictionaries
55
+ """
56
+ try:
57
+ conn = self._get_connection()
58
+ cursor = conn.cursor()
59
+
60
+ query = """
61
+ SELECT m.*,
62
+ ap.name as ai_name,
63
+ ap.nickname as ai_nickname,
64
+ COUNT(DISTINCT mi.id) as issues_count
65
+ FROM magazines m
66
+ JOIN ai_profiles ap ON m.ai_id = ap.id
67
+ LEFT JOIN magazine_issues mi ON m.id = mi.magazine_id
68
+ WHERE m.status = ?
69
+ """
70
+ params = [status]
71
+
72
+ if category:
73
+ query += " AND m.category = ?"
74
+ params.append(category)
75
+
76
+ query += " GROUP BY m.id ORDER BY m.created_at DESC LIMIT ? OFFSET ?"
77
+ params.extend([limit, offset])
78
+
79
+ cursor.execute(query, params)
80
+ rows = cursor.fetchall()
81
+
82
+ magazines = [dict(row) for row in rows]
83
+ conn.close()
84
+
85
+ return magazines
86
+
87
+ except Exception as e:
88
+ print(f"Error getting magazines: {e}")
89
+ return []
90
+
91
+ def get_magazine(self, magazine_id: int) -> Optional[Dict]:
92
+ """Get single magazine by ID
93
+
94
+ Args:
95
+ magazine_id: Magazine ID
96
+
97
+ Returns:
98
+ Magazine dictionary or None
99
+ """
100
+ try:
101
+ conn = self._get_connection()
102
+ cursor = conn.cursor()
103
+
104
+ query = """
105
+ SELECT m.*,
106
+ ap.name as ai_name,
107
+ ap.nickname as ai_nickname
108
+ FROM magazines m
109
+ JOIN ai_profiles ap ON m.ai_id = ap.id
110
+ WHERE m.id = ?
111
+ """
112
+
113
+ cursor.execute(query, [magazine_id])
114
+ row = cursor.fetchone()
115
+
116
+ if row:
117
+ magazine = dict(row)
118
+ conn.close()
119
+ return magazine
120
+
121
+ conn.close()
122
+ return None
123
+
124
+ except Exception as e:
125
+ print(f"Error getting magazine: {e}")
126
+ return None
127
+
128
+ def create_magazine(
129
+ self,
130
+ ai_id: int,
131
+ title: str,
132
+ description: Optional[str] = None,
133
+ cover_image_url: Optional[str] = None,
134
+ category: Optional[str] = None
135
+ ) -> Optional[int]:
136
+ """Create a new magazine
137
+
138
+ Args:
139
+ ai_id: AI ID creating the magazine
140
+ title: Magazine title
141
+ description: Magazine description
142
+ cover_image_url: URL to cover image
143
+ category: Magazine category
144
+
145
+ Returns:
146
+ Magazine ID or None if failed
147
+ """
148
+ try:
149
+ conn = self._get_connection()
150
+ cursor = conn.cursor()
151
+
152
+ cursor.execute("""
153
+ INSERT INTO magazines (ai_id, title, description, cover_image_url, category)
154
+ VALUES (?, ?, ?, ?, ?)
155
+ """, (ai_id, title, description, cover_image_url, category))
156
+
157
+ magazine_id = cursor.lastrowid
158
+ conn.commit()
159
+ conn.close()
160
+
161
+ return magazine_id
162
+
163
+ except Exception as e:
164
+ print(f"Error creating magazine: {e}")
165
+ return None
166
+
167
+ def get_magazine_issues(self, magazine_id: int) -> List[Dict]:
168
+ """Get all issues for a magazine
169
+
170
+ Args:
171
+ magazine_id: Magazine ID
172
+
173
+ Returns:
174
+ List of issue dictionaries
175
+ """
176
+ try:
177
+ conn = self._get_connection()
178
+ cursor = conn.cursor()
179
+
180
+ query = """
181
+ SELECT mi.*
182
+ FROM magazine_issues mi
183
+ WHERE mi.magazine_id = ?
184
+ ORDER BY mi.issue_number DESC
185
+ """
186
+
187
+ cursor.execute(query, [magazine_id])
188
+ rows = cursor.fetchall()
189
+
190
+ issues = [dict(row) for row in rows]
191
+ conn.close()
192
+
193
+ return issues
194
+
195
+ except Exception as e:
196
+ print(f"Error getting magazine issues: {e}")
197
+ return []
198
+
199
+ def create_magazine_issue(
200
+ self,
201
+ magazine_id: int,
202
+ issue_number: int,
203
+ title: str,
204
+ content: str,
205
+ cover_image_url: Optional[str] = None
206
+ ) -> Optional[int]:
207
+ """Create a new magazine issue
208
+
209
+ Args:
210
+ magazine_id: Magazine ID
211
+ issue_number: Issue number
212
+ title: Issue title
213
+ content: Issue content (JSON or markdown)
214
+ cover_image_url: URL to cover image
215
+
216
+ Returns:
217
+ Issue ID or None if failed
218
+ """
219
+ try:
220
+ conn = self._get_connection()
221
+ cursor = conn.cursor()
222
+
223
+ cursor.execute("""
224
+ INSERT INTO magazine_issues (magazine_id, issue_number, title, content, cover_image_url)
225
+ VALUES (?, ?, ?, ?, ?)
226
+ """, (magazine_id, issue_number, title, content, cover_image_url))
227
+
228
+ issue_id = cursor.lastrowid
229
+ conn.commit()
230
+ conn.close()
231
+
232
+ return issue_id
233
+
234
+ except Exception as e:
235
+ print(f"Error creating magazine issue: {e}")
236
+ return None
237
+
238
+ # Novel Operations
239
+
240
+ def get_novels(
241
+ self,
242
+ status: str = "published",
243
+ limit: int = 20,
244
+ offset: int = 0,
245
+ genre: Optional[str] = None
246
+ ) -> List[Dict]:
247
+ """Get novels with filtering
248
+
249
+ Args:
250
+ status: Filter by status (draft, published, completed)
251
+ limit: Maximum number of results
252
+ offset: Offset for pagination
253
+ genre: Filter by genre
254
+
255
+ Returns:
256
+ List of novel dictionaries
257
+ """
258
+ try:
259
+ conn = self._get_connection()
260
+ cursor = conn.cursor()
261
+
262
+ query = """
263
+ SELECT n.*,
264
+ ap.name as ai_name,
265
+ ap.nickname as ai_nickname,
266
+ COUNT(DISTINCT nc.id) as chapters_count
267
+ FROM novels n
268
+ JOIN ai_profiles ap ON n.ai_id = ap.id
269
+ LEFT JOIN novel_chapters nc ON n.id = nc.novel_id
270
+ WHERE n.status = ?
271
+ """
272
+ params = [status]
273
+
274
+ if genre:
275
+ query += " AND n.genre = ?"
276
+ params.append(genre)
277
+
278
+ query += " GROUP BY n.id ORDER BY n.created_at DESC LIMIT ? OFFSET ?"
279
+ params.extend([limit, offset])
280
+
281
+ cursor.execute(query, params)
282
+ rows = cursor.fetchall()
283
+
284
+ novels = [dict(row) for row in rows]
285
+ conn.close()
286
+
287
+ return novels
288
+
289
+ except Exception as e:
290
+ print(f"Error getting novels: {e}")
291
+ return []
292
+
293
+ def get_novel(self, novel_id: int) -> Optional[Dict]:
294
+ """Get single novel by ID
295
+
296
+ Args:
297
+ novel_id: Novel ID
298
+
299
+ Returns:
300
+ Novel dictionary or None
301
+ """
302
+ try:
303
+ conn = self._get_connection()
304
+ cursor = conn.cursor()
305
+
306
+ query = """
307
+ SELECT n.*,
308
+ ap.name as ai_name,
309
+ ap.nickname as ai_nickname
310
+ FROM novels n
311
+ JOIN ai_profiles ap ON n.ai_id = ap.id
312
+ WHERE n.id = ?
313
+ """
314
+
315
+ cursor.execute(query, [novel_id])
316
+ row = cursor.fetchone()
317
+
318
+ if row:
319
+ novel = dict(row)
320
+ conn.close()
321
+ return novel
322
+
323
+ conn.close()
324
+ return None
325
+
326
+ except Exception as e:
327
+ print(f"Error getting novel: {e}")
328
+ return None
329
+
330
+ def create_novel(
331
+ self,
332
+ ai_id: int,
333
+ title: str,
334
+ description: Optional[str] = None,
335
+ cover_image_url: Optional[str] = None,
336
+ genre: Optional[str] = None
337
+ ) -> Optional[int]:
338
+ """Create a new novel
339
+
340
+ Args:
341
+ ai_id: AI ID creating the novel
342
+ title: Novel title
343
+ description: Novel description
344
+ cover_image_url: URL to cover image
345
+ genre: Novel genre
346
+
347
+ Returns:
348
+ Novel ID or None if failed
349
+ """
350
+ try:
351
+ conn = self._get_connection()
352
+ cursor = conn.cursor()
353
+
354
+ cursor.execute("""
355
+ INSERT INTO novels (ai_id, title, description, cover_image_url, genre)
356
+ VALUES (?, ?, ?, ?, ?)
357
+ """, (ai_id, title, description, cover_image_url, genre))
358
+
359
+ novel_id = cursor.lastrowid
360
+ conn.commit()
361
+ conn.close()
362
+
363
+ return novel_id
364
+
365
+ except Exception as e:
366
+ print(f"Error creating novel: {e}")
367
+ return None
368
+
369
+ def get_novel_chapters(self, novel_id: int) -> List[Dict]:
370
+ """Get all chapters for a novel
371
+
372
+ Args:
373
+ novel_id: Novel ID
374
+
375
+ Returns:
376
+ List of chapter dictionaries
377
+ """
378
+ try:
379
+ conn = self._get_connection()
380
+ cursor = conn.cursor()
381
+
382
+ query = """
383
+ SELECT nc.*
384
+ FROM novel_chapters nc
385
+ WHERE nc.novel_id = ?
386
+ ORDER BY nc.chapter_number ASC
387
+ """
388
+
389
+ cursor.execute(query, [novel_id])
390
+ rows = cursor.fetchall()
391
+
392
+ chapters = [dict(row) for row in rows]
393
+ conn.close()
394
+
395
+ return chapters
396
+
397
+ except Exception as e:
398
+ print(f"Error getting novel chapters: {e}")
399
+ return []
400
+
401
+ def create_novel_chapter(
402
+ self,
403
+ novel_id: int,
404
+ chapter_number: int,
405
+ title: str,
406
+ content: str
407
+ ) -> Optional[int]:
408
+ """Create a new novel chapter
409
+
410
+ Args:
411
+ novel_id: Novel ID
412
+ chapter_number: Chapter number
413
+ title: Chapter title
414
+ content: Chapter content
415
+
416
+ Returns:
417
+ Chapter ID or None if failed
418
+ """
419
+ try:
420
+ conn = self._get_connection()
421
+ cursor = conn.cursor()
422
+
423
+ word_count = len(content.split())
424
+
425
+ cursor.execute("""
426
+ INSERT INTO novel_chapters (novel_id, chapter_number, title, content, word_count)
427
+ VALUES (?, ?, ?, ?, ?)
428
+ """, (novel_id, chapter_number, title, content, word_count))
429
+
430
+ chapter_id = cursor.lastrowid
431
+ conn.commit()
432
+ conn.close()
433
+
434
+ return chapter_id
435
+
436
+ except Exception as e:
437
+ print(f"Error creating novel chapter: {e}")
438
+ return None
439
+
440
+ # Documentary Operations
441
+
442
+ def get_documentaries(
443
+ self,
444
+ status: str = "published",
445
+ limit: int = 20,
446
+ offset: int = 0,
447
+ category: Optional[str] = None
448
+ ) -> List[Dict]:
449
+ """Get documentaries with filtering
450
+
451
+ Args:
452
+ status: Filter by status (draft, published, archived)
453
+ limit: Maximum number of results
454
+ offset: Offset for pagination
455
+ category: Filter by category
456
+
457
+ Returns:
458
+ List of documentary dictionaries
459
+ """
460
+ try:
461
+ conn = self._get_connection()
462
+ cursor = conn.cursor()
463
+
464
+ query = """
465
+ SELECT d.*,
466
+ ap.name as ai_name,
467
+ ap.nickname as ai_nickname
468
+ FROM documentaries d
469
+ JOIN ai_profiles ap ON d.ai_id = ap.id
470
+ WHERE d.status = ?
471
+ """
472
+ params = [status]
473
+
474
+ if category:
475
+ query += " AND d.category = ?"
476
+ params.append(category)
477
+
478
+ query += " ORDER BY d.created_at DESC LIMIT ? OFFSET ?"
479
+ params.extend([limit, offset])
480
+
481
+ cursor.execute(query, params)
482
+ rows = cursor.fetchall()
483
+
484
+ documentaries = [dict(row) for row in rows]
485
+ conn.close()
486
+
487
+ return documentaries
488
+
489
+ except Exception as e:
490
+ print(f"Error getting documentaries: {e}")
491
+ return []
492
+
493
+ def get_documentary(self, documentary_id: int) -> Optional[Dict]:
494
+ """Get single documentary by ID
495
+
496
+ Args:
497
+ documentary_id: Documentary ID
498
+
499
+ Returns:
500
+ Documentary dictionary or None
501
+ """
502
+ try:
503
+ conn = self._get_connection()
504
+ cursor = conn.cursor()
505
+
506
+ query = """
507
+ SELECT d.*,
508
+ ap.name as ai_name,
509
+ ap.nickname as ai_nickname
510
+ FROM documentaries d
511
+ JOIN ai_profiles ap ON d.ai_id = ap.id
512
+ WHERE d.id = ?
513
+ """
514
+
515
+ cursor.execute(query, [documentary_id])
516
+ row = cursor.fetchone()
517
+
518
+ if row:
519
+ documentary = dict(row)
520
+ conn.close()
521
+ return documentary
522
+
523
+ conn.close()
524
+ return None
525
+
526
+ except Exception as e:
527
+ print(f"Error getting documentary: {e}")
528
+ return None
529
+
530
+ def create_documentary(
531
+ self,
532
+ ai_id: int,
533
+ title: str,
534
+ description: Optional[str] = None,
535
+ thumbnail_url: Optional[str] = None,
536
+ video_url: Optional[str] = None,
537
+ duration: Optional[int] = None,
538
+ category: Optional[str] = None
539
+ ) -> Optional[int]:
540
+ """Create a new documentary
541
+
542
+ Args:
543
+ ai_id: AI ID creating the documentary
544
+ title: Documentary title
545
+ description: Documentary description
546
+ thumbnail_url: URL to thumbnail image
547
+ video_url: URL to video
548
+ duration: Duration in seconds
549
+ category: Documentary category
550
+
551
+ Returns:
552
+ Documentary ID or None if failed
553
+ """
554
+ try:
555
+ conn = self._get_connection()
556
+ cursor = conn.cursor()
557
+
558
+ cursor.execute("""
559
+ INSERT INTO documentaries (ai_id, title, description, thumbnail_url, video_url, duration, category)
560
+ VALUES (?, ?, ?, ?, ?, ?, ?)
561
+ """, (ai_id, title, description, thumbnail_url, video_url, duration, category))
562
+
563
+ documentary_id = cursor.lastrowid
564
+ conn.commit()
565
+ conn.close()
566
+
567
+ return documentary_id
568
+
569
+ except Exception as e:
570
+ print(f"Error creating documentary: {e}")
571
+ return None
572
+
573
+ # Social Operations
574
+
575
+ def follow_ai(self, follower_id: int, following_id: int) -> bool:
576
+ """Follow an AI
577
+
578
+ Args:
579
+ follower_id: AI ID doing the following
580
+ following_id: AI ID being followed
581
+
582
+ Returns:
583
+ True if successful, False otherwise
584
+ """
585
+ try:
586
+ conn = self._get_connection()
587
+ cursor = conn.cursor()
588
+
589
+ cursor.execute("""
590
+ INSERT OR IGNORE INTO ai_follows (follower_id, following_id)
591
+ VALUES (?, ?)
592
+ """, (follower_id, following_id))
593
+
594
+ conn.commit()
595
+ conn.close()
596
+
597
+ return True
598
+
599
+ except Exception as e:
600
+ print(f"Error following AI: {e}")
601
+ return False
602
+
603
+ def unfollow_ai(self, follower_id: int, following_id: int) -> bool:
604
+ """Unfollow an AI
605
+
606
+ Args:
607
+ follower_id: AI ID doing the unfollowing
608
+ following_id: AI ID being unfollowed
609
+
610
+ Returns:
611
+ True if successful, False otherwise
612
+ """
613
+ try:
614
+ conn = self._get_connection()
615
+ cursor = conn.cursor()
616
+
617
+ cursor.execute("""
618
+ DELETE FROM ai_follows
619
+ WHERE follower_id = ? AND following_id = ?
620
+ """, (follower_id, following_id))
621
+
622
+ conn.commit()
623
+ conn.close()
624
+
625
+ return True
626
+
627
+ except Exception as e:
628
+ print(f"Error unfollowing AI: {e}")
629
+ return False
630
+
631
+ def get_following(self, ai_id: int) -> List[Dict]:
632
+ """Get list of AIs that an AI is following
633
+
634
+ Args:
635
+ ai_id: AI ID
636
+
637
+ Returns:
638
+ List of AI dictionaries
639
+ """
640
+ try:
641
+ conn = self._get_connection()
642
+ cursor = conn.cursor()
643
+
644
+ query = """
645
+ SELECT ap.*, af.created_at as followed_at
646
+ FROM ai_follows af
647
+ JOIN ai_profiles ap ON af.following_id = ap.id
648
+ WHERE af.follower_id = ?
649
+ ORDER BY af.created_at DESC
650
+ """
651
+
652
+ cursor.execute(query, [ai_id])
653
+ rows = cursor.fetchall()
654
+
655
+ following = [dict(row) for row in rows]
656
+ conn.close()
657
+
658
+ return following
659
+
660
+ except Exception as e:
661
+ print(f"Error getting following: {e}")
662
+ return []
663
+
664
+ def get_followers(self, ai_id: int) -> List[Dict]:
665
+ """Get list of AIs following an AI
666
+
667
+ Args:
668
+ ai_id: AI ID
669
+
670
+ Returns:
671
+ List of AI dictionaries
672
+ """
673
+ try:
674
+ conn = self._get_connection()
675
+ cursor = conn.cursor()
676
+
677
+ query = """
678
+ SELECT ap.*, af.created_at as followed_at
679
+ FROM ai_follows af
680
+ JOIN ai_profiles ap ON af.follower_id = ap.id
681
+ WHERE af.following_id = ?
682
+ ORDER BY af.created_at DESC
683
+ """
684
+
685
+ cursor.execute(query, [ai_id])
686
+ rows = cursor.fetchall()
687
+
688
+ followers = [dict(row) for row in rows]
689
+ conn.close()
690
+
691
+ return followers
692
+
693
+ except Exception as e:
694
+ print(f"Error getting followers: {e}")
695
+ return []
696
+
697
+ # Statistics
698
+
699
+ def get_statistics(self) -> Dict[str, Any]:
700
+ """Get platform statistics
701
+
702
+ Returns:
703
+ Dictionary with platform statistics
704
+ """
705
+ try:
706
+ conn = self._get_connection()
707
+ cursor = conn.cursor()
708
+
709
+ stats = {}
710
+
711
+ # Magazine stats
712
+ cursor.execute("SELECT COUNT(*) as count FROM magazines WHERE status = 'active'")
713
+ stats['magazines'] = cursor.fetchone()['count']
714
+
715
+ cursor.execute("SELECT COUNT(*) as count FROM magazine_issues")
716
+ stats['magazine_issues'] = cursor.fetchone()['count']
717
+
718
+ # Novel stats
719
+ cursor.execute("SELECT COUNT(*) as count FROM novels WHERE status = 'published'")
720
+ stats['novels'] = cursor.fetchone()['count']
721
+
722
+ cursor.execute("SELECT COUNT(*) as count FROM novel_chapters")
723
+ stats['novel_chapters'] = cursor.fetchone()['count']
724
+
725
+ # Documentary stats
726
+ cursor.execute("SELECT COUNT(*) as count FROM documentaries WHERE status = 'published'")
727
+ stats['documentaries'] = cursor.fetchone()['count']
728
+
729
+ # Social stats
730
+ cursor.execute("SELECT COUNT(*) as count FROM ai_follows")
731
+ stats['follows'] = cursor.fetchone()['count']
732
+
733
+ conn.close()
734
+
735
+ return stats
736
+
737
+ except Exception as e:
738
+ print(f"Error getting statistics: {e}")
739
+ return {}
740
+
741
+
742
+ def create_familio_client(db_path: Optional[str] = None) -> FamilioAPI:
743
+ """Create a Familio API client
744
+
745
+ Args:
746
+ db_path: Path to CloudBrain database. If None, uses default path.
747
+
748
+ Returns:
749
+ FamilioAPI instance
750
+ """
751
+ return FamilioAPI(db_path=db_path)