prepare-industry-inbox.py 4.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. """Wrap existing JD VOC response bytes for the passive inbox, without calling VOC or publishing."""
  2. import argparse
  3. import json
  4. from pathlib import Path
  5. import re
  6. import sys
  7. sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'server/voc-library'))
  8. from monitoring_store import exact_id, exact_int, need, pointer, sha, verify_manifest
  9. def assert_no_credentials(value):
  10. if isinstance(value, dict):
  11. for key, item in value.items():
  12. need(not (key.lower() in ('authorization', 'cookie', 'password', 'token', 'apikey', 'api_key', 'secret', 'headers') and item), 'RAW_CONTAINS_PRIVATE_REQUEST_METADATA')
  13. assert_no_credentials(item)
  14. elif isinstance(value, list):
  15. for item in value:
  16. assert_no_credentials(item)
  17. def prepare(raw_path, kind, window, generation, output):
  18. need(kind in ('listings', 'reviews'), 'UNVERIFIED_RESPONSE_PROFILE')
  19. raw = Path(raw_path).read_bytes()
  20. need(len(raw) <= 25_000_000, 'RAW_TOO_LARGE')
  21. doc = json.loads(raw)
  22. assert_no_credentials(doc)
  23. if kind == 'listings':
  24. need(doc.get('id') == 'jd.get_shop_item_list_v1', 'RESPONSE_ENDPOINT_NOT_PROVEN')
  25. base = '/result/data'
  26. checks = [{'pointer': '/httpStatus', 'equals': 200}, {'pointer': '/status', 'equals': 'ok'},
  27. {'pointer': '/result/code', 'equals': 200}, {'pointer': base + '/code', 'equals': 0}]
  28. mapping = {'itemsPointer': base + '/data/ItemList', 'idPointer': '/itemId', 'titlePointer': '/title',
  29. 'scopePointers': {'shopId': base + '/data/shopId'}, 'totalPointer': base + '/data/totalResults',
  30. 'pageCountPointer': base + '/data/TotalPage'}
  31. scope = {'shopId': exact_id(pointer(doc, base + '/data/shopId'))}
  32. endpoint = doc['id']
  33. scope_name = pointer(doc, base + '/data/shopName')
  34. else:
  35. # The actually delivered g2/g3 raw response envelope. No guessing another provider's layout.
  36. base = '/data'
  37. checks = [{'pointer': '/code', 'equals': 200}, {'pointer': base + '/code', 'equals': 0}]
  38. mapping = {'itemsPointer': base + '/data/data', 'idPointer': '/commentId',
  39. 'scopePointers': {'productId': base + '/requestParams/query/itemId'},
  40. 'rowScopePointers': {'productId': '/productId'}, 'totalPointer': base + '/data/pageInfo/count',
  41. 'pageCountPointer': base + '/data/pageInfo/maxPage'}
  42. product = exact_id(pointer(doc, base + '/requestParams/query/itemId'))
  43. need(re.fullmatch(r'\d+', product), 'INVALID_JD_PRODUCT_ID')
  44. scope = {'productId': 'jd:' + product, 'mode': 'interface_default'}
  45. endpoint = 'jd.get_item_comments_v1'
  46. scope_name = scope['productId']
  47. mapping.update(pagePointer=base + '/requestParams/query/page', requestIdPointer=base + '/requestId',
  48. observedAtPointer=base + '/recordTime', successChecks=checks)
  49. page = {'page': exact_int(pointer(doc, mapping['pagePointer'])),
  50. 'requestKey': exact_id(pointer(doc, mapping['requestIdPointer'])), 'status': 'success',
  51. 'rawFile': 'response.json', 'rawSha256': sha(raw), 'mapping': mapping}
  52. manifest = {'schema': 'ksf.monitoring-run.v1', 'kind': kind, 'platform': 'jd', 'scope': scope,
  53. 'scopeName': scope_name, 'endpointId': endpoint, 'window': exact_id(window),
  54. 'generation': exact_id(generation), 'termination': 'unknown', 'pages': [page]}
  55. output = Path(output)
  56. output.mkdir(parents=True, exist_ok=False)
  57. (output / 'response.json').write_bytes(raw)
  58. verified, _ = verify_manifest(manifest, output)
  59. # Write the manifest last, by atomic rename, so a running watcher never sees a half package.
  60. temp = output / 'manifest.pending'
  61. temp.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + '\n', 'utf-8')
  62. temp.rename(output / 'manifest.json')
  63. return {'ok': True, 'manifest': str(output / 'manifest.json'), 'rawSha256': sha(raw),
  64. 'kind': kind, 'scope': scope, 'page': page['page'],
  65. 'verification': verified['verification'], 'paidCalls': 0, 'productionWrites': 0}
  66. if __name__ == '__main__':
  67. p = argparse.ArgumentParser(description=__doc__)
  68. p.add_argument('--raw', required=True, type=Path)
  69. p.add_argument('--kind', required=True, choices=['listings', 'reviews'])
  70. p.add_argument('--window', required=True)
  71. p.add_argument('--generation', required=True)
  72. p.add_argument('--out', required=True, type=Path)
  73. args = p.parse_args()
  74. try:
  75. print(json.dumps(prepare(args.raw, args.kind, args.window, args.generation, args.out), ensure_ascii=False))
  76. except Exception as exc:
  77. code = str(exc) if isinstance(exc, ValueError) and re.fullmatch('[A-Z][A-Z0-9_]+', str(exc)) else 'INBOX_PREPARE_FAILED'
  78. print(json.dumps({'ok': False, 'error': code, 'paidCalls': 0}))
  79. sys.exit(1)