ส่งออกไปยังคลังข้อมูล
นักวิเคราะห์มักต้องการข้อมูลของ Sursa ไว้คู่กับข้อมูลอื่นของบริษัท เช่น การ์ดใดผ่านการตรวจสอบแล้วและใครเป็นผู้ตรวจสอบ ความรู้เพิ่มขึ้นอย่างไรในแต่ละทีม และคำถามใดยังไม่ได้รับคำตอบ คู่มือนี้สร้างงานตามกำหนดเวลาที่คัดลอกข้อมูลของ Sursa ไปยังคลังข้อมูล (data warehouse) ของคุณและดูแลให้สำเนาเป็นปัจจุบัน
งานนี้รวมสามส่วนของ API เข้าด้วยกัน:
| ส่วน | สิ่งที่ได้ |
|---|---|
รายการการ์ดที่ใช้ updated_since และรายการสั้นที่อ่านทั้งรายการ | สถานะปัจจุบันของออบเจ็กต์ที่เปลี่ยนแปลงตั้งแต่การรันครั้งล่าสุด |
| ฟีดอีเวนต์ | ข้อมูลการลบและประวัติของสิ่งที่เกิดขึ้นย้อนหลัง 30 วัน |
| เคอร์เซอร์ | หน้าที่ครบถ้วนโดยไม่มีช่องว่างหรือรายการซ้ำ |
คุณต้องมีคีย์ที่มีขอบเขตสิทธิ์ cards:read และ users:read รวมถึง webhooks:manage สำหรับฟีดอีเวนต์ ให้งานนี้มีคีย์ของตัวเองที่มีบทบาท admin เพราะรายการผู้ใช้แบบเต็มและฟีดอีเวนต์เปิดให้เฉพาะบทบาทนี้ คีย์ที่มีบทบาทและโปรไฟล์ข้อมูลที่แคบกว่าจะส่งออกเฉพาะสิ่งที่กลุ่มใดกลุ่มหนึ่งมองเห็นได้ คีย์แบบนี้อ่านรายการผู้ใช้แบบย่อ (brief=true) และอ่านฟีดอีเวนต์ไม่ได้ จึงต้องรับข้อมูลการลบจากการโหลดแบบเต็มทุกสัปดาห์ที่อธิบายไว้ด้านล่าง
ตาราง
โครงสร้างแบบง่ายที่มีหนึ่งตารางต่อออบเจ็กต์และหนึ่งตารางสำหรับอีเวนต์:
| ตาราง | คีย์ | ข้อมูลมาจาก |
|---|---|---|
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 |
เก็บแต่ละออบเจ็กต์ตามที่ได้รับไว้ในคอลัมน์ JSON รวมถึงคอลัมน์ที่คุณคิวรีบ่อย (updated_at collection_id status และ owner_id) ฟิลด์ใหม่จะปรากฏใน API เวอร์ชันถัดไป คอลัมน์ JSON ทำให้คุณได้ฟิลด์เหล่านั้นโดยไม่ต้องแก้งาน
การโหลดครั้งแรก
อ่านทุกรายการให้ครบโดยไม่ใช้ updated_since และเขียนทุกออบเจ็กต์ จดเวลาที่การโหลด เริ่มต้น ไว้ การรันครั้งถัดไปจะทำต่อจากจุดนั้น
การโหลดส่วนเพิ่ม
ในทุกการรัน เช่น ทุก 15 นาที:
- จดเวลาเริ่มต้นของการรันครั้งนี้
- อ่านการ์ดโดยตั้ง
updated_sinceเป็นเวลาเริ่มต้นของการรันครั้งก่อนหักเวลาเผื่อ 1 นาที และขอแต่ละหน้าถัดไปด้วยเคอร์เซอร์และupdated_sinceค่าเดิม (เคอร์เซอร์ที่ไม่มีตัวกรองของหน้าแรกจะได้การตอบกลับ400 api.cursor_invalid) อ่านคอลเลกชัน ผู้ใช้ และกลุ่มทั้งรายการ เพราะคอลเลกชันและผู้ใช้ไม่มีupdated_atและupdated_atของกลุ่มไม่เปลี่ยนเมื่อสมาชิกของกลุ่มเปลี่ยน แล้วสร้างหรือแทนที่แต่ละออบเจ็กต์ตามid - อ่านฟีดอีเวนต์จากเคอร์เซอร์ที่บันทึกไว้ครั้งก่อน เพิ่มแต่ละอีเวนต์ต่อท้าย
sursa_eventsและดำเนินการลบตามอีเวนต์ อีเวนต์card.deletedจะนำการ์ดออกจากsursa_cardsหรือทำเครื่องหมายว่าลบแล้วหากคุณเก็บประวัติไว้ - บันทึกเวลาเริ่มต้นและเคอร์เซอร์ใหม่ของฟีด ในทรานแซกชันเดียวกับข้อมูลหากคลังข้อมูลของคุณรองรับ
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 นาทีและอ่านเฉพาะการเปลี่ยนแปลงจะใช้คำขอต่ำกว่าโควตาใดก็ตามอยู่มาก