Coverage for youversion/core/base_client.py: 100%

319 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-06-26 11:31 +0100

1"""Base client class implementing common functionality.""" 

2 

3from datetime import datetime, timezone 

4from typing import Any 

5 

6from ..enums import MomentKinds 

7from ..models.base import Moment 

8from ..models.moments import CreateMoment 

9from .authenticator import Authenticator 

10from .data_processor import DataProcessor 

11from .http_client import HttpClient 

12from .interfaces import IClient, IHttpClient 

13 

14 

15class BaseClient(IClient): 

16 """Base client implementing common API operations.""" 

17 

18 def __init__(self, username: str | None = None, password: str | None = None): 

19 """Initialize base client. 

20 

21 Args: 

22 username: Username for authentication 

23 password: Password for authentication 

24 """ 

25 self._authenticator = Authenticator(username, password) 

26 self._http_client: IHttpClient | None = None 

27 self._data_processor = DataProcessor() 

28 self._user_id = None 

29 self._access_token = None 

30 

31 async def _ensure_authenticated(self) -> None: 

32 """Ensure client is authenticated.""" 

33 if self._http_client is None: 

34 client = await self._authenticator.authenticate( 

35 self._authenticator.username, self._authenticator.password 

36 ) 

37 self._user_id = self._authenticator.user_id 

38 self._access_token = self._authenticator.access_token 

39 self._http_client = HttpClient(client, user_id=self._user_id) 

40 

41 async def _get_cards_data( 

42 self, kind: str = "", page: int = 1 

43 ) -> list[dict[str, Any]]: 

44 """Get cards data from API. 

45 

46 Args: 

47 kind: Kind of data to retrieve 

48 page: Page number 

49 

50 Returns: 

51 Raw cards data (list of moments) 

52 """ 

53 await self._ensure_authenticated() 

54 data = await self._http_client.get_cards(page=page, kind=kind) 

55 

56 # Extract moments array from response structure 

57 if "response" not in data: 

58 return data 

59 

60 moments_data = data.get("response", {}).get("data", {}).get("moments", []) 

61 return moments_data 

62 

63 async def moments(self, page: int = 1) -> list[Moment]: 

64 """Get moments. 

65 

66 Args: 

67 page: Page number 

68 

69 Returns: 

70 List of dynamically created Moment objects conforming to 

71 MomentProtocol 

72 """ 

73 await self._ensure_authenticated() 

74 raw_data = await self._http_client.get_cards(page=page, kind="") 

75 # Extract moments array from response structure 

76 moments_data = raw_data.get("moments", []) 

77 return self._data_processor.process_moments(moments_data) 

78 

79 async def highlights(self, page: int = 1) -> list[Any]: 

80 """Get highlights. 

81 

82 Args: 

83 page: Page number 

84 

85 Returns: 

86 List of Highlight objects 

87 """ 

88 raw_data = await self._get_cards_data( 

89 kind=MomentKinds.HIGHLIGHT.value, page=page 

90 ) 

91 raw_data = raw_data.get("moments", []) 

92 return self._data_processor.process_highlights(raw_data) 

93 

94 async def verse_of_the_day(self, day: int | None = None) -> Any: 

95 """Get verse of the day. 

96 Note: this does not need authentication 

97 

98 Args: 

99 day: Specific day number (optional) 

100 

101 Returns: 

102 Votd object 

103 """ 

104 raw_data = await self._http_client.get_verse_of_the_day() 

105 return self._data_processor.process_verse_of_the_day(raw_data, day) 

106 

107 async def notes(self, page: int = 1) -> list[Any]: 

108 """Get notes. 

109 

110 Args: 

111 page: Page number 

112 

113 Returns: 

114 List of note data 

115 """ 

116 raw_data = await self._get_cards_data(kind=MomentKinds.NOTE.value, page=page) 

117 raw_data = raw_data.get("moments", []) 

118 return self._data_processor.process_notes(raw_data) 

119 

120 async def bookmarks(self, page: int = 1) -> list[Any]: 

121 """Get bookmarks. 

122 

123 Args: 

124 page: Page number 

125 

126 Returns: 

127 List of bookmark data 

128 """ 

129 raw_data = await self._get_cards_data( 

130 kind=MomentKinds.BOOKMARK.value, page=page 

131 ) 

132 raw_data = raw_data.get("moments", []) 

133 return self._data_processor.process_bookmarks(raw_data) 

134 

135 async def my_images(self, page: int = 1) -> list[Any]: 

136 """Get images. 

137 

138 Args: 

139 page: Page number 

140 

141 Returns: 

142 List of image data 

143 """ 

144 raw_data = await self._get_cards_data(kind=MomentKinds.IMAGE.value, page=page) 

145 raw_data = raw_data.get("moments", []) 

146 return self._data_processor.process_images(raw_data) 

147 

148 async def plan_progress(self, page: int = 1) -> list[Any]: 

149 """Get plan progress. 

150 

151 Args: 

152 page: Page number 

153 

154 Returns: 

155 List of plan progress data 

156 """ 

157 raw_data = await self._get_cards_data( 

158 kind=MomentKinds.PLAN_SEGMENT_COMPLETION.value, page=page 

159 ) 

160 raw_data = raw_data.get("moments", []) 

161 return self._data_processor.process_plan_progress(raw_data) 

162 

163 async def plan_subscriptions(self, page: int = 1) -> list[Any]: 

164 """Get plan subscriptions. 

165 

166 Args: 

167 page: Page number 

168 

169 Returns: 

170 List of plan subscription data 

171 """ 

172 raw_data = await self._get_cards_data( 

173 kind=MomentKinds.PLAN_SUBSCRIPTION.value, page=page 

174 ) 

175 raw_data = raw_data.get("moments", []) 

176 return self._data_processor.process_plan_subscriptions(raw_data) 

177 

178 async def plan_completions(self, page: int = 1) -> list[Any]: 

179 """Get plan completions. 

180 

181 Args: 

182 page: Page number 

183 

184 Returns: 

185 List of plan completion data 

186 """ 

187 raw_data = await self._get_cards_data( 

188 kind=MomentKinds.PLAN_COMPLETION.value, page=page 

189 ) 

190 raw_data = raw_data.get("moments", []) 

191 return self._data_processor.process_plan_completions(raw_data) 

192 

193 async def convert_note_to_md(self) -> list[Any]: 

194 """Convert notes to markdown. 

195 

196 Returns: 

197 List of converted note data 

198 """ 

199 return await self.notes() 

200 

201 async def close(self) -> None: 

202 """Close the client and cleanup resources.""" 

203 if self._http_client: 

204 await self._http_client.close() 

205 

206 @property 

207 def username(self) -> str: 

208 """Get the username.""" 

209 return self._authenticator.username 

210 

211 @property 

212 def user_id(self) -> int | None: 

213 """Get the authenticated user ID.""" 

214 return self._user_id 

215 

216 # Bible API methods 

217 async def get_bible_configuration(self) -> dict[str, Any]: 

218 """Get Bible configuration. 

219 

220 Returns: 

221 Bible configuration data 

222 """ 

223 await self._ensure_authenticated() 

224 raw_data = await self._http_client.get_bible_configuration() 

225 return self._data_processor.process_bible_configuration(raw_data) 

226 

227 async def get_bible_versions( 

228 self, language_tag: str = "eng", version_type: str = "all" 

229 ) -> dict[str, Any]: 

230 """Get Bible versions for a language. 

231 

232 Args: 

233 language_tag: Language tag (e.g., 'eng', 'spa') 

234 version_type: Type of versions ('all', 'text', 'audio') 

235 

236 Returns: 

237 Bible versions data 

238 """ 

239 await self._ensure_authenticated() 

240 raw_data = await self._http_client.get_bible_versions( 

241 language_tag, version_type 

242 ) 

243 return self._data_processor.process_bible_versions(raw_data) 

244 

245 async def get_bible_version(self, version_id: int) -> dict[str, Any]: 

246 """Get specific Bible version details. 

247 

248 Args: 

249 version_id: Version ID 

250 

251 Returns: 

252 Bible version data 

253 """ 

254 await self._ensure_authenticated() 

255 raw_data = await self._http_client.get_bible_version(version_id) 

256 return self._data_processor.process_bible_version(raw_data) 

257 

258 async def get_bible_chapter( 

259 self, 

260 reference: str, 

261 version_id: int = 1, 

262 ) -> dict[str, Any]: 

263 """Get Bible chapter content. 

264 

265 Args: 

266 reference: USFM reference (e.g., 'GEN.1') 

267 version_id: Version ID 

268 

269 Returns: 

270 Chapter content data 

271 """ 

272 await self._ensure_authenticated() 

273 raw_data = await self._http_client.get_bible_chapter(version_id, reference) 

274 return self._data_processor.process_bible_chapter(raw_data) 

275 

276 async def get_recommended_languages(self, country: str = "US") -> dict[str, Any]: 

277 """Get recommended languages for a country. 

278 

279 Args: 

280 country: Country code (e.g., 'US', 'CA') 

281 

282 Returns: 

283 Recommended languages data 

284 """ 

285 await self._ensure_authenticated() 

286 raw_data = await self._http_client.get_recommended_languages(country) 

287 return self._data_processor.process_recommended_languages(raw_data) 

288 

289 # Audio Bible API methods 

290 async def get_audio_chapter( 

291 self, 

292 reference: str, 

293 version_id: int = 1, 

294 ) -> dict[str, Any]: 

295 """Get audio chapter information. 

296 

297 Args: 

298 reference: USFM reference (e.g., 'GEN.1') 

299 version_id: Audio version ID 

300 

301 Returns: 

302 Audio chapter data 

303 """ 

304 await self._ensure_authenticated() 

305 raw_data = await self._http_client.get_audio_chapter(version_id, reference) 

306 return self._data_processor.process_audio_chapter(raw_data) 

307 

308 async def get_audio_version(self, audio_id: int) -> dict[str, Any]: 

309 """Get audio version details. 

310 

311 Args: 

312 audio_id: Audio version ID 

313 

314 Returns: 

315 Audio version data 

316 """ 

317 await self._ensure_authenticated() 

318 raw_data = await self._http_client.get_audio_version(audio_id) 

319 return self._data_processor.process_audio_version(raw_data) 

320 

321 # Search API methods 

322 async def search_bible( 

323 self, 

324 query: str, 

325 version_id: int | None = None, 

326 book: str | None = None, 

327 page: int = 1, 

328 ) -> dict[str, Any]: 

329 """Search Bible text. 

330 

331 Args: 

332 query: Search query 

333 version_id: Version ID (optional) 

334 book: Book filter (optional) 

335 page: Page number 

336 

337 Returns: 

338 Search results data 

339 """ 

340 await self._ensure_authenticated() 

341 raw_data = await self._http_client.search_bible(query, version_id, book, page) 

342 return self._data_processor.process_search_bible(raw_data) 

343 

344 async def search_plans( 

345 self, query: str, language_tag: str = "en", page: int = 1 

346 ) -> dict[str, Any]: 

347 """Search reading plans. 

348 

349 Args: 

350 query: Search query 

351 language_tag: Language tag 

352 page: Page number 

353 

354 Returns: 

355 Plan search results data 

356 """ 

357 await self._ensure_authenticated() 

358 raw_data = await self._http_client.search_plans(query, language_tag, page) 

359 return self._data_processor.process_search_plans(raw_data) 

360 

361 async def search_users( 

362 self, query: str, language_tag: str = "eng", page: int = 1 

363 ) -> dict[str, Any]: 

364 """Search users. 

365 

366 Args: 

367 query: Search query 

368 language_tag: Language tag 

369 page: Page number 

370 

371 Returns: 

372 User search results data 

373 """ 

374 await self._ensure_authenticated() 

375 raw_data = await self._http_client.search_users(query, language_tag, page) 

376 return self._data_processor.process_search_users(raw_data) 

377 

378 # Videos API methods 

379 async def get_videos(self, language_tag: str = "eng") -> dict[str, Any]: 

380 """Get videos list. 

381 

382 Args: 

383 language_tag: Language tag 

384 

385 Returns: 

386 Videos data 

387 """ 

388 await self._ensure_authenticated() 

389 raw_data = await self._http_client.get_videos(language_tag) 

390 return self._data_processor.process_videos(raw_data) 

391 

392 async def get_video_details(self, video_id: int) -> dict[str, Any]: 

393 """Get video details. 

394 

395 Args: 

396 video_id: Video ID 

397 

398 Returns: 

399 Video details data 

400 """ 

401 await self._ensure_authenticated() 

402 raw_data = await self._http_client.get_video_details(video_id) 

403 return self._data_processor.process_video_details(raw_data) 

404 

405 # Badges API methods 

406 async def badges(self, page: int = 1) -> list[Any]: 

407 """Get badges. 

408 

409 Args: 

410 page: Page number 

411 

412 Returns: 

413 List of badge data 

414 """ 

415 raw_data = await self._get_cards_data(kind=MomentKinds.BADGE.value, page=page) 

416 

417 if raw_data.get("errors"): 

418 raw_data = raw_data.get("errors") 

419 

420 return self._data_processor.process_badges(raw_data) 

421 

422 # Images API methods 

423 async def get_images( 

424 self, reference: str, language_tag: str = "eng", page: int = 1 

425 ) -> dict[str, Any]: 

426 """Get images for a reference. 

427 

428 Args: 

429 reference: USFM reference 

430 language_tag: Language tag 

431 page: Page number 

432 

433 Returns: 

434 Images data 

435 """ 

436 await self._ensure_authenticated() 

437 raw_data = await self._http_client.get_images(reference, language_tag, page) 

438 raw_data = raw_data.get("images", []) 

439 return self._data_processor.process_images(raw_data) 

440 

441 async def get_image_upload_url(self) -> dict[str, Any]: 

442 """Get image upload URL and parameters. 

443 

444 Returns: 

445 Upload URL data 

446 """ 

447 await self._ensure_authenticated() 

448 raw_data = await self._http_client.get_image_upload_url() 

449 return self._data_processor.process_image_upload_url(raw_data) 

450 

451 # Events API methods 

452 async def search_events( 

453 self, 

454 query: str, 

455 latitude: float | None = None, 

456 longitude: float | None = None, 

457 page: int = 1, 

458 ) -> dict[str, Any]: 

459 """Search events. 

460 

461 Args: 

462 query: Search query 

463 latitude: Latitude (optional) 

464 longitude: Longitude (optional) 

465 page: Page number 

466 

467 Returns: 

468 Event search results data 

469 """ 

470 await self._ensure_authenticated() 

471 raw_data = await self._http_client.search_events( 

472 query, latitude, longitude, page 

473 ) 

474 return self._data_processor.process_search_events(raw_data) 

475 

476 async def get_event_details(self, event_id: int) -> dict[str, Any]: 

477 """Get event details. 

478 

479 Args: 

480 event_id: Event ID 

481 

482 Returns: 

483 Event details data 

484 """ 

485 await self._ensure_authenticated() 

486 raw_data = await self._http_client.get_event_details(event_id) 

487 return self._data_processor.process_event_details(raw_data) 

488 

489 async def get_saved_events(self, page: int = 1) -> dict[str, Any]: 

490 """Get saved events. 

491 

492 Args: 

493 page: Page number 

494 

495 Returns: 

496 Saved events data 

497 """ 

498 await self._ensure_authenticated() 

499 raw_data = await self._http_client.get_saved_events(page) 

500 return self._data_processor.process_saved_events(raw_data) 

501 

502 async def save_event( 

503 self, event_id: int, comments: dict[str, Any] | None = None 

504 ) -> dict[str, Any]: 

505 """Save event. 

506 

507 Args: 

508 event_id: Event ID 

509 comments: Comments (optional) 

510 

511 Returns: 

512 Save result data 

513 """ 

514 await self._ensure_authenticated() 

515 raw_data = await self._http_client.save_event(event_id, comments) 

516 return self._data_processor.process_save_event(raw_data) 

517 

518 async def delete_saved_event(self, event_id: int) -> dict[str, Any]: 

519 """Delete saved event. 

520 

521 Args: 

522 event_id: Event ID 

523 

524 Returns: 

525 Delete result data 

526 """ 

527 await self._ensure_authenticated() 

528 raw_data = await self._http_client.delete_saved_event(event_id) 

529 return self._data_processor.process_delete_saved_event(raw_data) 

530 

531 async def get_all_saved_event_ids(self) -> dict[str, Any]: 

532 """Get all saved event IDs. 

533 

534 Returns: 

535 All saved event IDs data 

536 """ 

537 await self._ensure_authenticated() 

538 raw_data = await self._http_client.get_all_saved_event_ids() 

539 return self._data_processor.process_all_saved_event_ids(raw_data) 

540 

541 async def get_event_configuration(self) -> dict[str, Any]: 

542 """Get event configuration. 

543 

544 Returns: 

545 Event configuration data 

546 """ 

547 await self._ensure_authenticated() 

548 raw_data = await self._http_client.get_event_configuration() 

549 return self._data_processor.process_event_configuration(raw_data) 

550 

551 # Moments API methods 

552 async def get_moments( 

553 self, 

554 page: int = 1, 

555 user_id: int | None = None, 

556 kind: str | None = None, 

557 version_id: int | None = None, 

558 usfm: str | None = None, 

559 ) -> dict[str, Any]: 

560 """Get moments list. 

561 

562 Args: 

563 page: Page number 

564 user_id: User ID (optional) 

565 kind: Kind of moment (optional) 

566 version_id: Bible version ID (optional) 

567 usfm: USFM reference (optional) 

568 

569 Returns: 

570 Moments data 

571 """ 

572 await self._ensure_authenticated() 

573 raw_data = await self._http_client.get_moments( 

574 page, user_id, kind, version_id, usfm 

575 ) 

576 return self._data_processor.process_moments_list(raw_data) 

577 

578 async def get_moment_details(self, moment_id: int) -> dict[str, Any]: 

579 """Get moment details. 

580 

581 Args: 

582 moment_id: Moment ID 

583 

584 Returns: 

585 Moment details data 

586 """ 

587 await self._ensure_authenticated() 

588 raw_data = await self._http_client.get_moment_details(moment_id) 

589 return self._data_processor.process_moment_details(raw_data) 

590 

591 async def create_moment( 

592 self, data: CreateMoment | dict[str, Any] 

593 ) -> dict[str, Any]: 

594 """Create a new moment. 

595 

596 Args: 

597 data: Moment data as CreateMoment model or dict 

598 

599 Returns: 

600 Created moment data 

601 """ 

602 await self._ensure_authenticated() 

603 # Convert to CreateMoment if dict is provided 

604 if isinstance(data, dict): 

605 moment_data = CreateMoment(**data) 

606 else: 

607 moment_data = data 

608 

609 # Add created_dt timestamp 

610 moment_dict = moment_data.model_dump() 

611 moment_dict["created_dt"] = datetime.now(tz=timezone.utc).isoformat() 

612 

613 raw_data = await self._http_client.create_moment(moment_dict) 

614 return self._data_processor.process_create_moment(raw_data) 

615 

616 async def update_moment(self, data: dict[str, Any]) -> dict[str, Any]: 

617 """Update an existing moment. 

618 

619 Args: 

620 data: Moment data 

621 

622 Returns: 

623 Updated moment data 

624 """ 

625 await self._ensure_authenticated() 

626 raw_data = await self._http_client.update_moment(data) 

627 return self._data_processor.process_update_moment(raw_data) 

628 

629 async def delete_moment(self, moment_id: int) -> dict[str, Any]: 

630 """Delete a moment. 

631 

632 Args: 

633 moment_id: Moment ID 

634 

635 Returns: 

636 Delete result data 

637 """ 

638 await self._ensure_authenticated() 

639 raw_data = await self._http_client.delete_moment(moment_id) 

640 return self._data_processor.process_delete_moment(raw_data) 

641 

642 async def get_moment_colors(self) -> dict[str, Any]: 

643 """Get available highlight colors. 

644 

645 Returns: 

646 Colors data 

647 """ 

648 await self._ensure_authenticated() 

649 raw_data = await self._http_client.get_moment_colors() 

650 return self._data_processor.process_moment_colors(raw_data) 

651 

652 async def get_moment_labels(self) -> dict[str, Any]: 

653 """Get moment labels. 

654 

655 Returns: 

656 Labels data 

657 """ 

658 await self._ensure_authenticated() 

659 raw_data = await self._http_client.get_moment_labels() 

660 return self._data_processor.process_moment_labels(raw_data) 

661 

662 async def get_verse_colors(self, usfm: str, version_id: int) -> dict[str, Any]: 

663 """Get verse highlight colors. 

664 

665 Args: 

666 usfm: USFM reference 

667 version_id: Bible version ID 

668 

669 Returns: 

670 Verse colors data 

671 """ 

672 await self._ensure_authenticated() 

673 raw_data = await self._http_client.get_verse_colors(usfm, version_id) 

674 return self._data_processor.process_verse_colors(raw_data) 

675 

676 async def hide_verse_colors(self, data: dict[str, Any]) -> dict[str, Any]: 

677 """Hide verse highlight colors. 

678 

679 Args: 

680 data: Hide colors data 

681 

682 Returns: 

683 Hide result data 

684 """ 

685 await self._ensure_authenticated() 

686 raw_data = await self._http_client.hide_verse_colors(data) 

687 return self._data_processor.process_hide_verse_colors(raw_data) 

688 

689 async def get_moments_configuration(self) -> dict[str, Any]: 

690 """Get moments configuration. 

691 

692 Returns: 

693 Moments configuration data 

694 """ 

695 await self._ensure_authenticated() 

696 raw_data = await self._http_client.get_moments_configuration() 

697 return self._data_processor.process_moments_configuration(raw_data) 

698 

699 # Comments API methods 

700 async def create_comment(self, moment_id: int, comment: str) -> dict[str, Any]: 

701 """Create a comment on a moment. 

702 

703 Args: 

704 moment_id: Moment ID 

705 comment: Comment text 

706 

707 Returns: 

708 Created comment data 

709 """ 

710 await self._ensure_authenticated() 

711 raw_data = await self._http_client.create_comment(moment_id, comment) 

712 return self._data_processor.process_create_comment(raw_data) 

713 

714 async def delete_comment(self, comment_id: int) -> dict[str, Any]: 

715 """Delete a comment. 

716 

717 Args: 

718 comment_id: Comment ID 

719 

720 Returns: 

721 Delete result data 

722 """ 

723 await self._ensure_authenticated() 

724 raw_data = await self._http_client.delete_comment(comment_id) 

725 return self._data_processor.process_delete_comment(raw_data) 

726 

727 # Likes API methods 

728 async def like_moment(self, moment_id: int) -> dict[str, Any]: 

729 """Like a moment. 

730 

731 Args: 

732 moment_id: Moment ID 

733 

734 Returns: 

735 Like result data 

736 """ 

737 await self._ensure_authenticated() 

738 raw_data = await self._http_client.like_moment(moment_id) 

739 return self._data_processor.process_like_moment(raw_data) 

740 

741 async def unlike_moment(self, moment_id: int) -> dict[str, Any]: 

742 """Unlike a moment. 

743 

744 Args: 

745 moment_id: Moment ID 

746 

747 Returns: 

748 Unlike result data 

749 """ 

750 await self._ensure_authenticated() 

751 raw_data = await self._http_client.unlike_moment(moment_id) 

752 return self._data_processor.process_unlike_moment(raw_data) 

753 

754 # Messaging API methods 

755 async def register_device( 

756 self, 

757 device_id: str, 

758 device_type: str = "android", 

759 user_id: int | None = None, 

760 old_device_id: str | None = None, 

761 tags: str | None = None, 

762 ) -> dict[str, Any]: 

763 """Register device for push notifications. 

764 

765 Args: 

766 device_id: Device ID 

767 device_type: Device type 

768 user_id: User ID (optional) 

769 old_device_id: Previous device ID (optional) 

770 tags: Device tags (optional) 

771 

772 Returns: 

773 Registration result data 

774 """ 

775 await self._ensure_authenticated() 

776 raw_data = await self._http_client.register_device( 

777 device_id, device_type, user_id, old_device_id, tags 

778 ) 

779 return self._data_processor.process_register_device(raw_data) 

780 

781 async def unregister_device(self, device_id: str) -> dict[str, Any]: 

782 """Unregister device from push notifications. 

783 

784 Args: 

785 device_id: Device ID 

786 

787 Returns: 

788 Unregistration result data 

789 """ 

790 await self._ensure_authenticated() 

791 raw_data = await self._http_client.unregister_device(device_id) 

792 return self._data_processor.process_unregister_device(raw_data) 

793 

794 # Themes API methods 

795 async def get_themes( 

796 self, page: int = 1, language_tag: str = "en" 

797 ) -> dict[str, Any]: 

798 """Get available themes. 

799 

800 Args: 

801 page: Page number 

802 language_tag: Language tag 

803 

804 Returns: 

805 Themes data 

806 """ 

807 await self._ensure_authenticated() 

808 raw_data = await self._http_client.get_themes(page, language_tag) 

809 return self._data_processor.process_themes(raw_data) 

810 

811 async def add_theme( 

812 self, 

813 theme_id: int, 

814 available_locales: list[str], 

815 colors: dict[str, Any], 

816 cta_urls: dict[str, Any], 

817 msgid_suffix: str, 

818 version_ids: dict[str, int], 

819 ) -> dict[str, Any]: 

820 """Add a theme to user's collection. 

821 

822 Args: 

823 theme_id: Theme ID 

824 available_locales: List of available locale codes 

825 colors: Theme colors dictionary 

826 cta_urls: Call-to-action URLs dictionary 

827 msgid_suffix: Message ID suffix 

828 version_ids: Dictionary of version IDs by locale code 

829 

830 Returns: 

831 Add result data 

832 """ 

833 await self._ensure_authenticated() 

834 raw_data = await self._http_client.add_theme( 

835 theme_id, available_locales, colors, cta_urls, msgid_suffix, version_ids 

836 ) 

837 return self._data_processor.process_add_theme(raw_data) 

838 

839 async def remove_theme(self, theme_id: int) -> dict[str, Any]: 

840 """Remove a theme from user's collection. 

841 

842 Args: 

843 theme_id: Theme ID 

844 

845 Returns: 

846 Remove result data 

847 """ 

848 await self._ensure_authenticated() 

849 raw_data = await self._http_client.remove_theme(theme_id) 

850 return self._data_processor.process_remove_theme(raw_data) 

851 

852 async def set_theme( 

853 self, theme_id: int, previous_theme_id: int | None = None 

854 ) -> dict[str, Any]: 

855 """Set active theme. 

856 

857 Args: 

858 theme_id: Theme ID 

859 previous_theme_id: Previous theme ID (optional) 

860 

861 Returns: 

862 Set result data 

863 """ 

864 await self._ensure_authenticated() 

865 raw_data = await self._http_client.set_theme(theme_id, previous_theme_id) 

866 return self._data_processor.process_set_theme(raw_data) 

867 

868 async def get_theme_description( 

869 self, theme_id: int, language_tag: str = "eng" 

870 ) -> dict[str, Any]: 

871 """Get theme description. 

872 

873 Args: 

874 theme_id: Theme ID 

875 language_tag: Language tag 

876 

877 Returns: 

878 Theme description data 

879 """ 

880 await self._ensure_authenticated() 

881 raw_data = await self._http_client.get_theme_description(theme_id, language_tag) 

882 return self._data_processor.process_theme_description(raw_data) 

883 

884 # Friendships API methods 

885 async def send_friend_request(self, user_id: int) -> dict[str, Any]: 

886 """Send a friend request to a user. 

887 

888 Args: 

889 user_id: User ID to send friend request to 

890 

891 Returns: 

892 Friend request response data with incoming and outgoing lists 

893 """ 

894 await self._ensure_authenticated() 

895 raw_data = await self._http_client.send_friend_request(user_id) 

896 return self._data_processor.process_send_friend_request(raw_data) 

897 

898 # Friends API methods 

899 async def get_friends(self, page: int = 1) -> dict[str, Any]: 

900 """Get the authenticated user's friends list. 

901 

902 Args: 

903 page: Page number for pagination. 

904 

905 Returns: 

906 Friends list data. 

907 """ 

908 await self._ensure_authenticated() 

909 return await self._http_client.get_friends(page) 

910 

911 async def get_all_friends(self, page: int = 1) -> dict[str, Any]: 

912 """Get all friends including extended metadata. 

913 

914 Args: 

915 page: Page number for pagination. 

916 

917 Returns: 

918 All friends data. 

919 """ 

920 await self._ensure_authenticated() 

921 return await self._http_client.get_all_friends(page) 

922 

923 async def delete_friend(self, user_id: int) -> dict[str, Any]: 

924 """Remove a friend by user ID. 

925 

926 Args: 

927 user_id: Friend's YouVersion user ID. 

928 

929 Returns: 

930 Delete operation result data. 

931 """ 

932 await self._ensure_authenticated() 

933 return await self._http_client.delete_friend(user_id) 

934 

935 # Extended friendships API methods 

936 async def get_incoming_friend_requests(self, page: int = 1) -> dict[str, Any]: 

937 """Get incoming friend requests. 

938 

939 Args: 

940 page: Page number for pagination. 

941 

942 Returns: 

943 Incoming friend request data. 

944 """ 

945 await self._ensure_authenticated() 

946 return await self._http_client.get_incoming_friend_requests(page) 

947 

948 async def get_friend_suggestions( 

949 self, page: int = 1, language_tag: str = "en" 

950 ) -> dict[str, Any]: 

951 """Get friend suggestions. 

952 

953 Args: 

954 page: Page number for pagination. 

955 language_tag: ISO 639-1 locale (e.g. ``en``). 

956 

957 Returns: 

958 Friend suggestion data. 

959 """ 

960 await self._ensure_authenticated() 

961 return await self._http_client.get_friend_suggestions(page, language_tag) 

962 

963 async def accept_friend_request(self, user_id: int) -> dict[str, Any]: 

964 """Accept an incoming friend request. 

965 

966 Args: 

967 user_id: Requester's user ID. 

968 

969 Returns: 

970 Accept operation result data. 

971 """ 

972 await self._ensure_authenticated() 

973 return await self._http_client.accept_friend_request(user_id) 

974 

975 async def decline_friend_request(self, user_id: int) -> dict[str, Any]: 

976 """Decline an incoming friend request. 

977 

978 Args: 

979 user_id: Requester's user ID. 

980 

981 Returns: 

982 Decline operation result data. 

983 """ 

984 await self._ensure_authenticated() 

985 return await self._http_client.decline_friend_request(user_id) 

986 

987 async def dismiss_friend_suggestion(self, user_id: int) -> dict[str, Any]: 

988 """Dismiss a friend suggestion. 

989 

990 Args: 

991 user_id: Suggested user's ID. 

992 

993 Returns: 

994 Dismiss operation result data. 

995 """ 

996 await self._ensure_authenticated() 

997 return await self._http_client.dismiss_friend_suggestion(user_id) 

998 

999 async def get_facebook_friends(self, page: int = 1) -> dict[str, Any]: 

1000 """Get Facebook-linked friend suggestions. 

1001 

1002 Args: 

1003 page: Page number for pagination. 

1004 

1005 Returns: 

1006 Facebook friends data. 

1007 """ 

1008 await self._ensure_authenticated() 

1009 return await self._http_client.get_facebook_friends(page) 

1010 

1011 async def sync_friendship_contacts( 

1012 self, contacts: list[dict[str, Any]] 

1013 ) -> dict[str, Any]: 

1014 """Upload contacts for friend matching. 

1015 

1016 Args: 

1017 contacts: Contact records to sync. 

1018 

1019 Returns: 

1020 Contact sync result data. 

1021 """ 

1022 await self._ensure_authenticated() 

1023 return await self._http_client.sync_friendship_contacts(contacts) 

1024 

1025 # Notifications API methods 

1026 async def get_notifications(self, page: int = 1) -> dict[str, Any]: 

1027 """Get notification feed items. 

1028 

1029 Args: 

1030 page: Page number for pagination. 

1031 

1032 Returns: 

1033 Notification feed data. 

1034 """ 

1035 await self._ensure_authenticated() 

1036 return await self._http_client.get_notifications(page) 

1037 

1038 async def get_notification_settings(self) -> dict[str, Any]: 

1039 """Get notification settings for the authenticated user. 

1040 

1041 Returns: 

1042 Notification settings data. 

1043 """ 

1044 await self._ensure_authenticated() 

1045 return await self._http_client.get_notification_settings() 

1046 

1047 async def update_notification_settings( 

1048 self, data: dict[str, Any] 

1049 ) -> dict[str, Any]: 

1050 """Update notification settings. 

1051 

1052 Args: 

1053 data: Settings fields to update. 

1054 

1055 Returns: 

1056 Updated settings data. 

1057 """ 

1058 await self._ensure_authenticated() 

1059 return await self._http_client.update_notification_settings(data) 

1060 

1061 async def update_notifications(self, data: dict[str, Any]) -> dict[str, Any]: 

1062 """Update notification read state. 

1063 

1064 Args: 

1065 data: Notification update payload. 

1066 

1067 Returns: 

1068 Update result data. 

1069 """ 

1070 await self._ensure_authenticated() 

1071 return await self._http_client.update_notifications(data) 

1072 

1073 async def get_votd_notification_settings(self) -> dict[str, Any]: 

1074 """Get verse-of-the-day notification settings. 

1075 

1076 Returns: 

1077 VOTD notification settings. 

1078 """ 

1079 await self._ensure_authenticated() 

1080 return await self._http_client.get_votd_notification_settings() 

1081 

1082 async def update_votd_notification_settings( 

1083 self, data: dict[str, Any] 

1084 ) -> dict[str, Any]: 

1085 """Update verse-of-the-day notification settings. 

1086 

1087 Args: 

1088 data: VOTD settings to update. 

1089 

1090 Returns: 

1091 Updated VOTD settings data. 

1092 """ 

1093 await self._ensure_authenticated() 

1094 return await self._http_client.update_votd_notification_settings(data) 

1095 

1096 # Share API methods 

1097 async def invite_by_email(self, data: dict[str, Any]) -> dict[str, Any]: 

1098 """Send a YouVersion invite by email. 

1099 

1100 Args: 

1101 data: Invite payload. 

1102 

1103 Returns: 

1104 Invite operation result data. 

1105 """ 

1106 await self._ensure_authenticated() 

1107 return await self._http_client.invite_by_email(data) 

1108 

1109 async def invite_by_sms(self, data: dict[str, Any]) -> dict[str, Any]: 

1110 """Send a YouVersion invite by SMS. 

1111 

1112 Args: 

1113 data: Invite payload. 

1114 

1115 Returns: 

1116 Invite operation result data. 

1117 """ 

1118 await self._ensure_authenticated() 

1119 return await self._http_client.invite_by_sms(data) 

1120 

1121 # Additional moments and search API methods 

1122 async def get_client_side_moments(self, page: int = 1) -> dict[str, Any]: 

1123 """Get client-side moment items. 

1124 

1125 Args: 

1126 page: Page number for pagination. 

1127 

1128 Returns: 

1129 Client-side moments data. 

1130 """ 

1131 await self._ensure_authenticated() 

1132 return await self._http_client.get_client_side_moments(page) 

1133 

1134 async def get_moments_votd(self, language_tag: str | None = None) -> dict[str, Any]: 

1135 """Get verse of the day from the moments API. 

1136 

1137 Args: 

1138 language_tag: Optional language tag; omit for default VOTD payload. 

1139 

1140 Returns: 

1141 VOTD data from moments service. 

1142 """ 

1143 await self._ensure_authenticated() 

1144 return await self._http_client.get_moments_votd(language_tag)