يحتاج فريق المنتج الذي يقدّم تقويمًا اقتصاديًا إلى مرشحات دول لكل مستخدم وأوقات إصدار بالمنطقة الزمنية لكل مشاهد وطريقة لعرض الإصدار الذي تغيّر موعده. يعيد Narwhal الإصدارات بوقت UTC ومنطقة الجهة الناشرة الزمنية والفترة والقيمة السابقة وعدّاد مراجعات، ويضيف التطبيق المرشح والساعة والذاكرة المؤقتة.
ما الذي يطلبه التطبيق من واجهة التقويم
لدى فريق المنتج الذي يقدّم تقويمًا اقتصاديًا داخل تطبيقه ثلاث مهام لا يستطيع مصدر البيانات أداءها عنه: أن يختار كل مستخدم دوله، وأن يعرض كل وقت بالمنطقة الزمنية للمشاهد، وأن يبقي المعروض على الشاشة صحيحًا حين يتغير موعد إصدار. توفّر API الإصدارات، ويوفّر التطبيق المرشح والساعة والذاكرة المؤقتة. يقبل GET /v1/economics/calendar المعاملات countries (كررها لكل دولة، بأكواد من ثلاثة أحرف مثل USA أو CAN) وstart_date وend_date وlimit وcursor. يجلب هذا الطلب دولتين ليومين.
curl -s -H "Authorization: Bearer $NARWHAL_API_KEY" \
"https://api.narwhalapi.com/v1/economics/calendar?countries=USA&countries=CAN&start_date=2026-11-05&end_date=2026-11-06&limit=100"{
"releases": [
{
"release_id": "1b503ce9-69ee-4ec4-8dd9-deba7f1a6554",
"revision": 2,
"country": "USA",
"series_id": "usa-employment-report",
"name": "Employment report",
"topic": "employment",
"family": "labour",
"period": "2026-10",
"status": "scheduled",
"at": "2026-11-06T13:30:00Z",
"timezone": "America/New_York",
"values": [
{
"measure": "nonfarm_payrolls_change",
"concept": "payrolls",
"unit": "thousands",
"transformation": "change",
"previous": "29"
},
{
"measure": "unemployment_rate",
"concept": "unemployment_rate",
"unit": "percent",
"previous": "4.2"
}
]
},
{
"release_id": "20c30c26-9a9a-4218-a8fb-6233ac8d291a",
"revision": 1,
"country": "USA",
"series_id": "usa-productivity-and-costs",
"name": "Productivity and costs",
"topic": "other",
"family": "labour",
"period": "2026-Q3",
"stage": "preliminary",
"status": "scheduled",
"at": "2026-11-05T13:30:00Z",
"timezone": "America/New_York",
"values": []
}
],
"next_cursor": null
}تضم الاستجابة الكاملة خمسة إصدارات. at هو وقت الإصدار بتوقيت UTC، وtimezone هي المنطقة الزمنية للجهة الناشرة نفسها، لذلك يحوّل التطبيق at لكل مشاهد ويستطيع عرض منطقة الناشر كتسمية ثانية. period هي فترة المرجع، وvalues[].previous هي القيمة التي سبقت. يعرّف release_id الإصدار عبر الطلبات، ويرتفع revision حين تتغير تفاصيله. values موجودة دائمًا وقد تكون قائمة فارغة.
خزّن مؤقتًا لكل دولة واعرض بمنطقة المشاهد الزمنية
يختار المستخدمون مجموعات دول مختلفة، لذلك خزّن كل دولة على حدة ثم ادمج القوائم لكل مستخدم. فمستخدمان يتابعان الولايات المتحدة يتشاركان قائمة واحدة مخزنة.
- اجلب دولة واحدة في كل مرة تطلب
country_releasesدولة واحدة وتتابعnext_cursorحتى يصبحnull، لأن الصفحة تضم 100 إصدار على الأكثر. - احتفظ بالإجابة لمدة قصيرة
TTLهو عدد الثواني التي تبقى فيها الدولة المخزنة حديثة. اخترها بحسب سرعة حاجة مستخدميك إلى رؤية موعد تغيّر. - ادمج للمستخدم وحوّل تجمع
user_calendarدول المستخدم، وتحوّل كلatبمنطقة المستخدم الزمنية، وترتّب بالوقت المحلي. تطبّقZoneInfoتغييرات ساعة المشاهد، فلا يخزّن التطبيق أي فروق توقيت.import os import time from datetime import datetime from zoneinfo import ZoneInfo import requests BASE = "https://api.narwhalapi.com" HEADERS = {"Authorization": f"Bearer {os.environ['NARWHAL_API_KEY']}"} TTL = 300 # seconds a cached country stays fresh cache = {} # (country, start, end) -> (fetched_at, releases) def country_releases(country, start, end): key = (country, start, end) hit = cache.get(key) if hit and time.time() - hit[0] < TTL: return hit[1] params = {"countries": country, "start_date": start, "end_date": end, "limit": 100} releases = [] while True: r = requests.get(f"{BASE}/v1/economics/calendar", params=params, headers=HEADERS, timeout=10) r.raise_for_status() body = r.json() releases += body["releases"] if not body["next_cursor"]: break params["cursor"] = body["next_cursor"] cache[key] = (time.time(), releases) return releases def user_calendar(user, start, end): zone = ZoneInfo(user["tz"]) rows = [] for country in user["countries"]: for x in country_releases(country, start, end): if "at" in x: local = datetime.fromisoformat(x["at"].replace("Z", "+00:00")).astimezone(zone) rows.append((local, x)) return sorted(rows, key=lambda t: t[0]) user = {"countries": ["USA", "CAN"], "tz": "Asia/Jakarta"} day = None for local, x in user_calendar(user, "2026-11-05", "2026-11-06"): if local.date() != day: day = local.date() print(day.isoformat()) print(f" {local:%H:%M} {x['country']} {x['name']} {x.get('period', '-')}")
المخرجات أدناه لمستخدم في جاكرتا يتابع الولايات المتحدة وكندا. إصدار Credit statistics في الساعة 20:00 بتوقيت UTC يوم 6 نوفمبر، أي الساعة 03:00 يوم 7 نوفمبر في جاكرتا، فيقع تحت تاريخ مختلف في عرض هذا المستخدم. جمّع بحسب تاريخ المشاهد المحلي، وليس بتاريخ UTC أبدًا.
2026-11-05
20:30 USA Productivity and costs 2026-Q3
20:30 USA Jobless claims 2026-10-31
2026-11-06
20:30 USA Employment report 2026-10
20:30 CAN Labour force survey 2026-10
2026-11-07
03:00 USA Credit statistics 2026-09أبقِ الشاشة صحيحة حين يتغير موعد إصدار
احفظ release_id وrevision مع كل صف تعرضه. بعد التحديث، الصف الذي مراجعته أعلى من المخزّنة قد تغيّر، فأعد رسمه ونبّه إلى الوقت الجديد. أما الإصدار بحالة cancelled فيُشطب أو يُخفى، والصف الذي لم يعد يرجع في النافذة قد يكون انتقل إلى تاريخ آخر، فأسقطه من ذلك العرض.
بعض الإصدارات بلا وقت دقيق وتحمل date أو between أو by بدل at. يتخطاها السكربت أعلاه. في تطبيق حقيقي، اعرضها في أعلى يومها كعناصر طوال اليوم. وللإشعارات الفورية تبلّغ webhooks عن release.scheduled وrelease.rescheduled وrelease.cancelled؛ راجع استقبال webhooks.
قرارات يتخذها فريق المنتج مرة واحدة
| القرار | الخيار | الحقل أو المعامل |
|---|---|---|
| مرشح الدول لكل مستخدم | أرسل دول المستخدم، مع قائمة مخزنة لكل دولة | countries |
| الوقت المعروض | حوّل وقت UTC إلى منطقة المشاهد الزمنية، واعرض منطقة الناشر كتسمية | at وtimezone |
| تجميع التواريخ | جمّع بحسب تاريخ المشاهد المحلي | at |
| كشف التغيير | قارن المراجعة المخزنة بالجديدة | release_id وrevision |
| شرائح الموضوع أو العائلة | رشّح داخل التطبيق، فالطلب لا يحتوي معامل موضوع | family وtopic |
| حدود المعدل وإعادة المحاولة | استخدم ذاكرة مؤقتة وتراجع عند استجابة 429، راجع تخزين الاستجابات مؤقتًا ومعالجة أخطاء 429 | ترويسات الاستجابة |
هل يستطيع المستخدمون التصفية بحسب الموضوع أو العائلة؟
يقبل الطلب الدول والتواريخ وlimit وcursor، ولا يحتوي مرشح موضوع. لكل إصدار family ولمعظمها topic، فرشّح في شيفرتك. استخدم x.get("topic") لأن بعض الإصدارات بلا موضوع، والبحث المباشر يثير خطأ مفتاح.
هل تتبع نافذة التاريخ توقيت UTC أم التاريخ المحلي للناشر؟
تتبع التاريخ المحلي للناشر. إصدارات اليابان في 23:30 أو 23:50 بتوقيت UTC تقع صباح اليوم التالي في طوكيو، فتكون في نافذة اليوم التالي. حين يبني التطبيق يومًا بمنطقة المشاهد الزمنية، اطلب يومًا إضافيًا من كل جانب ورشّح بحسب at.
كم طلبًا تحتاج شاشة واحدة؟
طلب واحد لكل دولة في الذاكرة المؤقتة، وطلب إضافي لكل صفحة زائدة حين تكون لـnext_cursor قيمة. الدول المخزنة لا تكلّف شيئًا حتى تنتهي مدتها.
نُشر في .