verify-expanded-review-publication.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. """Read production reviews at one generation and compare every round-two review."""
  2. import collections
  3. import datetime as dt
  4. import json
  5. from pathlib import Path
  6. import sqlite3
  7. import urllib.error
  8. import urllib.request
  9. ROOT = Path(__file__).resolve().parents[1]
  10. OUT = ROOT / 'docs/industry-product-reviews-20261106/round2'
  11. API = 'https://kangshifu.fmode.cn/api/library'
  12. def api(request):
  13. req = urllib.request.Request(API, json.dumps(request).encode(), {'Content-Type': 'application/json'})
  14. try:
  15. response = urllib.request.urlopen(req, timeout=40)
  16. except urllib.error.HTTPError as error:
  17. data = json.loads(error.read())
  18. raise ValueError(data.get('error', {}).get('code', 'READ_FAILED')) from error
  19. with response:
  20. data = json.load(response)
  21. if not data.get('ok'):
  22. raise ValueError(data.get('error', {}).get('code', 'READ_FAILED'))
  23. return data
  24. def append_text(raw):
  25. value = json.loads(raw or 'null')
  26. if isinstance(value, str):
  27. return value.strip()
  28. if isinstance(value, dict):
  29. return '\n'.join(value[k] for k in ('content', 'comment', 'appendComment') if isinstance(value.get(k), str)).strip()
  30. if isinstance(value, list):
  31. return '\n'.join(part[k] for part in value if isinstance(part, dict)
  32. for k in ('content', 'comment') if isinstance(part.get(k), str)).strip()
  33. return ''
  34. def verify():
  35. summary = api({'op': 'summary'})
  36. generation = summary['generation']
  37. actual = {}
  38. cursor = None
  39. pages = 0
  40. while True:
  41. query = {'op': 'browse', 'entity': 'reviews', 'quality': 'audit', 'limit': 100, 'generation': generation}
  42. if cursor:
  43. query['cursor'] = cursor
  44. data = api(query)
  45. assert data['generation'] == generation
  46. assert data['totalMatched'] == summary['counts']['reviews']
  47. for row in data['records']:
  48. assert row['record_id'] not in actual
  49. actual[row['record_id']] = row
  50. cursor = data['nextCursor']
  51. pages += 1
  52. if pages % 10 == 0:
  53. print('Read production reviews: ' + str(len(actual)), flush=True)
  54. if not cursor:
  55. break
  56. assert len(actual) == summary['counts']['reviews']
  57. assert len({r['product_id'] for r in actual.values()}) == data['distinctLinkedProducts']
  58. selected = {}
  59. batches = []
  60. failures = []
  61. product_counts = collections.Counter()
  62. fields = ('product_id', 'content', 'rating_raw', 'rating_scale', 'review_date_raw',
  63. 'purchased_sku_text', 'is_default', 'identity_state', 'category_state')
  64. for folder in sorted((ROOT / 'work/product-reviews/round2/intake').glob('batch-*')):
  65. receipt = json.loads((folder / 'verification.json').read_text(encoding='utf-8'))
  66. uri = (folder / 'staging.sqlite').resolve().as_uri() + '?mode=ro'
  67. matched = 0
  68. with sqlite3.connect(uri, uri=True) as connection:
  69. connection.row_factory = sqlite3.Row
  70. for row in connection.execute('SELECT * FROM reviews ORDER BY id'):
  71. row = dict(row)
  72. rid = row['id']
  73. assert rid not in selected
  74. selected[rid] = row
  75. production = actual.get(rid)
  76. if production is None:
  77. failures.append({'reviewId': rid, 'code': 'MISSING'})
  78. continue
  79. different = [key for key in fields if row[key] != production.get(key)]
  80. if append_text(row['append_content_json']) != production.get('append_content', ''):
  81. different.append('append_content')
  82. if different:
  83. failures.append({'reviewId': rid, 'code': 'FIELD_MISMATCH', 'fields': different})
  84. continue
  85. matched += 1
  86. product_counts[row['product_id']] += 1
  87. batches.append({'batchId': receipt['batchId'], 'expectedReviews': receipt['reviews'], 'verifiedReviews': matched})
  88. assert len(selected) == 512
  89. api({'op': 'version', 'generation': generation})
  90. coverage = len({r['product_id'] for r in actual.values()})
  91. result = {'ok': not failures and len(product_counts) == 57, 'checkedAt': dt.datetime.now(dt.timezone.utc).isoformat(),
  92. 'generation': generation, 'productionCounts': summary['counts'],
  93. 'analysisReviews': summary['analysisCounts']['reviews'],
  94. 'productionReviewsFullyRead': len(actual), 'pagesRead': pages,
  95. 'productsWithStoredReviews': coverage,
  96. 'productsWithoutStoredReviews': summary['counts']['products'] - coverage,
  97. 'expectedProducts': 57, 'verifiedProducts': len(product_counts),
  98. 'expectedReviews': len(selected), 'verifiedReviews': sum(product_counts.values()),
  99. 'comparedFields': ['review_id', *fields, 'append_content'],
  100. 'products': dict(sorted(product_counts.items())), 'batches': batches, 'failures': failures,
  101. 'productionImported': not failures, 'readOnly': True, 'productionWrites': 0, 'paidCalls': 0}
  102. OUT.mkdir(parents=True, exist_ok=True)
  103. (OUT / 'production-readback.json').write_text(json.dumps(result, ensure_ascii=False, indent=2) + '\n', encoding='utf-8')
  104. print(json.dumps({k: v for k, v in result.items() if k not in ('products', 'productionCounts')}, ensure_ascii=False, indent=2))
  105. assert result['ok'], 'PRODUCTION_READBACK_FAILED'
  106. if __name__ == '__main__':
  107. verify()