toolshed/testdata/generate_testdata.py
2026-08-01 22:20:45 +02:00

512 lines
36 KiB
Python

#!/usr/bin/env python3
"""Generate realistic test data for the offlinedata export/import functionality.
Produces user-a.key, user-b.key, user-a.zip, user-b.zip for two federated users
who are friends with each other.
"""
import csv
import io
import json
import os
import random
import zipfile
from hashlib import sha256
from nacl.signing import SigningKey
from nacl.encoding import HexEncoder
# Realistic item database with shared_data category/tag references using fully qualified handles
# Format: (name, category_handle, tags_handles, policy, description, qty, properties_list)
# Category handles: origin#category:name (e.g., 'git:base#category:tools')
# Tag handles: origin#tag:name (e.g., 'git:tools#tag:drill')
# Properties format: list of (property_handle, value) tuples
ITEMS_DATABASE = [
# IT/Electronics items
('MacBook Pro 14"', 'git:it#category:pc', 'git:it#tag:laptop,git:it#tag:monitor', 'private', 'Powerful development machine, 16GB RAM, M1 Pro', 1, [('git:base#property:memory', '16GB'), ('git:base#property:weight', '1.7kg')]),
('USB-C Hub', 'git:base#category:hardware', 'git:ee#tag:connector,git:ee#tag:cable', 'lend', '7-in-1 docking station with HDMI, USB3, SD reader', 3, [('git:base#property:power', '65W')]),
('Wireless Mouse', 'git:it#category:pc', 'git:it#tag:mouse', 'private', 'Logitech MX Master 3, rechargeable', 1, [('git:base#property:weight', '110g')]),
('Monitor 4K', 'git:it#category:pc', 'git:it#tag:monitor', 'private', '32" LG UltraFine, factory calibrated', 1, [('git:base#property:area', '32"'), ('git:base#property:power', '90W')]),
('Mechanical Keyboard', 'git:it#category:pc', 'git:it#tag:keyboard', 'lend', 'Keychron K8, hot-swappable, blue switches', 1, [('git:base#property:weight', '400g')]),
('USB Flash Drive', 'git:base#category:hardware', 'git:it#tag:USB', 'private', '256GB Kingston DataTraveler, encrypted', 2, [('git:base#property:memory', '256GB'), ('git:base#property:weight', '50g')]),
('HDMI Cables', 'git:electrical#category:connectors', 'git:it#tag:hdmi,git:it#tag:cable', 'share', 'Assorted 2.0 and 2.1 cables, 6 pack', 1, [('git:base#property:length', '2m')]),
('Power Banks', 'git:base#category:hardware', 'git:ee#tag:battery,git:ee#tag:power supply', 'lend', 'Anker 65W with multiple USB ports', 2, [('git:base#property:power', '65W'), ('git:base#property:weight', '195g')]),
('Webcam 4K', 'git:it#category:pc', 'git:it#tag:webcam', 'private', 'Logitech Brio, excellent for video calls', 1, [('git:base#property:power', '5W')]),
('Microphone', 'git:it#category:pc', 'git:it#tag:microphone', 'private', 'Blue Yeti condenser, noise cancellation', 1, [('git:base#property:power', '2.1W'), ('git:base#property:weight', '508g')]),
# Tools
('Cordless Drill', 'git:tools#category:powertools', 'git:tools#tag:cordless,git:tools#tag:drill', 'lend', 'DeWalt 20V, includes battery and charger', 1, [('git:base#property:voltage', '20V'), ('git:base#property:power', '1500W')]),
('Screwdriver Set', 'git:base#category:tools', 'git:base#tag:screwdriver', 'share', '24-piece precision set, magnetic', 1, [('git:base#property:weight', '1.2kg')]),
('Adjustable Wrench', 'git:base#category:tools', 'git:base#tag:wrench', 'share', 'Chrome plated, 0-24mm range', 3, [('git:base#property:length', '0.24m')]),
('Hammer', 'git:base#category:tools', 'git:base#tag:hammer', 'share', '16oz claw hammer, fiberglass handle', 2, [('git:base#property:weight', '453g')]),
('Level', 'git:base#category:tools', 'git:base#tag:level', 'share', '24" aluminum spirit level', 1, [('git:base#property:length', '0.61m')]),
('Tape Measure', 'git:base#category:tools', 'git:base#tag:tape measure', 'share', '25ft retractable, self-locking', 2, [('git:base#property:length', '7.6m')]),
('Flashlight', 'git:base#category:tools', 'git:base#tag:flashlight', 'lend', 'LED headlamp, 500 lumens, rechargeable', 1, [('git:base#property:power', '10W')]),
('Socket Set', 'git:base#category:tools', 'git:base#tag:wrench', 'share', 'SAE and metric, 70 pieces, impact-rated', 1, [('git:base#property:weight', '2.5kg')]),
('Pliers Set', 'git:base#category:tools', 'git:base#tag:pliers', 'share', 'Needle-nose, slip-joint, locking vise, 4pc', 1, [('git:base#property:weight', '1.8kg')]),
('Pipe Wrench', 'git:base#category:tools', 'git:base#tag:wrench', 'share', '10" adjustable, cast iron', 1, [('git:base#property:length', '0.25m')]),
# Electrical connectors
('Power Cable Type C', 'git:electrical#category:electrical', 'git:electrical#tag:Type C', 'private', 'CEE 7/16 Europlug cable, 3m', 2, [('git:base#property:voltage', '250V'), ('git:base#property:length', '3m')]),
('USB Type-C Adapter', 'git:base#category:hardware', 'git:it#tag:Type C,git:it#tag:USB', 'share', 'USB 3.1 adapter hub', 1, [('git:base#property:power', '100W')]),
('Ethernet Cable Cat6A', 'git:base#category:hardware', 'git:ee#tag:ethernet,git:ee#tag:cable', 'share', 'Cat6A shielded, 100ft spool', 1, [('git:base#property:length', '30m')]),
('HDMI 2.1 Cable', 'git:electrical#category:connectors', 'git:it#tag:hdmi', 'share', '4K60Hz capable, gold-plated', 1, [('git:base#property:length', '2m')]),
# Electronics & Hardware
('Resistor Assortment', 'git:base#category:hardware', 'git:ee#tag:resistor', 'share', '1000pc through-hole variety pack', 1, [('git:base#property:weight', '200g')]),
('LED Pack', 'git:base#category:hardware', 'git:ee#tag:led', 'share', '100pc RGB addressable LEDs', 1, [('git:base#property:power', '5V')]),
('Arduino Uno', 'git:base#category:hardware', 'git:ee#tag:arduino', 'lend', 'ATmega328P-based microcontroller board', 1, [('git:base#property:voltage', '5V'), ('git:base#property:memory', '32KB')]),
('Raspberry Pi 4', 'git:base#category:hardware', 'git:ee#tag:raspberry', 'private', '4GB RAM, Broadcom BCM2711', 1, [('git:base#property:memory', '4GB'), ('git:base#property:power', '10W')]),
('Soldering Iron', 'git:base#category:tools', 'git:ee#tag:soldering', 'share', 'Hakko FX-888D digital station', 1, [('git:base#property:temperature', '250°C'), ('git:base#property:power', '70W')]),
('Solder', 'git:base#category:hardware', 'git:ee#tag:solder', 'share', 'Lead-free, 0.8mm spool', 1, [('git:base#property:weight', '100g')]),
('Breadboard', 'git:base#category:hardware', 'git:ee#tag:connector', 'share', '830 point solderless breadboard', 1, [('git:base#property:weight', '180g')]),
# Fasteners
('M3 Screw Assortment', 'git:screws#category:screws', 'git:screws#tag:m3', 'share', '100pc socket head cap screws', 1, [('git:base#property:diameter', '3mm')]),
('M5 Bolt Pack', 'git:screws#category:screws', 'git:screws#tag:m5', 'share', '50pc hex bolts with nuts', 1, [('git:base#property:diameter', '5mm')]),
('Washer Set', 'git:base#category:hardware', 'git:base#tag:washer', 'share', '200pc stainless steel assortment', 1, [('git:base#property:weight', '300g')]),
# Miscellaneous
('Backpack', 'git:base#category:tools', 'git:base#tag:tool', 'private', 'Waterproof laptop backpack, 30L capacity', 1, [('git:base#property:volume', '30L'), ('git:base#property:weight', '1.2kg')]),
('Headphones', 'git:it#category:pc', 'git:it#tag:headset', 'private', 'Sony WH-1000XM5, noise canceling', 1, [('git:base#property:weight', '250g'), ('git:base#property:frequency', '20Hz')]),
# Extended PC/Laptop items
('Dell XPS 15', 'git:it#category:pc', 'git:it#tag:laptop', 'private', '15.6" FHD display, Intel i7, 32GB RAM', 1, [('git:base#property:memory', '32GB'), ('git:base#property:weight', '2.0kg')]),
('ThinkPad X1 Carbon', 'git:it#category:pc', 'git:it#tag:laptop', 'lend', '14" lightweight ultrabook, business class', 1, [('git:base#property:memory', '16GB'), ('git:base#property:weight', '1.13kg')]),
('iPad Pro 12.9"', 'git:it#category:pc', 'git:it#tag:tablet', 'private', 'M1 chip, 8GB RAM, excellent for design', 1, [('git:base#property:memory', '8GB'), ('git:base#property:area', '12.9"')]),
('Samsung Galaxy Tab S8', 'git:it#category:pc', 'git:it#tag:tablet', 'share', '11" OLED display, great for media', 1, [('git:base#property:memory', '8GB'), ('git:base#property:area', '11"')]),
('Surface Laptop 5', 'git:it#category:pc', 'git:it#tag:laptop', 'private', '13.5" touchscreen, sleek design', 1, [('git:base#property:memory', '16GB'), ('git:base#property:weight', '1.3kg')]),
('ASUS VivoBook', 'git:it#category:pc', 'git:it#tag:laptop', 'lend', '15.6" FHD, AMD Ryzen 5, budget friendly', 1, [('git:base#property:memory', '8GB'), ('git:base#property:weight', '1.8kg')]),
('Razer Blade 15', 'git:it#category:pc', 'git:it#tag:laptop', 'private', 'High-performance gaming laptop, RTX 3060', 1, [('git:base#property:memory', '16GB'), ('git:base#property:weight', '2.0kg')]),
('MacBook Air M2', 'git:it#category:pc', 'git:it#tag:laptop', 'private', '13" Retina display, fanless, 8GB RAM', 1, [('git:base#property:memory', '8GB'), ('git:base#property:weight', '1.24kg')]),
('Lenovo Yoga 9i', 'git:it#category:pc', 'git:it#tag:laptop', 'lend', '13" 2-in-1 convertible, OLED display', 1, [('git:base#property:memory', '16GB'), ('git:base#property:weight', '1.38kg')]),
('HP Spectre x360', 'git:it#category:pc', 'git:it#tag:laptop', 'share', '14" OLED touchscreen, convertible design', 1, [('git:base#property:memory', '16GB'), ('git:base#property:weight', '1.39kg')]),
# Monitors and Displays
('Dell U2723DE', 'git:it#category:pc', 'git:it#tag:monitor', 'share', '27" QHD USB-C dock monitor', 1, [('git:base#property:area', '27"'), ('git:base#property:power', '90W')]),
('LG 27GP850', 'git:it#category:pc', 'git:it#tag:monitor', 'private', '27" 1440p gaming monitor, 165Hz', 1, [('git:base#property:area', '27"'), ('git:base#property:power', '70W')]),
('ASUS PA279CV', 'git:it#category:pc', 'git:it#tag:monitor', 'share', '27" ProArt display, color-accurate', 1, [('git:base#property:area', '27"'), ('git:base#property:power', '65W')]),
('BenQ EW2780U', 'git:it#category:pc', 'git:it#tag:monitor', 'lend', '4K USB-C monitor with docking', 1, [('git:base#property:area', '27"'), ('git:base#property:power', '100W')]),
('Samsung M7', 'git:it#category:pc', 'git:it#tag:monitor', 'share', '32" Smart TV monitor with apps', 1, [('git:base#property:area', '32"'), ('git:base#property:power', '110W')]),
('Ultrawide Curved Monitor', 'git:it#category:pc', 'git:it#tag:monitor', 'lend', '34" 21:9 ultrawide, 100Hz', 1, [('git:base#property:area', '34"'), ('git:base#property:power', '65W')]),
('Portable USB-C Monitor', 'git:it#category:pc', 'git:it#tag:monitor', 'share', '15.6" full HD portable display', 1, [('git:base#property:area', '15.6"'), ('git:base#property:power', '5W')]),
# Keyboards and Input devices
('Keychron K2', 'git:it#category:pc', 'git:it#tag:keyboard', 'private', 'Mechanical, wireless, compact', 1, [('git:base#property:weight', '350g')]),
('Corsair K95 Platinum', 'git:it#category:pc', 'git:it#tag:keyboard', 'private', 'Mechanical RGB, aluminum frame', 1, [('git:base#property:weight', '1.2kg')]),
('Apple Magic Keyboard', 'git:it#category:pc', 'git:it#tag:keyboard', 'private', 'Wireless, rechargeable, sleek design', 1, [('git:base#property:weight', '240g')]),
('Ducky One 2', 'git:it#category:pc', 'git:it#tag:keyboard', 'share', 'Mechanical with PBT keycaps', 1, [('git:base#property:weight', '950g')]),
('Ergonomic Split Keyboard', 'git:it#category:pc', 'git:it#tag:keyboard', 'lend', 'Curved layout, wrist support', 1, [('git:base#property:weight', '1.5kg')]),
('Gaming Macro Keyboard', 'git:it#category:pc', 'git:it#tag:keyboard', 'private', 'Programmable keys, mechanical switches', 1, [('git:base#property:weight', '1.1kg')]),
# Mice and Trackpads
('Logitech G Pro X', 'git:it#category:pc', 'git:it#tag:mouse', 'private', 'Lightweight gaming mouse, 25000 DPI', 1, [('git:base#property:weight', '63g')]),
('Apple Magic Trackpad', 'git:it#category:pc', 'git:it#tag:mouse', 'private', 'Wireless multi-touch surface', 1, [('git:base#property:weight', '231g')]),
('Razer DeathAdder V3', 'git:it#category:pc', 'git:it#tag:mouse', 'share', 'Gaming mouse with HyperScroll wheel', 1, [('git:base#property:weight', '63g')]),
('MX Master 2S', 'git:it#category:pc', 'git:it#tag:mouse', 'share', 'Multi-device, customizable buttons', 1, [('git:base#property:weight', '112g')]),
('Vertical Ergonomic Mouse', 'git:it#category:pc', 'git:it#tag:mouse', 'lend', 'Reduces wrist strain', 1, [('git:base#property:weight', '150g')]),
# Audio Equipment
('Sony WH-CH720N', 'git:it#category:pc', 'git:it#tag:headset', 'lend', 'Wireless headphones with ANC', 1, [('git:base#property:weight', '192g'), ('git:base#property:frequency', '20Hz')]),
('Bose QuietComfort 45', 'git:it#category:pc', 'git:it#tag:headset', 'private', 'Premium ANC headphones', 1, [('git:base#property:weight', '238g'), ('git:base#property:frequency', '20Hz')]),
('Audio-Technica AT2020', 'git:it#category:pc', 'git:it#tag:microphone', 'share', 'Cardioid condenser microphone', 1, [('git:base#property:weight', '155g'), ('git:base#property:power', '1.0W')]),
('Shure SM7B', 'git:it#category:pc', 'git:it#tag:microphone', 'private', 'Broadcasting microphone, dynamic', 1, [('git:base#property:weight', '294g'), ('git:base#property:power', '0.5W')]),
('JBL 104 Studio Monitor', 'git:it#category:pc', 'git:it#tag:speaker', 'share', 'Compact desktop speaker pair', 2, [('git:base#property:weight', '1.2kg'), ('git:base#property:power', '24W')]),
# Cables and Adapters
('Lightning Cable', 'git:electrical#category:connectors', 'git:it#tag:cable', 'share', '2m, MFi certified', 3, [('git:base#property:length', '2m')]),
('DisplayPort Cable', 'git:electrical#category:connectors', 'git:it#tag:cable', 'share', '4K 60Hz capable, 3m', 2, [('git:base#property:length', '3m')]),
('Thunderbolt 3 Cable', 'git:electrical#category:connectors', 'git:ee#tag:cable', 'private', '40Gbps, 2m', 1, [('git:base#property:length', '2m')]),
('VGA to HDMI Adapter', 'git:base#category:hardware', 'git:it#tag:adapter', 'lend', 'Legacy display support', 1, [('git:base#property:power', '0W')]),
('USB to Ethernet Adapter', 'git:base#category:hardware', 'git:ee#tag:ethernet', 'share', 'Gigabit, plug and play', 1, [('git:base#property:power', '5W')]),
# Storage Devices
('Samsung 870 QVO SSD', 'git:base#category:hardware', 'git:it#tag:storage', 'private', '1TB SATA SSD, fast performance', 1, [('git:base#property:memory', '1TB'), ('git:base#property:weight', '60g')]),
('WD Blue NVMe', 'git:base#category:hardware', 'git:it#tag:storage', 'share', '500GB M.2 SSD, reliable', 1, [('git:base#property:memory', '500GB'), ('git:base#property:weight', '10g')]),
('Seagate Barracuda HDD', 'git:base#category:hardware', 'git:it#tag:storage', 'lend', '2TB 3.5" hard drive', 1, [('git:base#property:memory', '2TB'), ('git:base#property:weight', '600g')]),
('Samsung Portable SSD T7', 'git:base#category:hardware', 'git:it#tag:storage', 'share', '1TB USB-C, super fast', 1, [('git:base#property:memory', '1TB'), ('git:base#property:weight', '58g')]),
('OWC Envoy SSD', 'git:base#category:hardware', 'git:it#tag:storage', 'private', 'Thunderbolt 3, 2TB', 1, [('git:base#property:memory', '2TB'), ('git:base#property:weight', '100g')]),
# Networking Equipment
('ASUS RT-AX88U', 'git:base#category:hardware', 'git:ee#tag:router', 'private', 'WiFi 6 router, dual-band', 1, [('git:base#property:power', '35W')]),
('Ubiquiti UniFi AP', 'git:base#category:hardware', 'git:ee#tag:access point', 'share', 'Professional WiFi 6E access point', 1, [('git:base#property:power', '12W')]),
('TP-Link Deco X68', 'git:base#category:hardware', 'git:ee#tag:mesh', 'lend', 'WiFi 6 mesh system, 3-pack', 1, [('git:base#property:power', '24W')]),
('Netgear Nighthawk AXE300', 'git:base#category:hardware', 'git:ee#tag:router', 'share', 'WiFi 6E tri-band router', 1, [('git:base#property:power', '40W')]),
# Power Management
('APC UPS 1500VA', 'git:base#category:hardware', 'git:ee#tag:UPS', 'lend', 'Backup power supply, 10 minutes runtime', 1, [('git:base#property:power', '1500VA'), ('git:base#property:weight', '5.0kg')]),
('Belkin Surge Protector', 'git:base#category:hardware', 'git:ee#tag:power supply', 'share', '6 outlet with USB, 1080J', 1, [('git:base#property:power', '15A')]),
('Anker PowerStrip', 'git:base#category:hardware', 'git:ee#tag:power supply', 'share', 'Smart surge protector, WiFi enabled', 1, [('git:base#property:power', '15A')]),
# Tool Extensions
('Impact Driver', 'git:tools#category:powertools', 'git:tools#tag:cordless', 'lend', '20V, compact design', 1, [('git:base#property:voltage', '20V'), ('git:base#property:power', '1200W')]),
('Circular Saw', 'git:tools#category:powertools', 'git:tools#tag:saw', 'share', 'DeWalt 20V cordless, 7.25"', 1, [('git:base#property:voltage', '20V'), ('git:base#property:power', '800W')]),
('Random Orbital Sander', 'git:tools#category:powertools', 'git:tools#tag:sander', 'share', '5" dust collection', 1, [('git:base#property:power', '400W')]),
('Reciprocating Saw', 'git:tools#category:powertools', 'git:tools#tag:saw', 'lend', 'Variable speed, tool-free blade change', 1, [('git:base#property:power', '1100W')]),
('Jigsaw', 'git:tools#category:powertools', 'git:tools#tag:saw', 'share', 'Orbital motion, LED light', 1, [('git:base#property:power', '600W')]),
('Angle Grinder', 'git:tools#category:powertools', 'git:tools#tag:grinder', 'private', '4.5" with safety features', 1, [('git:base#property:power', '1200W')]),
('Heat Gun', 'git:tools#category:powertools', 'git:tools#tag:heat', 'share', '2 temperature settings', 1, [('git:base#property:power', '1800W')]),
('Belt Sander', 'git:tools#category:powertools', 'git:tools#tag:sander', 'lend', '3"x21" powerful sanding', 1, [('git:base#property:power', '800W')]),
('Table Saw', 'git:tools#category:powertools', 'git:tools#tag:saw', 'lend', '10" blade, 15A motor', 1, [('git:base#property:power', '1500W')]),
('Miter Saw', 'git:tools#category:powertools', 'git:tools#tag:saw', 'share', 'Compound sliding, 12"', 1, [('git:base#property:power', '1500W')]),
# Hand Tool Extensions
('Combination Square', 'git:base#category:tools', 'git:base#tag:measuring', 'share', '12" stainless steel', 1, [('git:base#property:length', '0.3m')]),
('Speed Square', 'git:base#category:tools', 'git:base#tag:measuring', 'share', '7" aluminum', 1, [('git:base#property:weight', '150g')]),
('Torpedo Level', 'git:base#category:tools', 'git:base#tag:level', 'share', '9" compact level', 1, [('git:base#property:length', '0.23m')]),
('Chisel Set', 'git:base#category:tools', 'git:base#tag:chisel', 'lend', '4-piece woodworking chisels', 1, [('git:base#property:weight', '800g')]),
('Nail Punch', 'git:base#category:tools', 'git:base#tag:punch', 'share', 'Set of 3, various sizes', 1, [('git:base#property:weight', '200g')]),
('Pry Bar', 'git:base#category:tools', 'git:base#tag:pry', 'share', '18" steel, multi-purpose', 1, [('git:base#property:length', '0.45m')]),
('Hex Key Set', 'git:base#category:tools', 'git:base#tag:hex', 'share', '25-piece, SAE and metric', 1, [('git:base#property:weight', '400g')]),
('Torque Wrench', 'git:base#category:tools', 'git:base#tag:wrench', 'private', '1/2" drive, 10-150 Nm', 1, [('git:base#property:length', '0.5m')]),
('C-Clamp', 'git:base#category:tools', 'git:base#tag:clamp', 'share', '4" capacity, pack of 4', 1, [('git:base#property:weight', '2.0kg')]),
('Bar Clamp', 'git:base#category:tools', 'git:base#tag:clamp', 'lend', '48" quick-grip style', 1, [('git:base#property:length', '1.2m')]),
# Electrical components continuation
('Capacitor Assortment', 'git:base#category:hardware', 'git:ee#tag:capacitor', 'share', '500pc various values', 1, [('git:base#property:weight', '250g')]),
('Diode Pack', 'git:base#category:hardware', 'git:ee#tag:diode', 'share', '200pc rectifier and signal diodes', 1, [('git:base#property:weight', '100g')]),
('Transistor Assortment', 'git:base#category:hardware', 'git:ee#tag:transistor', 'share', '100pc mixed types', 1, [('git:base#property:weight', '150g')]),
('IC Assortment Pack', 'git:base#category:hardware', 'git:ee#tag:ic', 'share', 'Common logic and op-amps', 1, [('git:base#property:weight', '200g')]),
('Jumper Wire Kit', 'git:base#category:hardware', 'git:ee#tag:cable', 'share', '60pc breadboard jumpers', 1, [('git:base#property:weight', '50g')]),
('PCB Prototyping Board', 'git:base#category:hardware', 'git:ee#tag:pcb', 'share', 'Copper clad universal board', 1, [('git:base#property:weight', '300g')]),
('Heat Shrink Tubing', 'git:base#category:hardware', 'git:ee#tag:insulation', 'share', 'Multi-size assortment', 1, [('git:base#property:weight', '100g')]),
('Flux Pen', 'git:base#category:hardware', 'git:ee#tag:solder', 'share', 'Liquid flux for soldering', 1, [('git:base#property:weight', '50g')]),
('Desoldering Braid', 'git:base#category:hardware', 'git:ee#tag:solder', 'share', 'Copper braid wick', 1, [('git:base#property:weight', '25g')]),
('Solder Sucker', 'git:base#category:hardware', 'git:ee#tag:solder', 'share', 'Manual desoldering pump', 1, [('git:base#property:weight', '50g')]),
# Additional fasteners
('Bolt Assortment', 'git:screws#category:screws', 'git:screws#tag:bolt', 'share', 'Various sizes and lengths', 1, [('git:base#property:weight', '2.0kg')]),
('Nut Assortment', 'git:screws#category:screws', 'git:screws#tag:nut', 'share', 'Metric and SAE sizes', 1, [('git:base#property:weight', '1.5kg')]),
('Anchor Assortment', 'git:screws#category:screws', 'git:screws#tag:anchor', 'share', 'Drywall and cavity anchors', 1, [('git:base#property:weight', '500g')]),
('Brad Nails', 'git:screws#category:screws', 'git:screws#tag:nail', 'share', '16 gauge, 500 pack', 1, [('git:base#property:weight', '200g')]),
('Finishing Nails', 'git:screws#category:screws', 'git:screws#tag:nail', 'share', 'Various sizes, 2 lb box', 1, [('git:base#property:weight', '900g')]),
# Storage and Organization
('Tool Box', 'git:base#category:tools', 'git:base#tag:storage', 'share', 'Metal with compartments', 1, [('git:base#property:volume', '30L'), ('git:base#property:weight', '2.5kg')]),
('Part Organizer', 'git:base#category:tools', 'git:base#tag:storage', 'share', 'Plastic drawer unit, 39 drawers', 1, [('git:base#property:volume', '50L'), ('git:base#property:weight', '3.0kg')]),
('Wall Mount Pegboard', 'git:base#category:tools', 'git:base#tag:storage', 'lend', '4ftx2ft with hooks and baskets', 1, [('git:base#property:weight', '5.0kg')]),
('Cable Organizer', 'git:base#category:hardware', 'git:it#tag:cable', 'share', 'Silicone clips and ties', 1, [('git:base#property:weight', '200g')]),
('Label Maker', 'git:base#category:hardware', 'git:it#tag:office', 'share', 'Handheld, easy to use', 1, [('git:base#property:weight', '150g')]),
# Additional Miscellaneous
('Work Gloves', 'git:base#category:tools', 'git:base#tag:safety', 'share', 'Leather palm, pack of 3', 1, [('git:base#property:weight', '300g')]),
('Safety Glasses', 'git:base#category:tools', 'git:base#tag:safety', 'share', 'Anti-scratch lens, 5 pack', 1, [('git:base#property:weight', '100g')]),
('Dust Mask', 'git:base#category:tools', 'git:base#tag:safety', 'share', 'N95 particulate, 50 pack', 1, [('git:base#property:weight', '200g')]),
('First Aid Kit', 'git:base#category:tools', 'git:base#tag:safety', 'share', 'Compact workspace kit', 1, [('git:base#property:weight', '500g')]),
('Desk Lamp', 'git:it#category:pc', 'git:it#tag:office', 'lend', 'LED, adjustable, USB powered', 1, [('git:base#property:power', '8W')]),
('Monitor Stand', 'git:it#category:pc', 'git:it#tag:office', 'share', 'Adjustable height, storage shelf', 1, [('git:base#property:weight', '2.0kg')]),
('Keyboard Wrist Rest', 'git:it#category:pc', 'git:it#tag:office', 'private', 'Memory foam, ergonomic', 1, [('git:base#property:weight', '300g')]),
('Mouse Pad', 'git:it#category:pc', 'git:it#tag:office', 'share', 'Large XL size with wrist support', 1, [('git:base#property:weight', '400g')]),
('Desk Organizer', 'git:it#category:pc', 'git:it#tag:office', 'private', 'Multi-compartment caddy', 1, [('git:base#property:weight', '600g')]),
('USB Hub Powered', 'git:base#category:hardware', 'git:it#tag:USB', 'share', '7-port with individual switches', 1, [('git:base#property:power', '60W')]),
]
LOCATIONS_DATABASE = [
('Home Office Desk', 'Primary workspace for development'),
('Study Cabinet', 'Vertical storage with shelving'),
('Tool Bench', 'Workshop storage for hardware'),
('Electronics Shelf', 'Electronics and components storage'),
('Bedroom Closet', 'Personal items and backups'),
('Garage Workbench', 'Large tools and equipment'),
('Storage Room', 'Archive and backup storage'),
('Component Drawer', 'Small electronics and adapters'),
('Utility Closet', 'Household maintenance supplies'),
]
def load_shared_data():
"""Load shared_data JSON files and return available tags, categories, and properties."""
shared_data_dir = os.path.join(os.path.dirname(__file__), '..', 'backend', 'shared_data')
all_tags = {} # Maps fully qualified name to tag info
all_categories = {} # Maps fully qualified name to category info
all_properties = {} # Maps fully qualified name to property info
for filename in os.listdir(shared_data_dir):
if filename.endswith('.json'):
filepath = os.path.join(shared_data_dir, filename)
try:
with open(filepath, 'r') as f:
data = json.load(f)
if 'tags' in data:
for tag in data['tags']:
name = tag.get('name', '')
category = tag.get('category', '')
fq_name = f"{category}/{name}" if category else name
all_tags[fq_name] = tag
if 'categories' in data:
for cat in data['categories']:
name = cat.get('name', '')
parent = cat.get('parent', '')
fq_name = f"{parent}/{name}" if parent else name
all_categories[fq_name] = cat
if 'properties' in data:
for prop in data['properties']:
name = prop.get('name', '')
fq_name = f"git:base#property:{name}"
all_properties[fq_name] = prop
except Exception as e:
print(f"Warning: Could not load {filepath}: {e}")
return all_tags, all_categories, all_properties
def generate_keypair():
"""Generate a signing key pair and return (hex_private_key, hex_public_key)."""
signing_key = SigningKey.generate()
private_hex = signing_key.encode(encoder=HexEncoder).decode('utf-8')
public_hex = signing_key.verify_key.encode(encoder=HexEncoder).decode('utf-8')
return private_hex, public_hex
def generate_locations_csv(count):
"""Generate a realistic locations.csv with varied storage locations and parent-child relationships."""
locations = random.sample(LOCATIONS_DATABASE, min(count, len(LOCATIONS_DATABASE)))
locations.extend([
(f'Storage {chr(65 + i)}', f'General storage area {i+1}')
for i in range(max(0, count - len(LOCATIONS_DATABASE)))
])
rows = []
location_id_map = {} # Maps location name to its ID
# First pass: create top-level locations
parent_locations = [locations[0]] if locations else []
for i, (name, description) in enumerate(parent_locations, 1):
location_id_map[name] = str(i)
rows.append({
'id': str(i),
'name': name,
'description': description,
'category': '',
'parent': '',
'path': name,
})
# Second pass: create remaining locations, some as children
current_id = len(rows) + 1
for i, (name, description) in enumerate(locations[1:], 1):
parent_name = ''
parent_id = ''
path = name
# Make every other location a child of a random parent
if i % 2 == 0 and parent_locations:
parent_name = random.choice([p[0] for p in parent_locations])
parent_id = location_id_map[parent_name]
path = f"{parent_name}/{name}"
rows.append({
'id': str(current_id),
'name': name,
'description': description,
'category': '',
'parent': parent_id,
'path': path,
})
location_id_map[name] = str(current_id)
current_id += 1
output = io.StringIO()
fieldnames = ['id', 'name', 'description', 'category', 'parent', 'path']
writer = csv.DictWriter(output, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
return output.getvalue().encode('utf-8'), location_id_map
def generate_friends_csv(friend_username, friend_domain, friend_public_key):
"""Generate a friends.csv with one friend entry."""
rows = [
{'username': friend_username, 'domain': friend_domain,
'handle': f'{friend_username}@{friend_domain}', 'public_key': friend_public_key}
]
output = io.StringIO()
fieldnames = ['username', 'domain', 'handle', 'public_key']
writer = csv.DictWriter(output, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
return output.getvalue().encode('utf-8')
def _quote_value_if_needed(value):
"""Mirror `toolshed.offlinedata._quote_value_if_needed()`: wrap a value in double quotes
(CSV-style, doubling any embedded quotes) if it contains a comma or a quote character, so it
round-trips correctly through the comma-separated "handle=value" properties cell.
"""
if any(ch in value for ch in ',"'):
return '"' + value.replace('"', '""') + '"'
return value
def generate_inventory_csv(count, num_locations, location_id_map, item_range=None):
"""Generate a realistic inventory.csv with varied items using shared_data references and properties.
Args:
count: Number of items to generate
num_locations: Number of locations
location_id_map: Map of location names to IDs
item_range: Tuple of (start_idx, end_idx) to partition the ITEMS_DATABASE, or None for all items
"""
# Get location names from the map
location_names = list(location_id_map.keys())
# Use partition if specified, otherwise use all items
if item_range is not None:
start_idx, end_idx = item_range
available_items = ITEMS_DATABASE[start_idx:end_idx + 1]
else:
available_items = ITEMS_DATABASE
# Ensure we have enough items by repeating the database
available_items = available_items * ((count // len(available_items)) + 1)
selected_items = random.sample(available_items, min(count, len(available_items)))
rows = []
for i, item_tuple in enumerate(selected_items, 1):
# Handle both old format (6 elements) and new format (7 elements with properties)
if len(item_tuple) == 7:
name, category_handle, tags_str, policy, description, qty, properties_list = item_tuple
else:
name, category_handle, tags_str, policy, description, qty = item_tuple
properties_list = []
location = random.choice(location_names) if location_names else ''
tags_for_item = [t.strip() for t in tags_str.split(',')]
# Format properties as a comma-separated "handle=value" list, matching
# `toolshed.offlinedata._encode_properties_cell()` (quoting values that contain a
# comma or a quote character so they survive the round trip).
properties_str = ', '.join(
f"{prop_handle}={_quote_value_if_needed(value)}" for prop_handle, value in properties_list)
rows.append({
'id': str(i),
'name': name,
'description': description,
'category': category_handle,
'availability_policy': policy,
'owned_quantity': str(qty),
'storage_location': location,
'tags': ', '.join(tags_for_item),
'properties': properties_str,
'files': '',
'created_at': '2026-08-01T00:00:00+00:00',
})
output = io.StringIO()
fieldnames = ['id', 'name', 'description', 'category', 'availability_policy',
'owned_quantity', 'storage_location', 'tags', 'properties', 'files', 'created_at']
writer = csv.DictWriter(output, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
return output.getvalue().encode('utf-8')
def generate_sample_files():
"""Generate sample files for the 'files/' folder in the zip."""
files = {}
sample_contents = [
b'Product specifications and manual for electronics.\n',
b'Workshop notes and maintenance log.\n',
b'Purchase receipt and warranty information.\n',
]
for i, content in enumerate(sample_contents):
content_hash = sha256(content).hexdigest()
arcname = f'files/sample_{i}.txt'
files[arcname] = content
return files
def generate_export_zip(username, domain, friend_username, friend_domain, friend_public_key,
num_locations=10, num_items=150, item_range=None):
"""Generate a zip file (bytes) like user_data() produces.
Args:
username: Username for this user
domain: Domain for this user
friend_username: Friend's username
friend_domain: Friend's domain
friend_public_key: Friend's public key
num_locations: Number of locations to generate
num_items: Number of items to generate
item_range: Tuple of (start_idx, end_idx) to partition the ITEMS_DATABASE, or None for all items
"""
num_locations = max(5, min(20, num_locations))
num_items = max(100, min(200, num_items))
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'a', zipfile.ZIP_DEFLATED) as zip_file:
locations_csv, location_id_map = generate_locations_csv(num_locations)
zip_file.writestr('locations.csv', locations_csv)
friends_csv = generate_friends_csv(friend_username, friend_domain, friend_public_key)
zip_file.writestr('friends.csv', friends_csv)
inventory_csv = generate_inventory_csv(num_items, num_locations, location_id_map, item_range)
zip_file.writestr('inventory.csv', inventory_csv)
sample_files = generate_sample_files()
for arcname, content in sample_files.items():
zip_file.writestr(arcname, content)
return zip_buffer.getvalue()
def main():
"""Generate and write test data files."""
script_dir = os.path.dirname(os.path.abspath(__file__))
print("Loading shared_data references...")
all_tags, all_categories, all_properties = load_shared_data()
print(f" ✓ Loaded {len(all_tags)} tags, {len(all_categories)} categories, and {len(all_properties)} properties")
print("\nGenerating keypairs...")
user_a_private, user_a_public = generate_keypair()
user_b_private, user_b_public = generate_keypair()
print(f" user-a@a.localhost: {user_a_private[:16]}...{user_a_private[-16:]}")
print(f" user-b@b.localhost: {user_b_private[:16]}...{user_b_private[-16:]}")
print("\nWriting key files...")
with open(os.path.join(script_dir, 'user-a.key'), 'w') as f:
f.write(user_a_private)
with open(os.path.join(script_dir, 'user-b.key'), 'w') as f:
f.write(user_b_private)
print(" ✓ user-a.key")
print(" ✓ user-b.key")
print("\nGenerating realistic export zips with partitioned items...")
# Partition the ITEMS_DATABASE randomly
partition_point = int(len(ITEMS_DATABASE)/4) + random.randint(0, int(len(ITEMS_DATABASE)/2))
user_a_item_range = (0, partition_point)
user_b_item_range = (partition_point + 1, len(ITEMS_DATABASE) - 1)
print(f" Database partitioned at index {partition_point}")
print(f" User A items: {user_a_item_range[0]}-{user_a_item_range[1]} ({user_a_item_range[1] - user_a_item_range[0] + 1} unique items)")
print(f" User B items: {user_b_item_range[0]}-{user_b_item_range[1]} ({user_b_item_range[1] - user_b_item_range[0] + 1} unique items)")
user_a_zip = generate_export_zip(
'user-a', 'a.localhost', 'user-b', 'b.localhost', user_b_public,
num_locations=12, num_items=175, item_range=user_a_item_range
)
user_b_zip = generate_export_zip(
'user-b', 'b.localhost', 'user-a', 'a.localhost', user_a_public,
num_locations=8, num_items=140, item_range=user_b_item_range
)
with open(os.path.join(script_dir, 'user-a.zip'), 'wb') as f:
f.write(user_a_zip)
with open(os.path.join(script_dir, 'user-b.zip'), 'wb') as f:
f.write(user_b_zip)
print(" ✓ user-a.zip")
print(" ✓ user-b.zip")
print("\n✓ Test data generation complete!")
print(f"Files written to: {script_dir}/")
if __name__ == '__main__':
main()