Metadata-Version: 2.5
Name: kayya-api
Version: 0.1.0
Summary: Integrasi server tenant dengan marketplace Kayya API: verifikasi panggilan gateway, webhook, dan sinkronisasi entitlement.
Project-URL: Dokumentasi, https://gitlab.com/sdk-marketplace-ui/sdk/-/tree/main/python
Project-URL: Repositori, https://gitlab.com/sdk-marketplace-ui/sdk
Project-URL: Keamanan, https://gitlab.com/sdk-marketplace-ui/sdk/-/blob/main/SECURITY.md
Author: Kayya API
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: api-marketplace,django,fastapi,flask,hmac,kayya,webhook
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Django
Classifier: Framework :: FastAPI
Classifier: Framework :: Flask
Classifier: Intended Audience :: Developers
Classifier: Natural Language :: Indonesian
Classifier: Operating System :: OS Independent
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 :: Internet :: WWW/HTTP :: WSGI :: Middleware
Classifier: Topic :: Security
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: dev
Requires-Dist: django>=4.2; extra == 'dev'
Requires-Dist: fastapi>=0.100; extra == 'dev'
Requires-Dist: flask>=2.2; extra == 'dev'
Requires-Dist: gunicorn>=21; extra == 'dev'
Requires-Dist: httpx>=0.24; extra == 'dev'
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Requires-Dist: sqlalchemy>=1.4; extra == 'dev'
Requires-Dist: starlette>=0.27; extra == 'dev'
Requires-Dist: uvicorn>=0.22; extra == 'dev'
Provides-Extra: django
Requires-Dist: django>=4.2; extra == 'django'
Provides-Extra: fastapi
Requires-Dist: starlette>=0.27; extra == 'fastapi'
Provides-Extra: flask
Requires-Dist: flask>=2.2; extra == 'flask'
Provides-Extra: sqlalchemy
Requires-Dist: sqlalchemy>=1.4; extra == 'sqlalchemy'
Description-Content-Type: text/markdown

# kayya-api — SDK Python

Menghubungkan server API kamu dengan marketplace **Kayya API** (kontrak
integrasi tenant v2). Python 3.9+, inti tanpa dependency.

Yang **wajib** hanya satu: memastikan setiap panggilan benar datang dari gateway
Kayya API. Tanpa itu, base URL kamu adalah API terbuka bagi siapa pun yang
menemukannya. Sisanya opsional:

| Fungsi | Wajib? | Untuk |
|---|---|---|
| Verifikasi panggilan gateway | **Ya** | Menolak panggilan yang tidak lewat gateway (`401`), menyediakan identitas buyer |
| Penerima webhook | Tidak | Diberi tahu saat entitlement dibeli, diperpanjang, atau dicabut |
| Penyimpan lokal | Tidak | Daftar pelanggan di database kamu sendiri |
| Sinkronisasi | Tidak | Jaring pengaman webhook; membangun ulang data lokal dari nol |
| `kayya-api doctor` | — | Membuktikan integrasi siap sebelum go-live |

Panduan lengkap kontraknya ada di halaman **Integrasi untuk developer** di situs
Kayya API (`/developer/integrasi`). SDK ini tidak wajib — yang diuji marketplace
adalah perilaku server kamu — tetapi ia menjadikan jalur yang aman sekaligus
jalur yang paling mudah.

## Instalasi

```bash
pip install kayya-api                # inti: ASGI, WSGI, webhook, sinkronisasi, doctor
pip install "kayya-api[fastapi]"     # + dependency FastAPI/Starlette (Depends)
pip install "kayya-api[flask]"
pip install "kayya-api[django]"
pip install "kayya-api[sqlalchemy]"  # penyimpan lokal lewat SQLAlchemy
```

## Konfigurasi

| Variabel lingkungan | Isi | Dibutuhkan untuk |
|---|---|---|
| `KAYYA_API_SECRET` | Secret integrasi — dashboard tenant → **Integrasi**. Pakai apa adanya, termasuk awalan `whsec_` | Semua |
| `KAYYA_API_TENANT_ID` | Tenant ID dari halaman yang sama | Sinkronisasi |
| `KAYYA_API_BASE_URL` | Alamat API marketplace yang diberikan saat onboarding (dengan atau tanpa `/api/v1`) | Sinkronisasi |

Setiap nilai juga bisa diberikan sebagai argumen, yang selalu menang atas
variabel lingkungan. Secret yang tidak diisi membuat aplikasi **gagal menyala**
(`KonfigurasiSalah`) — lebih aman daripada menyala dan menolak setiap buyer.

## Mulai cepat

Middleware dipasang **di depan seluruh route**, bukan per route. Uji integrasi
marketplace mengetuk satu path acak dengan tanda tangan palsu dan menuntut
`401`; verifikasi yang hanya dipasang di sebagian route gagal di sana.

### FastAPI / Starlette

```python
from fastapi import Depends, FastAPI
from kayya_api.fastapi import Pemanggil, pasang, pemanggil

app = FastAPI()
pasang(app)                               # secret dari KAYYA_API_SECRET

@app.get("/v1/cuaca")
def cuaca(p: Pemanggil = Depends(pemanggil)):
    return {"untuk": p.entitlement_id}
```

`pasang` memeriksa konfigurasi saat itu juga. `app.add_middleware(KayyaMiddleware)`
juga bekerja, tetapi Starlette baru membangun middleware saat request pertama —
secret yang lupa diisi baru ketahuan sebagai `500` di setiap request.

### Flask

```python
from flask import Flask
from kayya_api.flask import pasang, pemanggil

app = Flask(__name__)
pasang(app)

@app.get("/v1/cuaca")
def cuaca():
    return {"untuk": pemanggil().entitlement_id}
```

### Django

```python
# settings.py
MIDDLEWARE = [
    "kayya_api.django.middleware.KayyaMiddleware",   # paling atas
    # ...
]
KAYYA_API = {}   # opsional — lihat "Opsi"

# views.py
from kayya_api.django.middleware import pemanggil

def cuaca(request):
    return JsonResponse({"untuk": pemanggil(request).entitlement_id})
```

Sync maupun async (ASGI) didukung. Letakkan paling atas: sebelum
`CommonMiddleware` (pengalihan `APPEND_SLASH` untuk panggilan palsu harus tetap
`401`) dan sebelum `CsrfViewMiddleware`. View API yang menerima `POST` dari
gateway perlu `csrf_exempt` seperti API Django lainnya.

### ASGI & WSGI lain

```python
from kayya_api.asgi import KayyaMiddleware   # Quart, Litestar, ASGI murni
app = KayyaMiddleware(app)

from kayya_api.wsgi import KayyaMiddleware   # Bottle, Pyramid, Falcon, WSGI murni
application = KayyaMiddleware(application)
```

Identitas pemanggil ada di `scope["kayya.pemanggil"]` (ASGI) atau
`environ["kayya.pemanggil"]` (WSGI).

## Identitas pemanggil

| Atribut | Isi |
|---|---|
| `buyer_id` | UUID buyer di marketplace |
| `entitlement_id` | UUID entitlement — kunci yang sama dengan webhook & sinkronisasi |
| `produk` | Slug produk yang dipanggil |
| `request_id` | Sertakan saat menghubungi dukungan — ditelusuri di sisi marketplace juga |
| `expires_at` | `datetime` akhir periode berjalan — **informasi saja** |
| `uji_integrasi` | `True` untuk panggilan uji integrasi marketplace — jangan dicatat sebagai pemakaian |

**Jangan menolak panggilan berdasarkan `expires_at`.** Keputusan akses sepenuhnya
milik gateway: ada keadaan di mana akses tetap sah meski tanggalnya lewat, dan
panggilan yang sampai ke server kamu sudah diputuskan sah.

Kamu tidak pernah menerima API key buyer, dan tidak membutuhkannya.

## Opsi middleware

Sama di semua framework (Django: kunci huruf besar di `settings.KAYYA_API`).

| Opsi | Bawaan | Isi |
|---|---|---|
| `secret` | `KAYYA_API_SECRET` | Secret integrasi |
| `prefix_yang_dibuang` | `""` | Awalan path yang dibuang reverse proxy di depan server kamu (proxy menerima `/v1/cuaca`, meneruskan `/cuaca` → isi `"/v1"`) |
| `kecualikan` | `()` | Path yang **tidak** diverifikasi, dibandingkan persis — mis. `["/healthz"]` untuk probe Kubernetes. Path di sini terbuka untuk siapa pun; jangan pernah memasukkan route API |
| `periksa_path` | `True` | Matikan pencocokan path hanya kalau proxy menulis ulang path dengan cara yang tidak bisa dijelaskan satu awalan |
| `webhook` | `None` | `PenerimaWebhook` — menerima webhook di `path_webhook` (tidak diverifikasi sebagai panggilan gateway) |
| `path_webhook` | `"/kayya/webhook"` | |

Django menambah `"WEBHOOK": True` (penerima dibuat dari settings),
`"SAAT_PERUBAHAN"` (callable atau dotted path), dan `"SIMPAN_ENTITLEMENT"`
(bawaan `True` kalau `kayya_api.django` terpasang).

### Jawaban yang ditolak

`401` dengan body JSON:

```json
{"alasan": "timestamp", "keterangan": "X-Marketplace-Timestamp 412 detik di belakang jam server ini ..."}
```

| `alasan` | Penyebab paling mungkin |
|---|---|
| `header_kurang` | Bukan panggilan gateway (pemindai internet, health check) |
| `timestamp` | Jam server kamu meleset lebih dari 5 menit — nyalakan NTP |
| `tanda_tangan` | Secret berbeda: awalan `whsec_` terpotong, atau secret baru belum dipasang setelah diganti |
| `path` | Reverse proxy membuang awalan path — isi `prefix_yang_dibuang` |

Dashboard tenant membaca `alasan` untuk menampilkan penyebab saat uji integrasi
gagal. Akar base URL tanpa header apa pun dijawab `401` — bukan `5xx`, yang
dianggap health check marketplace sebagai server sakit.

Penolakan dicatat di logger `kayya_api` (`WARNING`; `header_kurang` di `DEBUG`
supaya pemindai internet tidak memenuhi log).

## Webhook

```python
from sqlalchemy import create_engine
from kayya_api import PenerimaWebhook, Perubahan
from kayya_api.penyimpan.sqlalchemy import PenyimpanSQLAlchemy

penyimpan = PenyimpanSQLAlchemy(create_engine(DATABASE_URL))

def saat_perubahan(p: Perubahan) -> None:
    if p.keputusan == "baru":
        kirim_email_sambutan.delay(p.entitlement.buyer_id)   # antrikan pekerjaan berat

pasang(app, webhook=PenerimaWebhook(penyimpan=penyimpan, saat_perubahan=saat_perubahan))
```

Isi URL webhook di dashboard tenant → Integrasi dengan `https://<server kamu>/kayya/webhook`.

- Tanda tangan dihitung dari **raw body**, sebelum body-parser framework bekerja —
  middleware menerimanya di luar framework.
- `ping` dan event yang belum dikenal versi SDK ini dijawab `2xx`.
- Dengan `penyimpan`, `entitlement.updated` diterapkan dengan aturan `updated_at`
  dan `saat_perubahan` hanya dipanggil kalau data lokal **berubah** —
  `p.keputusan` salah satu `baru`, `perpanjangan`, `dicabut`, `diperbarui`.
  Duplikat dan retry lama tidak memanggilnya lagi.
- Tanpa penyimpan, `saat_perubahan` dipanggil untuk setiap kiriman sah
  (`p.keputusan` = `None`), dan urutannya urusanmu.
- `saat_perubahan` berjalan **setelah** data tersimpan, paling banyak sekali per
  perubahan. Galat di dalamnya dicatat, tidak menggagalkan kiriman: jawaban
  `5xx` membuat marketplace mengulang, dan enam kegagalan berturut
  menonaktifkan webhook kamu. Pekerjaan yang tidak boleh hilang: antrikan ke
  antrean yang tahan lama (Celery, RQ) dari dalam callback.
- Marketplace menunggu paling lama 10 detik.

Webhook adalah **pemberitahuan**, bukan penjaga akses: entitlement yang dicabut
sudah ditolak gateway detik itu juga.

## Penyimpan lokal

| Adapter | Pemasangan |
|---|---|
| `kayya_api.penyimpan.PenyimpanMemori` | Test & percobaan — hilang saat restart |
| `kayya_api.penyimpan.sqlalchemy.PenyimpanSQLAlchemy(engine)` | SQLAlchemy 1.4/2.x; `buat_tabel=True`, atau sertakan `metadata` di Alembic |
| `kayya_api.django.penyimpan.PenyimpanDjango()` | `"kayya_api.django"` di `INSTALLED_APPS`, lalu `manage.py migrate` |

Kedua adapter database memakai tabel yang sama — `kayya_entitlement` (kunci
`entitlement_id`) dan `kayya_penanda_sync` — dengan waktu UTC berpresisi
mikrodetik (`DATETIME(6)` di MySQL/MariaDB). Satu aturan berlaku untuk webhook
maupun sinkronisasi: per `entitlement_id`, hanya kalau `updated_at` yang datang
lebih baru. Perpanjangan memajukan `expires_at`; retry lama yang tiba terlambat
tidak pernah menimpa data baru, juga saat webhook dan sinkronisasi menulis baris
yang sama bersamaan.

`Entitlement.masih_berlaku()` menghitung dari `status` + `expires_at` data lokal
— untuk ditampilkan, **bukan** untuk memutuskan akses. `limit_hit` disimpan tapi
tidak ditegakkan; kuota ditegakkan gateway.

Adapter sendiri cukup mengimplementasikan lima metode `kayya_api.penyimpan.Penyimpan`.

## Sinkronisasi

```python
from kayya_api import PenjadwalSync, Penyinkron

penyinkron = Penyinkron(penyimpan, saat_perubahan=saat_perubahan)
PenjadwalSync(penyinkron).mulai()     # sekali sekarang, lalu tiap jam, di thread daemon
```

FastAPI (lifespan):

```python
@asynccontextmanager
async def lifespan(app):
    task = asyncio.create_task(PenjadwalSync(Penyinkron(penyimpan)).jalankan_async())
    yield
    task.cancel()
```

Django:

```bash
python manage.py kayya_sinkron            # satu putaran
python manage.py kayya_sinkron --terus    # proses tersendiri: sekarang, lalu tiap jam
```

Celery beat:

```python
from celery import shared_task
from kayya_api.django.penyimpan import penyinkron

@shared_task
def sinkron_kayya():
    penyinkron().jalankan_sekali()
```

- Halaman pertama `?since=`, berikutnya `?kursor=` apa adanya; penanda (`sampai`)
  disimpan **hanya setelah halaman terakhir**. Putaran yang gagal di tengah
  diulang dari penanda lama.
- Kegagalan (jaringan, `401`, `5xx`) dicatat lalu diulang bertahap 1, 5, 15, 30
  menit — tidak pernah menjatuhkan service kamu, dan buyer tetap dilayani tanpa
  data lokal sama sekali.
- Jalankan penjadwal di **satu** proses. Beberapa worker (gunicorn, uvicorn
  `--workers`) yang masing-masing memulainya menarik data yang sama berkali-kali.
- Marketplace membatasi **60 permintaan per menit per tenant**. Lewat dari itu
  dijawab `429` (`GalatSync.status == 429`); penjadwal mundur 1 menit lalu
  mengulang putaran dari penanda lama.
- `KlienSync` tersedia untuk menarik halaman sendiri; `tarik_async` untuk kode async.

## `kayya-api doctor`

```bash
kayya-api doctor                                           # konfigurasi, test vector, sinkronisasi, jam
kayya-api doctor --url https://api.contoh.id --path /v1/ping
kayya-api doctor --webhook https://api.contoh.id/kayya/webhook
```

Memeriksa konfigurasi, menjalankan seluruh test vector resmi terhadap SDK yang
terpasang, mengirim satu permintaan sinkronisasi (membuktikan tenant ID &
secret), menghitung selisih jam dengan marketplace, lalu — kalau `--url`
diberikan — mengetuk server kamu seperti uji integrasi: tanda tangan sah harus
dilayani, tanda tangan palsu ke akar dan ke path acak harus `401`. Keluar
dengan status `1` kalau ada yang gagal. Secret tidak pernah dicetak.

## Menguji aplikasimu

`kayya_api.uji` membuat panggilan gateway dan kiriman webhook bertanda tangan
dengan secret uji, tanpa marketplace:

```python
from kayya_api.uji import header_gateway, kiriman_webhook

def test_cuaca(client):
    r = client.get("/v1/cuaca", headers=header_gateway("/v1/cuaca", secret="whsec_uji"))
    assert r.status_code == 200

def test_palsu(client):
    r = client.get("/v1/cuaca", headers=header_gateway("/v1/cuaca", secret="whsec_lain"))
    assert r.status_code == 401
```

## Kompatibilitas

| | |
|---|---|
| Python | 3.9 – 3.13 |
| Kontrak integrasi | v2 |
| FastAPI / Starlette | Starlette ≥ 0.27 |
| Flask | ≥ 2.2 |
| Django | ≥ 4.2, sync & async |
| SQLAlchemy | 1.4 & 2.x; diuji di SQLite, PostgreSQL, MySQL |

Setiap rilis diuji terhadap [test vector](https://gitlab.com/sdk-marketplace-ui/sdk/-/tree/main/test-vectors) resmi, dan aplikasi
[contoh](https://gitlab.com/sdk-marketplace-ui/sdk/-/tree/main/python/contoh) diuji di uvicorn & gunicorn sungguhan terhadap simulator
marketplace. Versi mengikuti semantic versioning; perubahan kontrak dicatat di
[CHANGELOG](https://gitlab.com/sdk-marketplace-ui/sdk/-/blob/main/python/CHANGELOG.md) dan di changelog halaman dokumentasi.

Menemukan celah keamanan? Lihat [SECURITY.md](https://gitlab.com/sdk-marketplace-ui/sdk/-/blob/main/SECURITY.md) — jangan lewat issue publik.
