ข้ามไปยังเนื้อหา
Sursa Developers

ส่งออกไปยังคลังข้อมูล

นักวิเคราะห์มักต้องการข้อมูลของ Sursa ไว้คู่กับข้อมูลอื่นของบริษัท เช่น การ์ดใดผ่านการตรวจสอบแล้วและใครเป็นผู้ตรวจสอบ ความรู้เพิ่มขึ้นอย่างไรในแต่ละทีม และคำถามใดยังไม่ได้รับคำตอบ คู่มือนี้สร้างงานตามกำหนดเวลาที่คัดลอกข้อมูลของ Sursa ไปยังคลังข้อมูล (data warehouse) ของคุณและดูแลให้สำเนาเป็นปัจจุบัน

งานนี้รวมสามส่วนของ API เข้าด้วยกัน:

ส่วนสิ่งที่ได้
รายการการ์ดที่ใช้ updated_since และรายการสั้นที่อ่านทั้งรายการสถานะปัจจุบันของออบเจ็กต์ที่เปลี่ยนแปลงตั้งแต่การรันครั้งล่าสุด
ฟีดอีเวนต์ข้อมูลการลบและประวัติของสิ่งที่เกิดขึ้นย้อนหลัง 30 วัน
เคอร์เซอร์หน้าที่ครบถ้วนโดยไม่มีช่องว่างหรือรายการซ้ำ

คุณต้องมีคีย์ที่มีขอบเขตสิทธิ์ cards:read และ users:read รวมถึง webhooks:manage สำหรับฟีดอีเวนต์ ให้งานนี้มีคีย์ของตัวเองที่มีบทบาท admin เพราะรายการผู้ใช้แบบเต็มและฟีดอีเวนต์เปิดให้เฉพาะบทบาทนี้ คีย์ที่มีบทบาทและโปรไฟล์ข้อมูลที่แคบกว่าจะส่งออกเฉพาะสิ่งที่กลุ่มใดกลุ่มหนึ่งมองเห็นได้ คีย์แบบนี้อ่านรายการผู้ใช้แบบย่อ (brief=true) และอ่านฟีดอีเวนต์ไม่ได้ จึงต้องรับข้อมูลการลบจากการโหลดแบบเต็มทุกสัปดาห์ที่อธิบายไว้ด้านล่าง

ตาราง

โครงสร้างแบบง่ายที่มีหนึ่งตารางต่อออบเจ็กต์และหนึ่งตารางสำหรับอีเวนต์:

ตารางคีย์ข้อมูลมาจาก
sursa_cardsidGET /v1/cards
sursa_collectionsidGET /v1/collections
sursa_usersidGET /v1/users
sursa_groupsidGET /v1/groups
sursa_eventsidGET /v1/webhook-events

เก็บแต่ละออบเจ็กต์ตามที่ได้รับไว้ในคอลัมน์ JSON รวมถึงคอลัมน์ที่คุณคิวรีบ่อย (updated_at collection_id status และ owner_id) ฟิลด์ใหม่จะปรากฏใน API เวอร์ชันถัดไป คอลัมน์ JSON ทำให้คุณได้ฟิลด์เหล่านั้นโดยไม่ต้องแก้งาน

การโหลดครั้งแรก

อ่านทุกรายการให้ครบโดยไม่ใช้ updated_since และเขียนทุกออบเจ็กต์ จดเวลาที่การโหลด เริ่มต้น ไว้ การรันครั้งถัดไปจะทำต่อจากจุดนั้น

การโหลดส่วนเพิ่ม

ในทุกการรัน เช่น ทุก 15 นาที:

  1. จดเวลาเริ่มต้นของการรันครั้งนี้
  2. อ่านการ์ดโดยตั้ง updated_since เป็นเวลาเริ่มต้นของการรันครั้งก่อนหักเวลาเผื่อ 1 นาที และขอแต่ละหน้าถัดไปด้วยเคอร์เซอร์และ updated_since ค่าเดิม (เคอร์เซอร์ที่ไม่มีตัวกรองของหน้าแรกจะได้การตอบกลับ 400 api.cursor_invalid) อ่านคอลเลกชัน ผู้ใช้ และกลุ่มทั้งรายการ เพราะคอลเลกชันและผู้ใช้ไม่มี updated_at และ updated_at ของกลุ่มไม่เปลี่ยนเมื่อสมาชิกของกลุ่มเปลี่ยน แล้วสร้างหรือแทนที่แต่ละออบเจ็กต์ตาม id
  3. อ่านฟีดอีเวนต์จากเคอร์เซอร์ที่บันทึกไว้ครั้งก่อน เพิ่มแต่ละอีเวนต์ต่อท้าย sursa_events และดำเนินการลบตามอีเวนต์ อีเวนต์ card.deleted จะนำการ์ดออกจาก sursa_cards หรือทำเครื่องหมายว่าลบแล้วหากคุณเก็บประวัติไว้
  4. บันทึกเวลาเริ่มต้นและเคอร์เซอร์ใหม่ของฟีด ในทรานแซกชันเดียวกับข้อมูลหากคลังข้อมูลของคุณรองรับ
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})

เคอร์เซอร์ของฟีดส่งคืนมาในทุกหน้า รวมถึงหน้าสุดท้าย การบันทึกเคอร์เซอร์ไว้ทำให้การรันครั้งถัดไปเริ่มต่อจากอีเวนต์สุดท้ายที่คุณเก็บไว้พอดี จึงไม่มีอีเวนต์ใดถูกอ่านซ้ำหรือตกหล่น

ดูแลให้สำเนาถูกต้อง

  • ไม่ได้รันนานกว่า 30 วัน ฟีดเก็บอีเวนต์ไว้ 30 วัน หากงานไม่ได้รันนานกว่านั้น ให้โหลดแบบเต็มอีกครั้ง และลบออบเจ็กต์ทุกรายการที่การโหลดแบบเต็มไม่ได้ส่งคืนออกจากตาราง
  • โหลดแบบเต็มทุกสัปดาห์ แม้ฟีดจะทำงานปกติ การโหลดแบบเต็มสัปดาห์ละครั้งแล้วเทียบกับตารางของคุณก็เป็นการตรวจที่มีต้นทุนต่ำว่าสำเนาครบถ้วน
  • สิ่งที่คีย์มองไม่เห็นจะไม่ถูกส่งออก งานเห็นเท่าที่คีย์ของงานเห็นเท่านั้น หากการ์ดหลุดออกจากสิทธิ์เข้าถึงของคีย์ เช่น เพราะย้ายไปอยู่ในคอลเลกชันที่จำกัดสิทธิ์ การโหลดแบบเต็มจะไม่ส่งคืนการ์ดนั้น ให้ถือว่าการ์ดนั้นถูกนำออกแล้ว
  • คำถาม บันทึกคำถามมีการส่งออกของตัวเอง GET /v1/questions/export.csv ส่งคืนคำถามในช่วงเวลาหนึ่งเป็นไฟล์ CSV ไฟล์เดียว พร้อมคะแนนและแหล่งที่มา ต้องใช้ขอบเขตสิทธิ์ ask

ใช้งานให้อยู่ในขีดจำกัด

การโหลดแบบเต็มขององค์กรขนาดใหญ่ส่งคำขอจำนวนมาก ให้อ่านทีละรายการด้วย limit=200 และเฝ้าดู เฮดเดอร์ RateLimit งานที่รันทุก 15 นาทีและอ่านเฉพาะการเปลี่ยนแปลงจะใช้คำขอต่ำกว่าโควตาใดก็ตามอยู่มาก