← back to Carnegie Identity Audit
collect.py
88 lines
#!/usr/bin/env python3
"""Bounded read-only queries; no API or database mutation path."""
import datetime, hashlib, json, os, pathlib, subprocess, time, urllib.request
BASE = pathlib.Path(__file__).resolve().parent
PRIOR = pathlib.Path.home() / 'Projects/ticket-system/data/codex-yoloforever/evidence/cycle-20260905T0721Z.n3ijiG/11246'
OUT = BASE / 'verification'
OUT.mkdir(exist_ok=True)
def save(name, value):
(OUT / name).write_text(json.dumps(value, indent=2) + '\n')
def token():
env = {}
for line in (pathlib.Path.home() / 'Projects/secrets-manager/.env').read_text().splitlines():
if '=' in line and not line.startswith('#'):
k, v = line.split('=', 1)
env[k] = v.strip().strip('\"\'')
return env.get('SHOPIFY_ADMIN_TOKEN') or env['SHOPIFY_ADMIN_API_TOKEN']
TOKEN = token()
def gql(query, variables=None):
assert query.lstrip().startswith('query '), 'Only explicit read queries allowed'
for attempt in range(4):
req = urllib.request.Request(
'https://designer-laboratory-sandbox.myshopify.com/admin/api/2026-07/graphql.json',
data=json.dumps({'query': query, 'variables': variables or {}}).encode(),
headers={'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json'})
with urllib.request.urlopen(req, timeout=35) as response:
result = json.load(response)
if result.get('errors'):
if all(e.get('extensions', {}).get('code') == 'THROTTLED' for e in result['errors']):
time.sleep(2 + attempt * 2)
continue
raise RuntimeError(json.dumps(result['errors']))
assert result.get('data') is not None
return result['data']
raise RuntimeError('Read query throttle exhausted')
FIELDS = '''id title handle vendor status tags updatedAt
custom:metafield(namespace:"custom",key:"manufacturer_sku"){id value type updatedAt}
dwc:metafield(namespace:"dwc",key:"manufacturer_sku"){id value type updatedAt}
variants(first:100){pageInfo{hasNextPage} nodes{id sku title}}'''
def main():
prior = json.loads((PRIOR / 'identity-report.json').read_text())
save('prior-identity-report.json', prior)
skus = [r['dw_sku'] for r in prior['rows']]
assert len(skus) == len(set(skus)) == 26
ids = list(dict.fromkeys('gid://shopify/Product/' + r[k] for r in prior['rows']
for k in ['candidate_shopify_product_id', 'earlier_generation_product_id']))
historical = []
for offset in range(0, len(ids), 10):
chunk = ids[offset:offset+10]
data = gql('query History($ids:[ID!]!){nodes(ids:$ids){... on Product{' + FIELDS + '}}}', {'ids':chunk})
historical.extend(dict(requested_id=i, product=p) for i,p in zip(chunk,data['nodes']))
time.sleep(0.5)
save('historical-products.json', historical)
searches = {}
for n, sku in enumerate(skus):
data = gql('query Current($q:String!){productVariants(first:100,query:$q){pageInfo{hasNextPage} nodes{id sku product{' + FIELDS + '}}}}', {'q':'sku:' + sku})
result = data['productVariants']
assert not result['pageInfo']['hasNextPage'], 'Truncated SKU search'
exact = [v for v in result['nodes'] if v['sku'] == sku]
assert exact, 'No exact match for ' + sku
for v in exact:
assert v['product']['vendor'] == 'Carnegie'
assert not v['product']['variants']['pageInfo']['hasNextPage']
searches[sku] = exact
print(f'{n+1}/26 {sku}: {len(exact)} exact variants', flush=True)
save('current-by-sku.json', searches)
time.sleep(0.5)
missing = gql('query Missing{node(id:"gid://shopify/Product/1"){id}}')
assert missing['node'] is None
save('negative-missing-product.json', missing)
sql = "SELECT json_build_object('observed_at',now(),'read_only',current_setting('default_transaction_read_only'),'catalog',(SELECT json_agg(t) FROM (SELECT id,dw_sku,mfr_sku,pattern_number,color_number,parent_sku,product_type,product_url,updated_at FROM carnegie_catalog ORDER BY id) t));"
(OUT/'current-readonly.sql').write_text(sql+'\n')
env = dict(os.environ, PGOPTIONS='-c default_transaction_read_only=on -c statement_timeout=5000', PGCONNECT_TIMEOUT='3')
db = subprocess.run(['psql','-X','-d','dw_unified','-At','-v','ON_ERROR_STOP=1','-f',str(OUT/'current-readonly.sql')],env=env,text=True,capture_output=True,check=True)
save('current-catalog.json',json.loads(db.stdout))
save('collection.json', {'observed_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),
'ticket':'TK-11246','shop':'designer-laboratory-sandbox.myshopify.com','api':'2026-07',
'historical_ids':len(ids),'sku_searches':len(searches),'business_writes':0,
'source_sha256':hashlib.sha256((PRIOR/'identity-report.json').read_bytes()).hexdigest()})
if __name__ == '__main__':
main()