Exportálás adattárházba
Az elemzők gyakran a cég többi adata mellett szeretnék látni a Sursa adatait is: mely kártyák ellenőrzöttek és ki ellenőrizte őket, hogyan gyarapodik a tudás csapatonként, mely kérdések maradnak megválaszolatlanul. Ez az útmutató egy ütemezett feladatot épít, amely a Sursa adatait az adattárházadba másolja, és naprakészen tartja a másolatot.
A feladat az API három részét kombinálja:
| Rész | Mit ad |
|---|---|
A kártyák listája az updated_since paraméterrel, valamint a teljes egészükben beolvasott rövid listák | Az előző futás óta megváltozott objektumok aktuális állapotát. |
| Az eseménycsatorna | A törléseket és a történtek előzményeit, 30 napra visszamenőleg. |
| Kurzorok | Hiánytalan oldalakat, kihagyás és ismétlés nélkül. |
Olyan kulcsra van szükséged, amely cards:read és users:read hatókörrel, valamint az eseménycsatornához
webhooks:manage hatókörrel rendelkezik. Adj a feladatnak saját, admin szerepkörű kulcsot: a felhasználók teljes
listája és az eseménycsatorna csak neki érhető el. A szűkebb szerepkörű és adatprofilú kulcs csak azt exportálja, amit
egy bizonyos csoport láthat; a felhasználók rövid listáját olvassa (brief=true), eseménycsatornát nem, ezért a
törléseket a lent leírt heti teljes betöltésből veszi át.
Táblák
Egyszerű elrendezés objektumonként egy táblával és egy táblával az eseményeknek:
| Tábla | Kulcs | Forrása |
|---|---|---|
sursa_cards | id | GET /v1/cards |
sursa_collections | id | GET /v1/collections |
sursa_users | id | GET /v1/users |
sursa_groups | id | GET /v1/groups |
sursa_events | id | GET /v1/webhook-events |
Minden objektumot úgy tárolj, ahogy megérkezik, JSON-oszlopban, továbbá a gyakran lekérdezett oszlopokban
(updated_at, collection_id, status, owner_id). Az API későbbi verzióiban új mezők jelennek meg; a
JSON-oszloppal ezeket a feladat módosítása nélkül is megkapod.
Első betöltés
Olvass be minden listát teljesen, updated_since nélkül, és írj ki minden objektumot. Jegyezd meg, mikor indult
a betöltés: a következő futás innen folytatja.
Növekményes betöltések
Minden futáskor, például 15 percenként:
- Jegyezd meg ennek a futásnak a kezdési idejét.
- A kártyákat olvasd be úgy, hogy az
updated_sinceaz előző futás kezdési ideje legyen, egy perc ráhagyással, és minden következő oldalt a kurzorral és ugyanazzal azupdated_sinceértékkel kérj le (az első oldal szűrői nélküli kurzorra a válasz400 api.cursor_invalid). A gyűjteményeket, a felhasználókat és a csoportokat teljes egészükben olvasd be: a gyűjteményeknek és a felhasználóknak nincsupdated_atmezőjük, a csoportupdated_atmezője pedig nem változik, amikor a tagjai változnak. Minden objektumot azidszerint hozz létre vagy cserélj le. - Olvasd be az eseménycsatornát a legutóbb elmentett kurzortól. Minden eseményt fűzz hozzá a
sursa_eventstáblához, és vezesd át a törléseket: acard.deletedesemény eltávolítja a kártyát asursa_cardstáblából, vagy ha megőrzöd az előzményeket, töröltként jelöli meg. - Mentsd el a kezdési időt és a csatorna új kurzorát, ha az adattárházad támogatja, ugyanabban a tranzakcióban, mint az adatokat.
import json
from datetime import datetime, timedelta, timezone
LISTS = {"cards": "/v1/cards", "collections": "/v1/collections", "users": "/v1/users", "groups": "/v1/groups"}
def read_list(path, since):
"""Every object of a list, changed since `since` (or all of them). The next page keeps the filters of the first."""
params = {"limit": 200}
if since:
params["updated_since"] = (since - timedelta(minutes=1)).isoformat()
while params:
response = session.get(BASE + path, params=params, timeout=120)
response.raise_for_status()
body = response.json()
items = body["data"] if isinstance(body, dict) and "data" in body else body
yield from items
if isinstance(body, dict) and "data" in body:
params = {**params, "cursor": body["next_cursor"]} if body.get("has_more") else None
else:
cursor = response.headers.get("X-Next-Cursor")
params = {**params, "cursor": cursor} if cursor else None
def read_events(cursor):
"""New events of the feed after `cursor` (or from the start of the 30 days), and the cursor to save."""
events = []
params = {"limit": 200, **({"cursor": cursor} if cursor else {})}
while True:
page = api("GET", "/v1/webhook-events?" + requests.compat.urlencode(params))
events += page["data"]
params["cursor"] = page["next_cursor"]
if not page["has_more"]:
return events, page["next_cursor"]
def run(state, warehouse):
started_at = datetime.now(timezone.utc)
since = state.get("since")
for table, path in LISTS.items():
for obj in read_list(path, since if table == "cards" else None):
warehouse.upsert(f"sursa_{table}", obj["id"], json.dumps(obj))
events, feed_cursor = read_events(state.get("feed_cursor"))
for event in events:
warehouse.insert_once("sursa_events", event["id"], json.dumps(event))
if event["type"] == "card.deleted":
warehouse.delete("sursa_cards", event["object"]["id"])
warehouse.save_state({"since": started_at, "feed_cursor": feed_cursor})A csatorna kurzora minden oldalon megérkezik, az utolsón is. Ha elmented, a következő futás pontosan az utolsó eltárolt esemény után kezd, így egyetlen eseményt sem olvasol be kétszer, és egyet sem hagysz ki.
A másolat pontosságának megőrzése
- Több mint 30 nap futás nélkül. A csatorna 30 napig őrzi az eseményeket. Ha a feladat ennél régebben nem futott, futtass újra teljes betöltést, és távolíts el a tábláidból minden olyan objektumot, amelyet a teljes betöltés nem adott vissza.
- Heti teljes betöltés. A csatorna hibátlan működése mellett is olcsó ellenőrzés, hogy a másolat teljes-e, ha hetente egyszer teljes betöltést futtatsz, és összeveted a tábláiddal.
- Amit a kulcs nem lát, az nem kerül exportálásra. A feladat pontosan azt látja, amit a kulcsa. Ha egy kártya kikerül a kulcs hozzáféréséből, például mert egy korlátozott gyűjteménybe került, a teljes betöltés nem adja vissza; kezeld eltávolítottként.
- Kérdések. A kérdésnaplónak saját exportja van: a
GET /v1/questions/export.csvegy időszak kérdéseit egyetlen CSV-fájlban adja vissza, az értékelésekkel és a forrásokkal együtt. Ehhez azaskhatókör szükséges.
A korlátok betartása
Egy nagy szervezet teljes betöltése sok kérést igényel. Egyszerre egy listát olvass be limit=200 értékkel, és
figyeld a RateLimit fejléceket. Egy 15 percenként futó, csak a változásokat olvasó
feladat messze minden kvóta alatt marad.