Coverage for youversion/core/http_client.py: 100%
320 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-26 11:31 +0100
« prev ^ index » next coverage.py v7.14.3, created at 2026-06-26 11:31 +0100
1"""HTTP client for YouVersion API operations."""
3from typing import Any
5import httpx
7from ..config import Config
8from .interfaces import IHttpClient
11class HttpClient(IHttpClient):
12 """Handles HTTP operations for YouVersion API."""
14 def __init__(self, client: httpx.AsyncClient, user_id: int | None = None):
15 """Initialize HTTP client.
17 Args:
18 client: Authenticated httpx.AsyncClient
19 user_id: Authenticated user ID
20 """
21 self._client = client
22 self._user_id = user_id
23 self._public_client: httpx.AsyncClient | None = None
25 def _ensure_public_client(self) -> httpx.AsyncClient:
26 """Return an httpx client without Authorization (public endpoints)."""
27 if self._public_client is None:
28 self._public_client = httpx.AsyncClient(
29 headers=dict(Config.DEFAULT_HEADERS),
30 timeout=Config.HTTP_TIMEOUT,
31 )
32 return self._public_client
34 @staticmethod
35 def _unwrap_envelope(data: dict[str, Any]) -> dict[str, Any]:
36 if "response" in data:
37 return data.get("response", {}).get("data", {})
38 return data
40 async def get(self, url: str, **kwargs) -> dict[str, Any]:
41 """Perform GET request.
43 Args:
44 url: URL to request
45 **kwargs: Additional arguments for the request
47 Returns:
48 JSON response as dictionary
49 """
50 # Always merge DEFAULT_HEADERS with any headers passed in kwargs
51 headers = {**Config.DEFAULT_HEADERS, **kwargs.get("headers", {})}
52 kwargs["headers"] = headers
53 response = await self._client.get(url, **kwargs)
54 try:
55 data = response.json()
56 # Support mocked AsyncMock json() returning a coroutine
57 if hasattr(data, "__await__"):
58 data = await data
59 return self._unwrap_envelope(data)
60 except ValueError:
61 # Re-raise to allow callers/tests to handle JSON parse errors
62 raise
64 async def get_public(self, url: str, **kwargs) -> dict[str, Any]:
65 """Perform GET without Authorization (endpoints that reject Bearer tokens).
67 Args:
68 url: URL to request
69 **kwargs: Additional arguments for the request
71 Returns:
72 JSON response data from the API envelope
73 """
74 headers = {**Config.DEFAULT_HEADERS, **kwargs.get("headers", {})}
75 kwargs["headers"] = headers
76 response = await self._ensure_public_client().get(url, **kwargs)
77 try:
78 data = response.json()
79 if hasattr(data, "__await__"):
80 data = await data
81 return self._unwrap_envelope(data)
82 except ValueError:
83 raise
85 async def post(self, url: str, **kwargs) -> dict[str, Any]:
86 """Perform POST request.
88 Args:
89 url: URL to request
90 **kwargs: Additional arguments for the request
92 Returns:
93 JSON response as dictionary
94 """
95 # Always merge DEFAULT_HEADERS with any headers passed in kwargs
96 headers = {**Config.DEFAULT_HEADERS, **kwargs.get("headers", {})}
97 kwargs["headers"] = headers
98 response = await self._client.post(url, **kwargs)
99 try:
100 data = response.json()
101 if hasattr(data, "__await__"):
102 data = await data
103 if hasattr(data, "__await__"):
104 data = await data
105 return self._unwrap_envelope(data)
106 except ValueError:
107 # Re-raise to allow callers/tests to handle JSON parse errors
108 raise
110 async def close(self) -> None:
111 """Close the HTTP client."""
112 if self._public_client:
113 await self._public_client.aclose()
114 if self._client:
115 await self._client.aclose()
117 # Legacy methods for backward compatibility
118 async def get_cards(self, page: int = 1, kind: str = "") -> dict[str, Any]:
119 """Get cards data from moments endpoint.
121 Args:
122 page: Page number
123 kind: Kind of data to retrieve
125 Returns:
126 Cards data as dictionary
127 """
128 url = f"{Config.MOMENTS_API_BASE}" f"{Config.MOMENTS_ITEMS_URL}"
129 # Build params dict, omitting falsy values (False, None, empty strings)
130 params = {}
131 if page is not None and page is not False:
132 params["page"] = page
133 if kind: # Excludes None, False, and empty strings
134 params["kind"] = kind
135 if self._user_id is not None and self._user_id is not False:
136 params["user_id"] = self._user_id
138 return await self.get(url, params=params)
140 async def get_verse_of_the_day(self) -> dict[str, Any]:
141 """Get verse of the day data.
143 Returns:
144 Verse of the day data as dictionary
145 """
147 return await self.get(Config.VOTD_URL)
149 # Bible API methods
150 async def get_bible_configuration(self) -> dict[str, Any]:
151 """Get Bible configuration.
153 Returns:
154 Bible configuration data
155 """
156 url = f"{Config.BIBLE_API_BASE}{Config.BIBLE_CONFIGURATION_URL}"
157 return await self.get(url)
159 async def get_bible_versions(
160 self, language_tag: str = "eng", version_type: str = "all"
161 ) -> dict[str, Any]:
162 """Get Bible versions for a language.
164 Args:
165 language_tag: Language tag (e.g., 'eng', 'spa')
166 version_type: Type of versions ('all', 'text', 'audio')
168 Returns:
169 Bible versions data
170 """
171 url = f"{Config.BIBLE_API_BASE}{Config.BIBLE_VERSIONS_URL}"
172 params = {"language_tag": language_tag, "type": version_type}
173 return await self.get(url, params=params)
175 async def get_bible_version(self, version_id: int) -> dict[str, Any]:
176 """Get specific Bible version details.
178 Args:
179 version_id: Version ID
181 Returns:
182 Bible version data
183 """
184 url = f"{Config.BIBLE_API_BASE}{Config.BIBLE_VERSION_URL}"
185 params = {"id": version_id}
186 return await self.get(url, params=params)
188 async def get_bible_chapter(
189 self, version_id: int, reference: str
190 ) -> dict[str, Any]:
191 """Get Bible chapter content.
193 Args:
194 version_id: Version ID
195 reference: USFM reference (e.g., 'GEN.1')
197 Returns:
198 Chapter content data
199 """
200 url = f"{Config.BIBLE_API_BASE}{Config.BIBLE_CHAPTER_URL}"
201 params = {"id": version_id, "reference": reference}
202 return await self.get_public(url, params=params)
204 async def get_recommended_languages(self, country: str = "US") -> dict[str, Any]:
205 """Get recommended languages for a country.
207 Args:
208 country: Country code (e.g., 'US', 'CA')
210 Returns:
211 Recommended languages data
212 """
213 url = f"{Config.BIBLE_API_BASE}{Config.BIBLE_RECOMMENDED_LANGUAGES_URL}"
214 params = {"country": country}
215 return await self.get(url, params=params)
217 # Audio Bible API methods
218 async def get_audio_chapter(
219 self, version_id: int, reference: str
220 ) -> dict[str, Any]:
221 """Get audio chapter information.
223 Args:
224 version_id: Audio version ID
225 reference: USFM reference (e.g., 'GEN.1')
227 Returns:
228 Audio chapter data
229 """
230 url = f"{Config.AUDIO_BIBLE_API_BASE}{Config.AUDIO_CHAPTER_URL}"
231 params = {"version_id": version_id, "reference": reference}
232 return await self.get_public(url, params=params)
234 async def get_audio_version(self, audio_id: int) -> dict[str, Any]:
235 """Get audio version details.
237 Args:
238 audio_id: Audio version ID
240 Returns:
241 Audio version data
242 """
243 url = f"{Config.AUDIO_BIBLE_API_BASE}{Config.AUDIO_VIEW_URL}"
244 params = {"id": audio_id}
245 return await self.get(url, params=params)
247 # Search API methods
248 async def search_bible(
249 self,
250 query: str,
251 version_id: int | None = None,
252 book: str | None = None,
253 page: int = 1,
254 ) -> dict[str, Any]:
255 """Search Bible text.
257 Args:
258 query: Search query
259 version_id: Version ID (optional)
260 book: Book filter (optional)
261 page: Page number
263 Returns:
264 Search results data
265 """
266 url = f"{Config.SEARCH_API_BASE}{Config.SEARCH_BIBLE_URL}"
267 params = {"query": query, "page": page}
268 if version_id:
269 params["version_id"] = version_id
270 if book:
271 params["book"] = book
272 return await self.get(url, params=params)
274 async def search_plans(
275 self, query: str, language_tag: str = "eng", page: int = 1
276 ) -> dict[str, Any]:
277 """Search reading plans.
279 Args:
280 query: Search query
281 language_tag: Language tag
282 page: Page number
284 Returns:
285 Plan search results data
286 """
287 url = f"{Config.SEARCH_API_BASE}{Config.SEARCH_PLANS_URL}"
288 params = {"query": query, "language_tag": language_tag, "page": page}
289 return await self.get(url, params=params)
291 async def search_users(
292 self, query: str, language_tag: str = "eng", page: int = 1
293 ) -> dict[str, Any]:
294 """Search users.
296 Args:
297 query: Search query
298 language_tag: Language tag
299 page: Page number
301 Returns:
302 User search results data
303 """
304 url = f"{Config.SEARCH_API_BASE}{Config.SEARCH_USERS_URL}"
305 params = {"query": query, "language_tag": language_tag, "page": page}
306 return await self.get(url, params=params)
308 # Videos API methods
309 async def get_videos(self, language_tag: str = "eng") -> dict[str, Any]:
310 """Get videos list.
312 Args:
313 language_tag: Language tag
315 Returns:
316 Videos data
317 """
318 url = f"{Config.SEARCH_API_BASE}{Config.SEARCH_VIDEOS_URL}"
319 params = {"language_tag": language_tag}
320 return await self.get(url, params=params)
322 async def get_video_details(self, video_id: int) -> dict[str, Any]:
323 """Get video details.
325 Args:
326 video_id: Video ID
328 Returns:
329 Video details data
330 """
331 url = f"{Config.VIDEOS_API_BASE}{Config.VIDEOS_VIEW_URL}"
332 params = {"id": video_id}
333 return await self.get(url, params=params)
335 # Images API methods
336 async def get_images(
337 self, reference: str, language_tag: str = "eng", page: int = 1
338 ) -> dict[str, Any]:
339 """Get images for a reference.
341 Args:
342 reference: USFM reference
343 language_tag: Language tag
344 page: Page number
346 Returns:
347 Images data
348 """
349 url = f"{Config.IMAGES_API_BASE}{Config.IMAGES_ITEMS_URL}"
350 params = {"reference": reference, "language_tag": language_tag, "page": page}
351 return await self.get(url, params=params)
353 async def get_image_upload_url(self) -> dict[str, Any]:
354 """Get image upload URL and parameters.
356 Returns:
357 Upload URL data
358 """
359 url = f"{Config.IMAGES_API_BASE}{Config.IMAGES_UPLOAD_URL}"
360 return await self.get(url)
362 # Events API methods
363 async def search_events(
364 self,
365 query: str,
366 latitude: float | None = None,
367 longitude: float | None = None,
368 page: int = 1,
369 ) -> dict[str, Any]:
370 """Search events.
372 Args:
373 query: Search query
374 latitude: Latitude (optional)
375 longitude: Longitude (optional)
376 page: Page number
378 Returns:
379 Event search results data
380 """
381 url = f"{Config.EVENTS_API_BASE}{Config.EVENTS_SEARCH_URL}"
382 params = {"query": query, "page": page}
383 if latitude:
384 params["latitude"] = latitude
385 if longitude:
386 params["longitude"] = longitude
387 return await self.get(url, params=params)
389 async def get_event_details(self, event_id: int) -> dict[str, Any]:
390 """Get event details.
392 Args:
393 event_id: Event ID
395 Returns:
396 Event details data
397 """
398 url = f"{Config.EVENTS_API_BASE}{Config.EVENTS_VIEW_URL}"
399 params = {"id": event_id}
400 return await self.get(url, params=params)
402 async def get_saved_events(self, page: int = 1) -> dict[str, Any]:
403 """Get saved events.
405 Args:
406 page: Page number
408 Returns:
409 Saved events data
410 """
411 url = f"{Config.EVENTS_API_BASE}{Config.EVENTS_SAVED_ITEMS_URL}"
412 params = {"page": page}
413 return await self.get(url, params=params)
415 async def save_event(
416 self, event_id: int, comments: dict[str, Any] | None = None
417 ) -> dict[str, Any]:
418 """Save event.
420 Args:
421 event_id: Event ID
422 comments: Comments (optional)
424 Returns:
425 Save result data
426 """
427 url = f"{Config.EVENTS_API_BASE}{Config.EVENTS_SAVE_URL}"
428 data = {"id": event_id}
429 if comments:
430 data["comments"] = comments
431 return await self.post(url, json=data)
433 async def delete_saved_event(self, event_id: int) -> dict[str, Any]:
434 """Delete saved event.
436 Args:
437 event_id: Event ID
439 Returns:
440 Delete result data
441 """
442 url = f"{Config.EVENTS_API_BASE}{Config.EVENTS_DELETE_SAVED_URL}"
443 return await self.post(url, json={"id": event_id})
445 async def get_all_saved_event_ids(self) -> dict[str, Any]:
446 """Get all saved event IDs.
448 Returns:
449 All saved event IDs data
450 """
451 url = f"{Config.EVENTS_API_BASE}{Config.EVENTS_SAVED_ALL_ITEMS_URL}"
452 return await self.get(url)
454 async def get_event_configuration(self) -> dict[str, Any]:
455 """Get event configuration.
457 Returns:
458 Event configuration data
459 """
460 url = f"{Config.EVENTS_API_BASE}{Config.EVENTS_CONFIGURATION_URL}"
461 return await self.get(url)
463 # Moments API methods
464 async def get_moments(
465 self,
466 page: int = 1,
467 user_id: int | None = None,
468 kind: str | None = None,
469 version_id: int | None = None,
470 usfm: str | None = None,
471 ) -> dict[str, Any]:
472 """Get moments list.
474 Args:
475 page: Page number
476 user_id: User ID (optional)
477 kind: Kind of moment (optional)
478 version_id: Bible version ID (optional)
479 usfm: USFM reference (optional)
481 Returns:
482 Moments data
483 """
484 url = f"{Config.MOMENTS_API_BASE}{Config.MOMENTS_ITEMS_URL}"
485 params = {"page": page}
486 if user_id:
487 params["user_id"] = user_id
488 if kind:
489 params["kind"] = kind
490 if version_id:
491 params["version_id"] = version_id
492 if usfm:
493 params["usfm"] = usfm
494 return await self.get(url, params=params)
496 async def get_moment_details(self, moment_id: int) -> dict[str, Any]:
497 """Get moment details.
499 Args:
500 moment_id: Moment ID
502 Returns:
503 Moment details data
504 """
505 url = f"{Config.MOMENTS_API_BASE}{Config.MOMENTS_VIEW_URL}"
506 params = {"id": moment_id}
507 return await self.get(url, params=params)
509 async def create_moment(self, data: dict[str, Any]) -> dict[str, Any]:
510 """Create a new moment.
512 Args:
513 data: Moment data
515 Returns:
516 Created moment data
517 """
518 url = f"{Config.MOMENTS_API_BASE}{Config.MOMENTS_CREATE_URL}"
519 return await self.post(url, json=data)
521 async def update_moment(self, data: dict[str, Any]) -> dict[str, Any]:
522 """Update an existing moment.
524 Args:
525 data: Moment data
527 Returns:
528 Updated moment data
529 """
530 url = f"{Config.MOMENTS_API_BASE}{Config.MOMENTS_UPDATE_URL}"
531 return await self.post(url, json=data)
533 async def delete_moment(self, moment_id: int) -> dict[str, Any]:
534 """Delete a moment.
536 Args:
537 moment_id: Moment ID
539 Returns:
540 Delete result data
541 """
542 url = f"{Config.MOMENTS_API_BASE}{Config.MOMENTS_DELETE_URL}"
543 data = {"id": moment_id}
544 return await self.post(url, json=data)
546 async def get_moment_colors(self) -> dict[str, Any]:
547 """Get available highlight colors.
549 Returns:
550 Colors data
551 """
552 url = f"{Config.MOMENTS_API_BASE}{Config.MOMENTS_COLORS_URL}"
553 return await self.get(url)
555 async def get_moment_labels(self) -> dict[str, Any]:
556 """Get moment labels.
558 Returns:
559 Labels data
560 """
561 url = f"{Config.MOMENTS_API_BASE}{Config.MOMENTS_LABELS_URL}"
562 return await self.get(url)
564 async def get_verse_colors(self, usfm: str, version_id: int) -> dict[str, Any]:
565 """Get verse highlight colors.
567 Args:
568 usfm: USFM reference
569 version_id: Bible version ID
571 Returns:
572 Verse colors data
573 """
574 url = f"{Config.MOMENTS_API_BASE}{Config.MOMENTS_VERSE_COLORS_URL}"
575 params = {"usfm": usfm, "version_id": version_id}
576 return await self.get(url, params=params)
578 async def hide_verse_colors(self, data: dict[str, Any]) -> dict[str, Any]:
579 """Hide verse highlight colors.
581 Args:
582 data: Hide colors data
584 Returns:
585 Hide result data
586 """
587 url = f"{Config.MOMENTS_API_BASE}{Config.MOMENTS_HIDE_VERSE_COLORS_URL}"
588 return await self.post(url, json=data)
590 async def get_moments_configuration(self) -> dict[str, Any]:
591 """Get moments configuration.
593 Returns:
594 Moments configuration data
595 """
596 url = f"{Config.MOMENTS_API_BASE}{Config.MOMENTS_CONFIGURATION_URL}"
597 return await self.get_public(url)
599 # Comments API methods
600 async def create_comment(self, moment_id: int, comment: str) -> dict[str, Any]:
601 """Create a comment on a moment.
603 Args:
604 moment_id: Moment ID
605 comment: Comment text
607 Returns:
608 Created comment data
609 """
610 url = f"{Config.MOMENTS_API_BASE}{Config.COMMENTS_CREATE_URL}"
611 data = {"moment_id": moment_id, "comment": comment}
612 return await self.post(url, json=data)
614 async def delete_comment(self, comment_id: int) -> dict[str, Any]:
615 """Delete a comment.
617 Args:
618 comment_id: Comment ID
620 Returns:
621 Delete result data
622 """
623 url = f"{Config.MOMENTS_API_BASE}{Config.COMMENTS_DELETE_URL}"
624 data = {"id": comment_id}
625 return await self.post(url, json=data)
627 # Likes API methods
628 async def like_moment(self, moment_id: int) -> dict[str, Any]:
629 """Like a moment.
631 Args:
632 moment_id: Moment ID
634 Returns:
635 Like result data
636 """
637 url = f"{Config.MOMENTS_API_BASE}{Config.LIKES_CREATE_URL}"
638 data = {"moment_id": moment_id}
639 return await self.post(url, json=data)
641 async def unlike_moment(self, moment_id: int) -> dict[str, Any]:
642 """Unlike a moment.
644 Args:
645 moment_id: Moment ID
647 Returns:
648 Unlike result data
649 """
650 url = f"{Config.MOMENTS_API_BASE}{Config.LIKES_DELETE_URL}"
651 data = {"moment_id": moment_id}
652 return await self.post(url, json=data)
654 # Messaging API methods
655 async def register_device(
656 self,
657 device_id: str,
658 device_type: str = "android",
659 user_id: int | None = None,
660 old_device_id: str | None = None,
661 tags: str | None = None,
662 ) -> dict[str, Any]:
663 """Register device for push notifications.
665 Args:
666 device_id: Device ID
667 device_type: Device type
668 user_id: User ID (optional)
669 old_device_id: Previous device ID (optional)
670 tags: Device tags (optional)
672 Returns:
673 Registration result data
674 """
675 url = f"{Config.MOMENTS_API_BASE}{Config.MESSAGING_REGISTER_URL}"
676 data = {"id": device_id, "type": device_type}
677 if user_id:
678 data["user_id"] = user_id
679 if old_device_id:
680 data["old_id"] = old_device_id
681 if tags:
682 data["tags"] = tags
683 return await self.post(url, json=data)
685 async def unregister_device(self, device_id: str) -> dict[str, Any]:
686 """Unregister device from push notifications.
688 Args:
689 device_id: Device ID
691 Returns:
692 Unregistration result data
693 """
694 url = f"{Config.MOMENTS_API_BASE}{Config.MESSAGING_UNREGISTER_URL}"
695 data = {"id": device_id}
696 return await self.post(url, json=data)
698 # Themes API methods
699 async def get_themes(
700 self, page: int = 1, language_tag: str = "en"
701 ) -> dict[str, Any]:
702 """Get available themes.
704 Args:
705 page: Page number
706 language_tag: Language tag
708 Returns:
709 Themes data
710 """
711 url = f"{Config.THEMES_API_BASE}{Config.THEMES_ITEMS_URL}"
712 params = {"page": page, "language_tag": language_tag}
713 return await self.get(url, params=params)
715 async def add_theme(
716 self,
717 theme_id: int,
718 available_locales: list[str],
719 colors: dict[str, Any],
720 cta_urls: dict[str, Any],
721 msgid_suffix: str,
722 version_ids: dict[str, int],
723 ) -> dict[str, Any]:
724 """Add a theme to user's collection.
726 Args:
727 theme_id: Theme ID
728 available_locales: List of available locale codes
729 colors: Theme colors dictionary
730 cta_urls: Call-to-action URLs dictionary
731 msgid_suffix: Message ID suffix
732 version_ids: List of version IDs
734 Returns:
735 Add result data
736 """
737 url = f"{Config.THEMES_API_BASE}{Config.THEMES_ADD_URL}"
738 data = {
739 "id": theme_id,
740 "available_locales": available_locales,
741 "colors": colors,
742 "cta_urls": cta_urls,
743 "msgid_suffix": msgid_suffix,
744 "version_ids": version_ids,
745 }
747 return await self.post(url, json=data)
749 async def remove_theme(self, theme_id: int) -> dict[str, Any]:
750 """Remove a theme from user's collection.
752 Args:
753 theme_id: Theme ID
755 Returns:
756 Remove result data
757 """
758 url = f"{Config.THEMES_API_BASE}{Config.THEMES_REMOVE_URL}"
759 data = {"id": theme_id}
760 return await self.post(url, json=data)
762 async def set_theme(
763 self, theme_id: int, previous_theme_id: int | None = None
764 ) -> dict[str, Any]:
765 """Set active theme.
767 Args:
768 theme_id: Theme ID
769 previous_theme_id: Previous theme ID (optional)
771 Returns:
772 Set result data
773 """
774 url = f"{Config.THEMES_API_BASE}{Config.THEMES_SET_URL}"
775 data = {"id": theme_id}
776 if previous_theme_id:
777 data["previous_id"] = previous_theme_id
778 return await self.post(url, json=data)
780 async def get_theme_description(
781 self, theme_id: int, language_tag: str = "eng"
782 ) -> dict[str, Any]:
783 """Get theme description.
785 Args:
786 theme_id: Theme ID
787 language_tag: Language tag
789 Returns:
790 Theme description data
791 """
792 url = f"{Config.THEMES_API_BASE}{Config.THEMES_DESCRIPTION_URL}"
793 params = {"id": theme_id, "language_tag": language_tag}
794 return await self.get(url, params=params)
796 # Friendships API methods
797 async def send_friend_request(self, user_id: int) -> dict[str, Any]:
798 """Send a friend request to a user.
800 Args:
801 user_id: User ID to send friend request to
803 Returns:
804 Friend request response data
805 """
806 url = f"{Config.FRIENDSHIPS_API_BASE}{Config.FRIENDSHIPS_OFFER_URL}"
807 data = {"user_id": user_id}
808 return await self.post(url, json=data)
810 # Friends API methods
811 async def get_friends(self, page: int = 1) -> dict[str, Any]:
812 """Get the authenticated user's friends list.
814 Args:
815 page: Page number for pagination.
817 Returns:
818 Friends list data including ``users`` and ``next_page``.
819 """
820 url = f"{Config.FRIENDS_API_BASE}{Config.FRIENDS_ITEMS_URL}"
821 return await self.get(url, params={"page": page})
823 async def get_all_friends(self, page: int = 1) -> dict[str, Any]:
824 """Get all friends including extended friend metadata.
826 Args:
827 page: Page number for pagination.
829 Returns:
830 All friends data including a ``friends`` collection.
831 """
832 url = f"{Config.FRIENDS_API_BASE}{Config.FRIENDS_ALL_ITEMS_URL}"
833 return await self.get(url, params={"page": page})
835 async def delete_friend(self, user_id: int) -> dict[str, Any]:
836 """Remove a friend.
838 Args:
839 user_id: YouVersion user ID of the friend to remove.
841 Returns:
842 Delete operation result data.
843 """
844 url = f"{Config.FRIENDS_API_BASE}{Config.FRIENDS_DELETE_URL}"
845 return await self.post(url, json={"user_id": user_id})
847 # Friendships API methods
848 async def get_incoming_friend_requests(self, page: int = 1) -> dict[str, Any]:
849 """Get incoming friend requests.
851 Args:
852 page: Page number for pagination.
854 Returns:
855 Incoming friend request data.
856 """
857 url = f"{Config.FRIENDSHIPS_API_BASE}{Config.FRIENDSHIPS_INCOMING_URL}"
858 return await self.get(url, params={"page": page})
860 async def get_friend_suggestions(
861 self, page: int = 1, language_tag: str = "en"
862 ) -> dict[str, Any]:
863 """Get friend suggestions.
865 Args:
866 page: Page number for pagination.
867 language_tag: ISO 639-1 locale (e.g. ``en``, not ``eng``).
869 Returns:
870 Friend suggestion data.
871 """
872 url = f"{Config.FRIENDSHIPS_API_BASE}{Config.FRIENDSHIPS_SUGGESTIONS_URL}"
873 return await self.get(url, params={"page": page, "language_tag": language_tag})
875 async def accept_friend_request(self, user_id: int) -> dict[str, Any]:
876 """Accept an incoming friend request.
878 Args:
879 user_id: User ID of the requester to accept.
881 Returns:
882 Accept operation result data.
883 """
884 url = f"{Config.FRIENDSHIPS_API_BASE}{Config.FRIENDSHIPS_ACCEPT_URL}"
885 return await self.post(url, json={"user_id": user_id})
887 async def decline_friend_request(self, user_id: int) -> dict[str, Any]:
888 """Decline an incoming friend request.
890 Args:
891 user_id: User ID of the requester to decline.
893 Returns:
894 Decline operation result data.
895 """
896 url = f"{Config.FRIENDSHIPS_API_BASE}{Config.FRIENDSHIPS_DECLINE_URL}"
897 return await self.post(url, json={"user_id": user_id})
899 async def dismiss_friend_suggestion(self, user_id: int) -> dict[str, Any]:
900 """Dismiss a friend suggestion.
902 Args:
903 user_id: Suggested user ID to dismiss.
905 Returns:
906 Dismiss operation result data.
907 """
908 url = (
909 f"{Config.FRIENDSHIPS_API_BASE}{Config.FRIENDSHIPS_DISMISS_SUGGESTION_URL}"
910 )
911 return await self.post(url, json={"user_id": user_id})
913 async def get_facebook_friends(self, page: int = 1) -> dict[str, Any]:
914 """Get Facebook-linked friend suggestions.
916 Args:
917 page: Page number for pagination.
919 Returns:
920 Facebook friends data.
921 """
922 url = f"{Config.FRIENDSHIPS_API_BASE}{Config.FRIENDSHIPS_FACEBOOK_FRIENDS_URL}"
923 return await self.get(url, params={"page": page})
925 async def sync_friendship_contacts(
926 self, contacts: list[dict[str, Any]]
927 ) -> dict[str, Any]:
928 """Upload device contacts for friend matching.
930 Args:
931 contacts: Contact records to sync with YouVersion.
933 Returns:
934 Contact sync result data.
935 """
936 url = f"{Config.FRIENDSHIPS_API_BASE}{Config.FRIENDSHIPS_CONTACTS_URL}"
937 return await self.post(url, json={"contacts": contacts})
939 # Notifications API methods
940 async def get_notifications(self, page: int = 1) -> dict[str, Any]:
941 """Get notification feed items.
943 Args:
944 page: Page number for pagination.
946 Returns:
947 Notification items and unread counts.
948 """
949 url = f"{Config.NOTIFICATIONS_API_BASE}{Config.NOTIFICATIONS_ITEMS_URL}"
950 return await self.get(url, params={"page": page})
952 async def get_notification_settings(self) -> dict[str, Any]:
953 """Get notification settings for the authenticated user.
955 Returns:
956 Notification settings payload.
957 """
958 url = f"{Config.NOTIFICATIONS_API_BASE}{Config.NOTIFICATIONS_SETTINGS_URL}"
959 return await self.get(url)
961 async def update_notification_settings(
962 self, data: dict[str, Any]
963 ) -> dict[str, Any]:
964 """Update notification settings.
966 Args:
967 data: Settings fields to update.
969 Returns:
970 Updated settings data.
971 """
972 url = (
973 f"{Config.NOTIFICATIONS_API_BASE}{Config.NOTIFICATIONS_UPDATE_SETTINGS_URL}"
974 )
975 return await self.post(url, json=data)
977 async def update_notifications(self, data: dict[str, Any]) -> dict[str, Any]:
978 """Mark notifications read or update notification state.
980 Args:
981 data: Notification update payload.
983 Returns:
984 Update result data.
985 """
986 url = f"{Config.NOTIFICATIONS_API_BASE}{Config.NOTIFICATIONS_UPDATE_URL}"
987 return await self.post(url, json=data)
989 async def get_votd_notification_settings(self) -> dict[str, Any]:
990 """Get verse-of-the-day notification settings.
992 Returns:
993 VOTD notification settings data.
994 """
995 url = f"{Config.NOTIFICATIONS_API_BASE}{Config.NOTIFICATIONS_VOTD_SETTINGS_URL}"
996 return await self.get(url)
998 async def update_votd_notification_settings(
999 self, data: dict[str, Any]
1000 ) -> dict[str, Any]:
1001 """Update verse-of-the-day notification settings.
1003 Args:
1004 data: VOTD settings fields to update.
1006 Returns:
1007 Updated VOTD settings data.
1008 """
1009 url = (
1010 f"{Config.NOTIFICATIONS_API_BASE}"
1011 f"{Config.NOTIFICATIONS_UPDATE_VOTD_SETTINGS_URL}"
1012 )
1013 return await self.post(url, json=data)
1015 # Share API methods
1016 async def invite_by_email(self, data: dict[str, Any]) -> dict[str, Any]:
1017 """Send a YouVersion invite email.
1019 Args:
1020 data: Invite payload (e.g. email address and message fields).
1022 Returns:
1023 Invite operation result data.
1024 """
1025 url = f"{Config.SHARE_API_BASE}{Config.SHARE_INVITE_EMAIL_URL}"
1026 return await self.post(url, json=data)
1028 async def invite_by_sms(self, data: dict[str, Any]) -> dict[str, Any]:
1029 """Send a YouVersion invite SMS.
1031 Args:
1032 data: Invite payload (e.g. phone number and message fields).
1034 Returns:
1035 Invite operation result data.
1036 """
1037 url = f"{Config.SHARE_API_BASE}{Config.SHARE_INVITE_SMS_URL}"
1038 return await self.post(url, json=data)
1040 # Additional moments API methods
1041 async def get_client_side_moments(self, page: int = 1) -> dict[str, Any]:
1042 """Get client-side moment items.
1044 Args:
1045 page: Page number for pagination.
1047 Returns:
1048 Client-side moments data.
1049 """
1050 url = f"{Config.MOMENTS_API_BASE}{Config.MOMENTS_CLIENT_SIDE_ITEMS_URL}"
1051 return await self.get(url, params={"page": page})
1053 async def get_moments_votd(self, language_tag: str | None = None) -> dict[str, Any]:
1054 """Get verse of the day from the moments API.
1056 Args:
1057 language_tag: Optional language tag; omit for default VOTD payload.
1059 Returns:
1060 Verse of the day data from moments service.
1061 """
1062 url = f"{Config.MOMENTS_API_BASE}{Config.MOMENTS_VOTD_URL}"
1063 params: dict[str, Any] = {}
1064 if language_tag:
1065 params["language_tag"] = language_tag
1066 return await self.get_public(url, params=params or None)