Metadata-Version: 2.4
Name: quran-foundation-api
Version: 0.4.0
Summary: Official Python SDK for Quran Foundation APIs.
Project-URL: Homepage, https://api-docs.quran.foundation/docs/sdk/python/
Project-URL: Documentation, https://api-docs.quran.foundation/docs/sdk/python/
Project-URL: Support, https://api-docs.quran.foundation/request-access/
Author-email: Quran Foundation <developers@quran.foundation>
License-Expression: MIT
License-File: LICENSE
Keywords: api,oauth2,quran,quran-foundation,sdk
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx<1,>=0.27
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Requires-Dist: twine>=6.0; extra == 'dev'
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.115; extra == 'fastapi'
Requires-Dist: uvicorn>=0.30; extra == 'fastapi'
Description-Content-Type: text/markdown

# quran-foundation-api

Official Python SDK for Quran Foundation APIs.

```bash
pip install quran-foundation-api
```

```python
from quran_foundation import QuranClient

client = QuranClient(client_id="YOUR_CLIENT_ID", access_token="ACCESS_TOKEN")
chapters = client.list_chapters()
print(chapters)
```

## Content and search

```python
from quran_foundation import QuranClient

client = QuranClient(client_id="YOUR_CLIENT_ID", access_token="ACCESS_TOKEN")

chapter = client.get_chapter(1)
verses = client.get_verses_by_range("1:1", "1:7", translations="131")
translations = client.list_translations()
results = client.search("mercy", mode="advanced", params={"size": 10})
```

### Content resource sync

Chapter recitations, Mushafs, word-by-word translations, and word-by-word
transliterations can be included in content sync and fetched as resource snapshots:

```python
changes = client.sync_resources(
    params={
        "bootstrap": True,
        "resources": (
            "chapter_recitations:159;mushafs:1;word_by_word_translations:85;"
            "word_by_word_transliterations:60"
        ),
    }
)
chapter_recitation_snapshot = client.get_chapter_recitation_snapshot(159)
snapshot = client.get_word_by_word_translation_snapshot(85)
transliteration_snapshot = client.get_word_by_word_transliteration_snapshot(60)
mushaf_snapshot = client.get_mushaf_snapshot(1)
```

Chapter-recitation snapshots use the audio recitation ID and contain its chapter
audio files. Mushaf snapshots contain the layout metadata, pages, publicly
distributable font assets, and words needed for offline use. The API also exposes
approved, shareable word-translation and word-transliteration resources. A
word-by-word transliteration snapshot record is available as the exported
`WordByWordTransliterationSnapshotRecord` type and contains `id`,
`resource_content_id`, `resource_id`, `word_id`, `language_id`, `language_name`,
`text`, and `updated_at`. Incremental row events for these resources use
`record_type="word_transliteration"`.

For endpoints that do not yet have a dedicated helper, use the service request helpers:

```python
client.content_request("/verses/by_page/1", params={"translations": "131"})
client.search_request("/api/v1/search", params={"query": "mercy", "mode": "quick"})
client.user_request("/bookmarks", params={"page": 1})
```

## Analytics Events

Analytics submission is server-side and requires a client-credentials access
token with the `analytics.events.write` scope. Keep the client secret and token
in server-side environment variables.

```python
import os
from datetime import datetime, timezone

from quran_foundation import AnalyticsEvent, QuranClient
from quran_foundation.oauth import client_credentials

token = client_credentials(
    client_id=os.environ["QURAN_CLIENT_ID"],
    client_secret=os.environ["QURAN_CLIENT_SECRET"],
    scope="analytics.events.write",
)
client = QuranClient(
    client_id=os.environ["QURAN_CLIENT_ID"],
    access_token=token["access_token"],
)

result = client.submit_analytics_events(
    [
        AnalyticsEvent(
            event_id="stable-event-id-1",
            name="quran.reader.verse_viewed",
            version=1,
            occurred_at=datetime.now(timezone.utc),
            user_id="QURAN_FOUNDATION_USER_ID",
            session_id="session-123",
            properties={"verse_key": "2:255", "surface": "reader"},
        ),
        AnalyticsEvent(
            event_id="stable-event-id-2",
            name="quran.app.started",
            version=1,
            occurred_at=datetime.now(timezone.utc),
            anonymous_id="anonymous-123",
        ),
    ]
)
```

A successful response accepts the complete batch. Retry a failed batch with
the same event IDs so downstream processing can identify duplicates. Use a
Quran Foundation OAuth user ID for `user_id`; omit it for guests or unknown
users.

## User APIs

Use signed-in User API helpers with a user access token. Keep the token server-side.

```python
profile = client.get_profile()
bookmarks = client.list_bookmarks()
client.create_bookmark({"verse_key": "2:255", "mushaf_id": 1})
client.update_preference({"key": "theme", "value": "dark"})
```

### App State

App State stores app-owned JSON documents for signed-in users. First read the
enabled data groups, then use a high-entropy idempotency key for every write.
The response ETag is opaque and quoted; store it unchanged for conditional
writes.

```python
config = client.get_app_state_configuration()

created = client.put_app_state_document(
    "settings",
    "theme",
    value={"mode": "dark"},
    schema_version=1,
    idempotency_key="01HZX4EXAMPLE8J4K7M2PQ9RST",
    if_none_match="*",
)

current = client.get_app_state_document("settings", "theme")
client.put_app_state_document(
    "settings",
    "theme",
    value={"mode": "light"},
    schema_version=1,
    idempotency_key="01HZX4EXAMPLE9Y5M8N3QR0STU",
    if_match=current.etag,
)
```

`if_match` and `if_none_match` are mutually exclusive; the client rejects calls
that provide both before sending an HTTP request. API failures raise
`QuranHttpError` with the existing `status_code` and parsed `payload` attributes,
plus copied read-only `headers` and the original `response_text`. Empty and
non-JSON error bodies therefore remain available for diagnostics without making
the response headers caller-mutable.

For offline startup, page through `bootstrap_app_state()` until `hasMore` is
false, then persist its `nextSyncToken`. Apply every page from
`get_app_state_changes()` and its next token atomically. If a token expires and
the API returns HTTP 410, preserve pending local writes, bootstrap again, drain
changes, replay pending writes with their original idempotency keys, then pull
once more.

For an application-managed offline workflow, use the synchronous transactional
reconciler with an explicit account identifier. `QuranClient` structurally
implements `AppStateTransport`; the included memory store is useful for
short-lived processes, while durable applications can implement `AppStateStore`
with the same atomic reducer contract.

```python
from quran_foundation import AppStateReconciler, MemoryAppStateStore

app_state = AppStateReconciler(
    client,
    MemoryAppStateStore(),
    account_id="your-stable-account-id",
)

queued = app_state.put_document(
    "settings",
    "theme",
    value={"mode": "dark"},
    schema_version=1,
)
assert queued.visible["settings/theme"].pending is True

current = app_state.reconcile()  # pull, replay the captured queue, then pull
other_account = app_state.switch_account(
    "another-stable-account-id",
    next_account_client,
)
```

Account switching replaces the local account boundary and transport atomically. Supply a separate
`QuranClient` whose access token belongs to the target account, and do not mutate one shared
client's token across accounts while reconciliation is in flight. An in-flight request retains the
client captured for its original account, and its late result cannot commit after the generation
changes.

Views, queued JSON bodies, and mutation snapshots are deeply immutable. The
reconciler stages bootstrap pages until the change drain completes, preserves
complete-replacement intent across bounded HTTP 412 rebases, recovers bounded
HTTP 410 token failures, and prevents late responses from a previous account
generation from committing. Account identity is always supplied by the caller;
the reconciler never parses access tokens.

## OAuth2 helpers

Use OAuth helpers on the server side. Never expose `client_secret`, access tokens, or refresh tokens to browser code.

```python
from quran_foundation.oauth import build_authorization_url, create_pkce_pair, exchange_code

verifier, challenge = create_pkce_pair()
authorize_url = build_authorization_url(
    client_id="YOUR_CLIENT_ID",
    redirect_uri="https://your-app.com/callback",
    scope="openid offline_access user bookmark",
    state="random-state",
    code_challenge=challenge,
)

tokens = exchange_code(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
    code="CODE_FROM_CALLBACK",
    code_verifier=verifier,
    redirect_uri="https://your-app.com/callback",
)
```

## Development

```bash
python -m pip install -e ".[dev]"
ruff check .
pytest
python -m build
twine check dist/*
```

## Contributing and releases

Public API changes require tests, documentation, a changelog entry, and a
version bump. Merging to `main` runs CI but does not publish the package. See
[`CONTRIBUTING.md`](CONTRIBUTING.md) for the versioning and release process.
