pyolivetree 2.0.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,55 @@
1
+ """In-app XML endpoints: product registration, info and download links."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pyolivetree.constants import XML
8
+ from pyolivetree.resources._base import Resource, ids
9
+
10
+
11
+ class Products(Resource):
12
+ """``www.olivetree.com/store/br5/app/in-app/xml``. Available as ``ot.products``.
13
+
14
+ These return XML as ``str``.
15
+ """
16
+
17
+ def _post(self, path: str, **kw: Any) -> Any:
18
+ return self._c.request("POST", f"{XML}/{path}", **kw)
19
+
20
+ def download_links(self, *product_ids: int) -> str:
21
+ """``<documents>`` XML describing each owned product and its download URLs.
22
+
23
+ Purchased book files are encrypted; this returns where to fetch them, not readable text.
24
+ """
25
+ return self._post(f"download_products/{ids(product_ids)}")
26
+
27
+ def register(self, **data: Any) -> str:
28
+ """Register products installed on this device."""
29
+ return self._post("register_products", data=data)
30
+
31
+ def info(self, **data: Any) -> str:
32
+ """Product info for installed products."""
33
+ return self._post("product_info", data=data)
34
+
35
+ def updates(self, body: Any = None) -> str:
36
+ """Check installed products for updates.
37
+
38
+ Args:
39
+ body: JSON body describing installed products.
40
+ """
41
+ return self._post("product_updates", json=body or {}, auth=False)
42
+
43
+ def email_signup(self, email: str, bibles: str = "", token: str = "") -> str:
44
+ """Sign an email up for free Bibles / newsletters."""
45
+ return self._post("email_signup", data={"email": email, "bibles": bibles, "token": token})
46
+
47
+ def reading_templates(self, *template_ids: int) -> str:
48
+ """Reading-plan template XML.
49
+
50
+ Args:
51
+ *template_ids: Template ids (``reading_template_id`` in your reading plans).
52
+ """
53
+ return self._c.request(
54
+ "GET", f"{XML}/download_reading_templates", params={"templateIds": ids(template_ids)}, auth=False
55
+ )
@@ -0,0 +1,53 @@
1
+ """Reading plans, devotionals, recommendations and verse of the day."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pyolivetree.constants import SPEECH, VOTD, WWW
8
+ from pyolivetree.resources._base import Resource
9
+
10
+
11
+ class Reading(Resource):
12
+ """Available as ``ot.reading``. No login needed."""
13
+
14
+ def daily_reading(self, type: str = "featured", **params: Any) -> Any:
15
+ """Reading plans and devotionals.
16
+
17
+ Args:
18
+ type: ``"featured"``, ``"template_versions"``, ...
19
+ **params: ``template_ids``, ``template_id``, ``num_recommended``, ``q``.
20
+ """
21
+ return self._c.request(
22
+ "GET",
23
+ f"{WWW}/api/daily_reading_v2.php",
24
+ auth=False,
25
+ params={"type": type, "supportsDevotionalPlans": "true", **params},
26
+ )
27
+
28
+ def recommended(self, **data: Any) -> Any:
29
+ """Recommended content for the home screen."""
30
+ return self._c.request("POST", f"{WWW}/api/recommended.php", data=data, auth=False)
31
+
32
+ def verse_of_the_day(self, date: str, version: str | None = None) -> str:
33
+ """Verse of the day page.
34
+
35
+ Args:
36
+ date: ``YYYY-MM-DD``.
37
+ version: Optional Bible version code.
38
+ """
39
+ return self._c.request(
40
+ "GET", f"{WWW}/votd/index.php", auth=False, params={"date": date, "version": version}
41
+ )
42
+
43
+ def verse_of_the_day_image(self, month: int, day: int, product_id: int) -> bytes:
44
+ """Verse of the day image (bytes)."""
45
+ return self._c.request("GET", f"{VOTD}/{month}/{day}/{product_id}", auth=False)
46
+
47
+ def now_reading(self, locale: str, path: str) -> Any:
48
+ """Text-to-speech audio for an audio reading plan."""
49
+ return self._c.request("GET", f"{SPEECH}/nowreading/{locale}/{path}", auth=False)
50
+
51
+ def completed_message(self, locale: str = "en") -> dict:
52
+ """Audio played when a reading is completed (``{"urls": [...]}``)."""
53
+ return self._c.request("GET", f"{SPEECH}/messages/{locale}/completed", auth=False)
@@ -0,0 +1,185 @@
1
+ """Store endpoints: catalog, search, your library and wishlists."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator
6
+ from typing import Any
7
+
8
+ from pyolivetree.constants import STORE
9
+ from pyolivetree.resources._base import Resource, ids
10
+
11
+
12
+ class Store(Resource):
13
+ """``www.olivetree.com/store/br5/app/json/store``. Available as ``ot.store``.
14
+
15
+ Most list endpoints return a ``product_set``: ``{"product_results": [...], "pages": [urls],
16
+ "total_count": n}``. Use :meth:`all_products` to walk every page.
17
+ """
18
+
19
+ def _post(self, path: str, **data: Any) -> Any:
20
+ return self._c.request("POST", f"{STORE}/{path}", data=data)
21
+
22
+ # --- catalog and feeds ---------------------------------------------------
23
+
24
+ def home(self, **kw: Any) -> dict:
25
+ """Store home feed (``{"rows": [...]}``)."""
26
+ return self._post("home", **kw)
27
+
28
+ def banner(self, **kw: Any) -> dict:
29
+ """Current promotional banner."""
30
+ return self._post("banner", **kw)
31
+
32
+ def featured_categories(self, **kw: Any) -> dict:
33
+ """Featured store categories (``{"categories": [...]}``)."""
34
+ return self._post("featured_categories", **kw)
35
+
36
+ def category(self, category_id: int, **kw: Any) -> dict:
37
+ """Products in a category.
38
+
39
+ Args:
40
+ category_id: Category id (from :meth:`featured_categories` or :meth:`home`).
41
+ """
42
+ return self._post(f"cat/{category_id}", **kw)
43
+
44
+ def search(self, q: str, **kw: Any) -> dict:
45
+ """Search the store.
46
+
47
+ Args:
48
+ q: Search text, e.g. ``"study bible"``.
49
+
50
+ Returns:
51
+ A ``product_set``.
52
+ """
53
+ return self._c.request("POST", f"{STORE}/search", params={"q": q}, data=kw)
54
+
55
+ def product(self, product_id: int) -> dict:
56
+ """Full details for one product.
57
+
58
+ Args:
59
+ product_id: The ``productId`` field from any product listing.
60
+ """
61
+ return self._post(f"product/{product_id}")
62
+
63
+ def products(self, *product_ids: int) -> dict:
64
+ """Details for several products at once, as a ``product_set``."""
65
+ return self._post(f"product_set/{ids(product_ids)}")
66
+
67
+ def path(self, dynamic_path: str, **kw: Any) -> Any:
68
+ """Open any store page by path (e.g. a home-feed ``path`` value).
69
+
70
+ Args:
71
+ dynamic_path: Path below ``/store/br5/app/json/store/``.
72
+ """
73
+ return self._post(dynamic_path.lstrip("/"), **kw)
74
+
75
+ def all_products(self, result: dict) -> Iterator[dict]:
76
+ """Yield every product across all pages of a ``product_set``.
77
+
78
+ Args:
79
+ result: A first page from :meth:`purchased`, :meth:`search`, :meth:`category`, etc.
80
+
81
+ Yields:
82
+ Product dicts (``productId``, ``title``, ``owns_product``, ...).
83
+ """
84
+ yield from result.get("product_results") or []
85
+ for url in (result.get("pages") or [])[1:]:
86
+ yield from self._c.request("POST", url).get("product_results") or []
87
+
88
+ # --- your account ----------------------------------------------------------
89
+
90
+ def purchased(self, **kw: Any) -> dict:
91
+ """Products you own, as a ``product_set``."""
92
+ return self._post("purchased", **kw)
93
+
94
+ def library(self, **kw: Any) -> Any:
95
+ """Library listing shown on first run."""
96
+ return self._post("library", **kw)
97
+
98
+ def library_details(self, *product_ids: int) -> Any:
99
+ """Library metadata for owned products."""
100
+ return self._post(f"library_details/{ids(product_ids)}")
101
+
102
+ def wishlists(self, **kw: Any) -> Any:
103
+ """Your wishlists (``{"lists": [...]}``)."""
104
+ return self._post("user_wishlists", **kw)
105
+
106
+ def wishlist_add(self, product_id: int, wishlist_id: int | None = None) -> Any:
107
+ """Add a product to a wishlist. Changes your account."""
108
+ return self._post("modify_wishlist/add", productId=product_id, wishlistId=wishlist_id)
109
+
110
+ def wishlist_remove(self, product_id: int, wishlist_id: int | None = None) -> Any:
111
+ """Remove a product from a wishlist. Changes your account."""
112
+ return self._post("modify_wishlist/remove", productId=product_id, wishlistId=wishlist_id)
113
+
114
+ # --- onboarding and marketing ---------------------------------------------
115
+
116
+ def first_run_subscriptions(self, **kw: Any) -> Any:
117
+ """Subscriptions offered on first launch."""
118
+ return self._post("first_run_subscriptions", **kw)
119
+
120
+ def first_run_interests(self, **kw: Any) -> Any:
121
+ """Interest topics offered on first launch."""
122
+ return self._post("first_run_interests", **kw)
123
+
124
+ def first_run_bible_list(self, **kw: Any) -> Any:
125
+ """Free Bibles offered on first launch."""
126
+ return self._post("first_run_bible_list", **kw)
127
+
128
+ def need_interests(self, **kw: Any) -> Any:
129
+ """Whether to ask the user for interests."""
130
+ return self._post("need_interests", **kw)
131
+
132
+ def paid_features(self, **kw: Any) -> Any:
133
+ """Paid features unlocked on your account."""
134
+ return self._post("paid_features", **kw)
135
+
136
+ def paid_features_marketing(self, **kw: Any) -> Any:
137
+ """Marketing copy for paid features."""
138
+ return self._post("paid_features_marketing", **kw)
139
+
140
+ def ads(self, **kw: Any) -> Any:
141
+ """In-app ads."""
142
+ return self._post("ads", **kw)
143
+
144
+ def product_manifest(self, **kw: Any) -> Any:
145
+ """Product manifest used to check for updates."""
146
+ return self._post("product_manifest", **kw)
147
+
148
+ def giveaway(self, code: str) -> Any:
149
+ """Look up or redeem a giveaway code.
150
+
151
+ Args:
152
+ code: The giveaway code.
153
+ """
154
+ return self._post(f"giveaway/{code}")
155
+
156
+ # --- subscriptions and purchases (change your account) --------------------
157
+
158
+ def subscriptions_available(self, **kw: Any) -> Any:
159
+ """Subscriptions you can buy."""
160
+ return self._post("subscriptions_available", **kw)
161
+
162
+ def allow_subscription_purchase(self, base_plan_id: str) -> Any:
163
+ """Check whether a subscription plan can be bought."""
164
+ return self._post(f"allow_subscription_purchase/{base_plan_id}")
165
+
166
+ def subscription_variable_product_selection(self, subscription_id: int) -> Any:
167
+ """Products to choose from for a pick-your-books subscription."""
168
+ return self._post(f"subscription_variable_product_selection/{subscription_id}")
169
+
170
+ def register_subscription(self, **data: Any) -> Any:
171
+ """Register a store purchase receipt. Changes your account.
172
+
173
+ Args:
174
+ **data: ``signupSource``, ``receiptData``, ``subscriptionProductId``,
175
+ ``platformToken``, ``packageName``.
176
+ """
177
+ return self._post("register_subscription", **data)
178
+
179
+ def validate_google_in_app_purchase(self, **data: Any) -> Any:
180
+ """Validate a Google Play purchase. Changes your account."""
181
+ return self._post("validate_google_in_app_purchase", **data)
182
+
183
+ def support_delete_account(self, **data: Any) -> Any:
184
+ """Request deletion of your Olive Tree account. **Irreversible.**"""
185
+ return self._post("support_delete_account", **data)
@@ -0,0 +1,156 @@
1
+ """Sync endpoints: your notes, highlights, library and reading-plan data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import platform
6
+ from collections.abc import Iterator
7
+ from typing import Any
8
+
9
+ from pyolivetree.constants import PLATFORM_ID, SYNC, SYNC_PROTOCOL, DataSet, data_set_name
10
+ from pyolivetree.resources._base import Resource
11
+
12
+
13
+ class Sync(Resource):
14
+ """``sync.olivetree.com``. Available as ``ot.sync``.
15
+
16
+ Data is split into data sets (:class:`~pyolivetree.DataSet`; plain strings also work). Each row looks like
17
+ ``{"id", "table_name", "table_version", "sequence_number", "deleted", "datum": {...}}``.
18
+ Every change gets a higher ``sequence_number``, so you can fetch only what's new.
19
+
20
+ Main tables in ``annotations``: ``annotations`` (notes, highlights, clips; text in
21
+ ``datum.content``), ``annotation_locations``, ``annotation_categories``, ``highlighters``.
22
+ """
23
+
24
+ def _q(self, **kw: Any) -> dict[str, Any]:
25
+ return {"device_id": self._c.device_id, "sync_client_protocol_version": SYNC_PROTOCOL, **kw}
26
+
27
+ def status(self, lock: bool = False, data_set: DataSet | str | None = None) -> dict:
28
+ """Server status and the latest sequence number per data set.
29
+
30
+ Args:
31
+ lock: Take the write lock (needed before :meth:`push`). Default leaves it alone.
32
+ data_set: Optional data set to scope the lock to.
33
+
34
+ Returns:
35
+ ``{"customer": {"user_id", "master_sequence_numbers": {...}}, "server": {...}}``.
36
+ """
37
+ q = self._q(
38
+ managed_data_set=_managed(data_set) if data_set else None,
39
+ do_not_lock=None if lock else "true",
40
+ reader_version=self._c.reader_version,
41
+ )
42
+ return self._c.request("GET", f"{SYNC}/syncables/status.json", params=q)
43
+
44
+ def page(self, data_set: DataSet | str = DataSet.ANNOTATIONS, above: int = 0) -> list[dict]:
45
+ """One page of rows changed after sequence number ``above``.
46
+
47
+ Args:
48
+ data_set: A :class:`~pyolivetree.DataSet` or its name, e.g. ``"library"``.
49
+ above: Return rows with a higher sequence number than this.
50
+ """
51
+ q = self._q(
52
+ platform_id=PLATFORM_ID,
53
+ above=above,
54
+ managed_data_set=_managed(data_set),
55
+ reader_version=self._c.reader_version,
56
+ )
57
+ return self._c.request("GET", f"{SYNC}/syncables.json", params=q).get("syncables") or []
58
+
59
+ def pull(self, data_set: DataSet | str = DataSet.ANNOTATIONS, above: int = 0) -> Iterator[dict]:
60
+ """Yield every row changed after ``above``, fetching pages until caught up.
61
+
62
+ Args:
63
+ data_set: Data set to read (:class:`~pyolivetree.DataSet` or name).
64
+ above: Start after this sequence number (0 = everything).
65
+
66
+ Yields:
67
+ Sync rows. Rows with ``deleted == 1`` were removed on another device.
68
+ """
69
+ latest = self.status()["customer"]["master_sequence_numbers"].get(data_set_name(data_set), 0)
70
+ while above < latest:
71
+ page = self.page(data_set, above)
72
+ if not page:
73
+ break
74
+ yield from page
75
+ top = max(int(i["sequence_number"]) for i in page)
76
+ if top <= above: # server didn't advance; stop instead of looping
77
+ break
78
+ above = top
79
+
80
+ def table_schema(self, data_set: DataSet | str, table: str, version: int = 0) -> dict:
81
+ """The server's SQLite schema for a synced table.
82
+
83
+ Returns:
84
+ ``{"managed_data_set", "table_name", "table_version", "sqlite3_schema"}``.
85
+ """
86
+ return self._c.request(
87
+ "GET",
88
+ f"{SYNC}/syncables/get_table_schema/{_managed(data_set)}/{table}/{version}.json",
89
+ params={"sync_client_protocol_version": SYNC_PROTOCOL},
90
+ )
91
+
92
+ def push(
93
+ self,
94
+ table: str,
95
+ datum: dict,
96
+ *,
97
+ data_set: DataSet | str = DataSet.ANNOTATIONS,
98
+ item_id: int = -1,
99
+ sequence_number: int = -1,
100
+ deleted: bool = False,
101
+ customer_id: int | None = None,
102
+ release_lock: bool = True,
103
+ ) -> dict:
104
+ """Create or update one row on the server. **Changes your real account.**
105
+
106
+ The expected flow is ``status(lock=True)``, then one ``push`` per row, with
107
+ ``release_lock=True`` on the last one.
108
+
109
+ Args:
110
+ table: Table name, e.g. ``"annotations"``.
111
+ datum: Row fields, shaped like ``datum`` from :meth:`pull` (``client_id`` = local id).
112
+ data_set: Data set the table belongs to.
113
+ item_id: Server row id; ``-1`` creates a new row.
114
+ sequence_number: Current sequence number of the row; ``-1`` for new rows.
115
+ deleted: Mark the row deleted.
116
+ customer_id: Your user id. Looked up via :meth:`status` if omitted.
117
+ release_lock: Release the write lock after this push.
118
+
119
+ Returns:
120
+ ``{"syncable": {...}}`` with the saved row, or ``{"exception": {...}}``.
121
+ """
122
+ if customer_id is None:
123
+ customer_id = self.status()["customer"]["user_id"]
124
+ body = {
125
+ "device_id": self._c.device_id,
126
+ "device_model": "pyolivetree",
127
+ "device_os": platform.release(),
128
+ "sync_client_protocol_version": SYNC_PROTOCOL,
129
+ "managed_data_set": _managed(data_set),
130
+ "table_name": table,
131
+ "deleted": int(deleted),
132
+ "id": item_id,
133
+ "customer_id": customer_id,
134
+ "sequence_number": sequence_number,
135
+ "datum": datum,
136
+ }
137
+ q = {"sync_client_protocol_version": SYNC_PROTOCOL, "release_lock": "true" if release_lock else None}
138
+ return self._c.request("POST", f"{SYNC}/syncables/syncitem.json", params=q, json=body)
139
+
140
+ def release_lock(self, data_set: DataSet | str = DataSet.ANNOTATIONS) -> Any:
141
+ """Release the write lock taken by ``status(lock=True)``."""
142
+ return self._c.request(
143
+ "PUT", f"{SYNC}/syncables/release_lock", params=self._q(managed_data_set=_managed(data_set))
144
+ )
145
+
146
+ def possibly_bad_annotation_data(self) -> Any:
147
+ """Annotations the server flagged as possibly corrupt.
148
+
149
+ The server currently rejects this request (HTTP 401 with a token, 410 Gone with Basic auth).
150
+ """
151
+ return self._c.request("GET", f"{SYNC}/syncables/get_possibly_bad_annotation_data.json")
152
+
153
+
154
+ def _managed(data_set: DataSet | str) -> str:
155
+ """Server form of a data set name: ``"managed_annotations"``."""
156
+ return f"managed_{data_set_name(data_set)}"
@@ -0,0 +1,63 @@
1
+ """Analytics, experiments, consent and session endpoints."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pyolivetree.constants import ANALYTICS, PRIVACY
8
+ from pyolivetree.resources._base import Resource
9
+
10
+
11
+ class Telemetry(Resource):
12
+ """``analytics.olivetree.com`` and ``privacy.olivetree.com``. Available as ``ot.telemetry``.
13
+
14
+ The POST/PUT methods send data to Olive Tree; wrapped for completeness.
15
+ """
16
+
17
+ def event_manifest(self, from_date: str | None = None, to_date: str | None = None) -> dict:
18
+ """Analytics event definitions, optionally for a date range (``YYYY-MM-DD``)."""
19
+ url = (
20
+ f"{ANALYTICS}/event_manifests/from/{from_date}/to/{to_date}"
21
+ if from_date
22
+ else f"{ANALYTICS}/event_manifest"
23
+ )
24
+ return self._c.request("GET", url, auth=False)
25
+
26
+ def get_variant(self, experiment_id: str, customer_id: int | None = None) -> Any:
27
+ """A/B experiment variant assigned to this device."""
28
+ return self._c.request(
29
+ "GET",
30
+ f"{ANALYTICS}/variant",
31
+ auth=False,
32
+ params={
33
+ "experiment_id": experiment_id,
34
+ "install_id": self._c.device_id,
35
+ "customer_id": customer_id,
36
+ },
37
+ )
38
+
39
+ def set_variant(self, **body: Any) -> Any:
40
+ """Set an experiment variant."""
41
+ return self._c.request("PUT", f"{ANALYTICS}/variant", json=body, auth=False)
42
+
43
+ def events(self, events: Any) -> Any:
44
+ """Send analytics events (signed)."""
45
+ return self._c.request("POST", f"{ANALYTICS}/events", json=events, sign="telemetry", auth=False)
46
+
47
+ def session_start(self, **body: Any) -> Any:
48
+ """Report an app session start (signed)."""
49
+ return self._c.request("POST", f"{PRIVACY}/session/start", json=body, sign="telemetry", auth=False)
50
+
51
+ def consent_receipt(self, **body: Any) -> Any:
52
+ """Record a consent receipt."""
53
+ return self._c.request("POST", f"{PRIVACY}/consent/receipt", json=body, auth=False)
54
+
55
+ def location_settings(self, **body: Any) -> dict:
56
+ """Read location-based consent settings; pass fields to change them."""
57
+ if body:
58
+ return self._c.request("POST", f"{PRIVACY}/consent/location/settings", json=body, auth=False)
59
+ return self._c.request("GET", f"{PRIVACY}/consent/location/settings", auth=False)
60
+
61
+ def subscription_log_usage(self, **body: Any) -> Any:
62
+ """Log subscription content usage."""
63
+ return self._c.request("POST", f"{PRIVACY}/subscription/log_usage", json=body, auth=False)
@@ -0,0 +1,61 @@
1
+ """Olive Tree web pages and client config files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pyolivetree.constants import HELP, WWW
8
+ from pyolivetree.resources._base import Resource
9
+
10
+
11
+ class Web(Resource):
12
+ """Available as ``ot.web``. Returns HTML (``str``) or JSON."""
13
+
14
+ def _get(self, url: str, **params: Any) -> Any:
15
+ return self._c.request("GET", url, params=params, auth=False)
16
+
17
+ def timeouts(self) -> dict:
18
+ """Recommended network timeouts."""
19
+ return self._get(f"{WWW}/static/app_assets/timeouts.json")
20
+
21
+ def appirater_config(self) -> dict:
22
+ """Rate-this-app prompt settings."""
23
+ return self._get(f"{WWW}/api/appirater_config.json")
24
+
25
+ def app_to_web_bridge(self, **params: Any) -> str:
26
+ """Open a store page in the browser, optionally logged in.
27
+
28
+ Args:
29
+ **params: ``platform``, ``ReaderVersion``, ``OEM``, ``clickId``, ``productid``,
30
+ ``login_handoff_token`` (from
31
+ :meth:`~pyolivetree.resources.account.Account.web_handoff_token`).
32
+ """
33
+ return self._get(f"{WWW}/store/br5/app_to_web_bridge.php", **params)
34
+
35
+ def product_page(self, product_id: int) -> str:
36
+ """Store web page for a product (HTML)."""
37
+ return self._get(f"{WWW}/store/product.php", productid=product_id)
38
+
39
+ def bible_study_packs(self) -> str:
40
+ """Bible study packs page (HTML)."""
41
+ return self._get(f"{WWW}/store/biblestudypacks.php")
42
+
43
+ def manage_subscriptions(self) -> str:
44
+ """Manage-subscriptions page (HTML)."""
45
+ return self._get(f"{WWW}/store/manage_subscriptions.php")
46
+
47
+ def eula(self) -> str:
48
+ """End-user license agreement (HTML)."""
49
+ return self._get(f"{WWW}/eula")
50
+
51
+ def privacy_policy(self) -> str:
52
+ """Privacy policy (HTML)."""
53
+ return self._get(f"{WWW}/privacy_policy")
54
+
55
+ def help_articles(self) -> str:
56
+ """Help-center section for the mobile app (HTML)."""
57
+ return self._get(f"{HELP}/hc/en-us/sections/360000034263-Android-OLIVE-TREE-BIBLE-APP-7-1")
58
+
59
+ def help_login(self) -> str:
60
+ """Help article on logging in (HTML)."""
61
+ return self._get(f"{HELP}/hc/en-us/articles/210270643-Get-Started-Logging-In")