stash
This commit is contained in:
parent
cfcc2c15d3
commit
4f2fe011c0
28 changed files with 3499 additions and 2922 deletions
512
testdata/generate_testdata.py
vendored
Normal file
512
testdata/generate_testdata.py
vendored
Normal file
|
|
@ -0,0 +1,512 @@
|
|||
#!/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()
|
||||
|
||||
2
testdata/user-a.key
vendored
2
testdata/user-a.key
vendored
|
|
@ -1 +1 @@
|
|||
2a7ddeb75181afedbc755924db9f1d527278375ff562858c93a502ab783a6815
|
||||
795e884532881938a1b03e37731443f4d1974327dafd5d508e0cb00cb0b161b6
|
||||
BIN
testdata/user-a.zip
vendored
BIN
testdata/user-a.zip
vendored
Binary file not shown.
2
testdata/user-b.key
vendored
2
testdata/user-b.key
vendored
|
|
@ -1 +1 @@
|
|||
5b0f8e19806c5ab87eced9a0d08855429e1b2be957b760b46fbbef2309eccd43
|
||||
d011fedc3ba733baef6aa7550bbee47db66c2ad3dbfb0f6dcc14c984866df5eb
|
||||
BIN
testdata/user-b.zip
vendored
BIN
testdata/user-b.zip
vendored
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue