HIPAA/GDPR-ready medical image sanitization and encryption library with blockchain audit anchoring
Project description
PyMedSec - Medical Image Security Framework
Medical Image Security and Compliance Library
Secure medical image processing with policy-driven sanitization, encryption, and audit logging.
Quick Start • Documentation • Architecture • Examples • Compliance
Overview
PyMedSec is a Python library for secure medical image processing in healthcare environments. It provides policy-driven sanitization, encryption, and audit logging for protected health information (PHI) workflows.
Core Features
- Envelope Encryption: AES-256-GCM encryption with KMS integration
- PHI Sanitization: DICOM/EXIF metadata removal and de-identification
- Audit Logging: Tamper-evident logs with optional blockchain anchoring
- ML Integration: Memory-only decryption for ML workflows
- KMS Backends: AWS KMS, HashiCorp Vault, and mock backends
- Compliance Alignment: HIPAA, GDPR, CLIA, and GxP support documentation
Architecture
PyMedSec uses a modular architecture focused on security and traceability.
System Architecture
┌─────────────────────────────────────────────────────────────────────────────────┐
│ PyMedSec Framework │
├─────────────────────┬─────────────────────┬─────────────────────┬──────────────┤
│ Public API │ Core Security │ Compliance │ Audit │
│ │ │ │ │
│ ┌─────────────┐ │ ┌─────────────┐ │ ┌─────────────┐ │ ┌──────────┐ │
│ │ load_policy │ │ │ Envelope │ │ │ PHI │ │ │ Tamper │ │
│ │ scrub_dicom │ │ │ Encryption │ │ │ Sanitizer │ │ │ Evident │ │
│ │ encrypt_blob│ │ │ AES-256-GCM │ │ │ DICOM/EXIF │ │ │ Logging │ │
│ │ decrypt_blob│ │ │ │ │ │ │ │ │ HMAC │ │
│ └─────────────┘ │ └─────────────┘ │ └─────────────┘ │ └──────────┘ │
└─────────────────────┴─────────────────────┴─────────────────────┴──────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────┐
│ KMS Adapters │ │ Crypto Provider │ │ Policy Engine │ │ Blockchain │
│ │ │ │ │ │ │ Anchoring │
│ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ ┌─────────┐ │
│ │ AWS KMS │ │ │ │ Key │ │ │ │ HIPAA │ │ │ │Ethereum │ │
│ │ Vault │ │ │ │ Generation │ │ │ │ GDPR │ │ │ │Fabric │ │
│ │ Mock │ │ │ │ Wrapping │ │ │ │ GxP/CLIA │ │ │ │Mock │ │
│ └─────────────┘ │ │ └─────────────┘ │ │ └─────────────┘ │ │ └─────────┘ │
└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────┘
Data Flow Architecture
Medical Image Processing Pipeline
┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ │ │ │ │ │ │ │
│ Raw Image │──▶│ PHI Scrubbing │──▶│ Encryption │──▶│ Secure Storage │
│ (DICOM/ │ │ │ │ │ │ │
│ PNG/JPEG) │ │ • Remove PII │ │ • Generate DEK │ │ • Encrypted Pkg │
│ │ │ • Strip EXIF │ │ • AES-256-GCM │ │ • Audit Trail │
│ │ │ • Regenerate │ │ • Wrap with KMS │ │ • Blockchain │
│ │ │ UIDs │ │ • Sign Package │ │ Anchor │
└─────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Sanitization │ │ Encryption │ │ Audit Events │
│ Report │ │ Metadata │ │ │
│ • Removed Tags │ │ • Algorithm │ │ • Actor │
│ • Pseudo PID │ │ • Key Reference │ │ • Timestamp │
│ • Hash │ │ • IV/Nonce │ │ • Operation │
│ • Compliance │ │ • AAD Context │ │ • Outcome │
└─────────────────┘ └─────────────────┘ └─────────────────┘
ML Training Pipeline
Secure Machine Learning Workflow
┌─────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Encrypted │ │ Memory-Only │ │ Tensor │ │ Model Training │
│ Dataset │──▶│ Decryption │──▶│ Conversion │──▶│ │
│ │ │ │ │ │ │ • PyTorch │
│ • .enc files│ │ • KMS Unwrap │ │ • DICOM→Tensor │ │ • TensorFlow │
│ • Metadata │ │ • AES Decrypt │ │ • Preprocessing │ │ • No Disk I/O │
│ • Audit Log │ │ • Verify HMAC │ │ • Normalization │ │ • Auto Cleanup │
└─────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Zero-Copy │ │ Image Tensors │ │ Privacy │
│ Operations │ │ │ │ Guarantees │
│ │ │ • Shape: (H,W,C)│ │ │
│ • No temp files │ │ • Dtype: float32│ │ • No PHI leaks │
│ • Memory pools │ │ • Range: [0,1] │ │ • Secure delete │
│ • Automatic │ │ • Batch ready │ │ • Audit trail │
│ cleanup │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
Quick Start
Installation
# Production installation
pip install pymedsec
# Development installation with all features
pip install pymedsec[dev,aws,vault,ocr]
# Specific feature sets
pip install pymedsec[aws] # AWS KMS support
pip install pymedsec[vault] # HashiCorp Vault support
pip install pymedsec[ocr] # OCR-based redaction
pip install pymedsec[ethereum] # Ethereum blockchain backend
pip install pymedsec[hyperledger] # Hyperledger blockchain backend
pip install pymedsec[blockchain-all] # All blockchain backends
30-Second Example
from pymedsec import load_policy, scrub_dicom, get_kms_client, encrypt_blob
# 1. Load HIPAA-compliant policy
policy = load_policy("hipaa_default")
# 2. Initialize KMS (use AWS KMS in production)
kms = get_kms_client("mock") # or "aws" with proper credentials
# 3. Sanitize medical image
with open("patient_scan.dcm", "rb") as f:
clean_data = scrub_dicom(f.read(), policy=policy, pseudo_pid="PX001")
# 4. Encrypt for secure storage
encrypted_package = encrypt_blob(
clean_data,
kms_client=kms,
aad={"dataset": "study2025", "modality": "CT"}
)
# 5. Save encrypted package
with open("secure_scan.enc", "w") as f:
f.write(encrypted_package.to_json())
print("Medical image securely processed and encrypted.")
API Documentation
High-Level API (Recommended)
Policy Management
from pymedsec import load_policy, list_policies, set_active_policy
# Load built-in policies
policy = load_policy("hipaa_default") # or "gdpr_default", "gxp_default"
# Load custom policy
policy = load_policy("/path/to/custom_policy.yaml")
# List all available policies
policies = list_policies()
# Set global active policy
set_active_policy("hipaa_default")
KMS Integration
from pymedsec import get_kms_client
# AWS KMS (production)
kms = get_kms_client("aws",
key_id="alias/medical-images",
region_name="us-east-1")
# HashiCorp Vault
kms = get_kms_client("vault",
vault_url="https://vault.company.com",
vault_path="medical/keys/imaging")
# Mock KMS (development/testing)
kms = get_kms_client("mock")
Image Processing
from pymedsec import scrub_dicom, scrub_image, encrypt_blob, decrypt_blob
# DICOM sanitization with PHI removal
clean_dicom = scrub_dicom(dicom_bytes,
policy=policy,
pseudo_pid="ANON123",
preserve_technical=True)
# Generic image sanitization
clean_image = scrub_image(image_bytes,
format_hint="png", # or "jpeg", "tiff"
policy=policy)
# Encryption with authenticated additional data
package = encrypt_blob(clean_data,
kms_client=kms,
aad={"study": "TRIAL001", "modality": "MRI"})
# Decryption
original_data = decrypt_blob(package, kms_client=kms)
ML Integration
from pymedsec import SecureImageDataset, decrypt_to_tensor
# Create secure dataset for training
dataset = SecureImageDataset(
data_dir="./encrypted_scans/",
policy=policy,
kms_client=kms,
transform=torchvision.transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485], std=[0.229])
])
)
# Use with PyTorch DataLoader
dataloader = torch.utils.data.DataLoader(dataset, batch_size=32, shuffle=True)
for batch_tensors, metadata in dataloader:
# Train your model
outputs = model(batch_tensors)
loss = criterion(outputs, targets)
# Direct tensor decryption (zero-copy)
tensor = decrypt_to_tensor(encrypted_package,
kms_client=kms,
format_hint="dicom")
Low-Level API (Advanced Use Cases)
from pymedsec.config import SecurityConfig
from pymedsec.crypto import EncryptionEngine
from pymedsec.sanitize import DicomSanitizer
from pymedsec.audit import AuditLogger
# Advanced configuration
config = SecurityConfig.load_from_file("/etc/pymedsec/config.yaml")
# Direct encryption engine
engine = EncryptionEngine(config)
encrypted_data = engine.encrypt(data, key_ref="prod-key-001")
# Advanced DICOM processing
sanitizer = DicomSanitizer(config)
result = sanitizer.sanitize(dicom_dataset)
# Audit logging
logger = AuditLogger(audit_path="/var/log/pymedsec.jsonl")
logger.log_operation("ENCRYPT", outcome="success", file_hash="sha256:abc123...")
Expected Outputs & Policy Examples
This section shows what to expect when using different policies and operations with PyMedSec.
Policy Selection & Expected Behavior
HIPAA Default Policy
from pymedsec import load_policy, scrub_dicom
# Load HIPAA policy
policy = load_policy("hipaa_default")
print(f"Policy: {policy.name}")
print(f"PHI Removal: {policy.sanitization.dicom.remove_private_tags}")
print(f"UID Regeneration: {policy.sanitization.dicom.regenerate_uids}")
Expected Output:
Policy: HIPAA Default Policy
PHI Removal: True
UID Regeneration: True
Compliance Framework: HIPAA
Retention Period: 2557 days (7 years)
GDPR Default Policy
# Load GDPR policy
policy = load_policy("gdpr_default")
print(f"Data Minimization: {policy.compliance.data_minimization}")
print(f"Pseudonymization: {policy.compliance.pseudonymization_required}")
Expected Output:
Policy: GDPR Compliance Policy
Data Minimization: True
Pseudonymization: True
Right to Erasure: Enabled
Purpose Limitation: medical_research
GxP/Laboratory Policy
# Load GxP policy for clinical trials
policy = load_policy("gxp_default")
print(f"Validation Required: {policy.compliance.validation_required}")
print(f"Audit Level: {policy.audit.detail_level}")
Expected Output:
Policy: GxP Clinical Laboratory Policy
Validation Required: True
Audit Level: comprehensive
21 CFR Part 11: Compliant
CLIA Standards: Aligned
DICOM Sanitization Examples
Basic HIPAA Sanitization
from pymedsec import scrub_dicom, load_policy
# Load original DICOM
with open("patient_scan.dcm", "rb") as f:
original_dicom = f.read()
# Apply HIPAA sanitization
policy = load_policy("hipaa_default")
result = scrub_dicom(original_dicom, policy=policy, pseudo_pid="STUDY001_P001")
print(f"Original size: {len(original_dicom)} bytes")
print(f"Sanitized size: {len(result.sanitized_data)} bytes")
print(f"Tags removed: {len(result.removed_tags)}")
print(f"UIDs regenerated: {result.uids_regenerated}")
Expected Output:
Original size: 2,847,392 bytes
Sanitized size: 2,834,156 bytes
Tags removed: 23
UIDs regenerated: True
Pseudo Patient ID: STUDY001_P001
Compliance Hash: sha256:f4a7b2c9e8d6...
Detailed Sanitization Report
# Get detailed sanitization report
result = scrub_dicom(original_dicom, policy=policy, detailed_report=True)
print("=== DICOM Sanitization Report ===")
print(f"Patient Name: {result.report.original_patient_name} → REMOVED")
print(f"Patient ID: {result.report.original_patient_id} → {result.report.pseudo_patient_id}")
print(f"Study Date: {result.report.original_study_date} → {result.report.anonymized_study_date}")
print(f"Institution: {result.report.original_institution} → REMOVED")
print("\nTechnical tags preserved:")
for tag in result.report.preserved_tags[:5]:
print(f" {tag}")
Expected Output:
=== DICOM Sanitization Report ===
Patient Name: John Doe → REMOVED
Patient ID: 12345678 → STUDY001_P001
Study Date: 2025-09-09 → 2025-01-01
Institution: General Hospital → REMOVED
Technical tags preserved:
(0018,0050) Slice Thickness: 5.0
(0018,0088) Spacing Between Slices: 5.0
(0020,0032) Image Position Patient: [...]
(0028,0030) Pixel Spacing: [0.5, 0.5]
(0028,0010) Rows: 512
Image Type Specific Examples
PyMedSec supports multiple medical image formats, each with specific metadata handling and sanitization approaches.
DICOM (.dcm) - Complete Example
from pymedsec import scrub_dicom, load_policy
# Input: Original DICOM file
with open("mri_brain_001.dcm", "rb") as f:
original_dicom = f.read()
policy = load_policy("hipaa_default")
result = scrub_dicom(original_dicom, policy=policy, pseudo_pid="MRI_STUDY_001")
print("=== DICOM Processing ===")
print(f"Original file size: {len(original_dicom):,} bytes")
print(f"Image dimensions: {result.metadata['Rows']}x{result.metadata['Columns']}")
print(f"Modality: {result.metadata['Modality']}")
print(f"Bits allocated: {result.metadata['BitsAllocated']}")
Expected Input (DICOM Tags):
(0008,0020) Study Date: '20250909'
(0008,0030) Study Time: '143015.123000'
(0008,0080) Institution Name: 'General Hospital'
(0008,0090) Referring Physician: 'Dr. Smith'
(0010,0010) Patient's Name: 'Doe^John^M'
(0010,0020) Patient ID: '12345678'
(0010,0030) Patient's Birth Date: '19851203'
(0010,0040) Patient's Sex: 'M'
(0018,0050) Slice Thickness: '5.0'
(0018,0088) Spacing Between Slices: '5.0'
(0020,000D) Study Instance UID: '1.2.840.113619.2.5.1762583153...'
(0028,0010) Rows: 512
(0028,0011) Columns: 512
(0028,0100) Bits Allocated: 16
Expected Output (Sanitized):
=== DICOM Processing ===
Original file size: 2,847,392 bytes
Image dimensions: 512x512
Modality: MR
Bits allocated: 16
=== Sanitization Results ===
PHI Tags Removed: 23
Technical Tags Preserved: 156
UIDs Regenerated: 4
Date Shift Applied: +67 days
Pseudo Patient ID: MRI_STUDY_001
Processing Time: 0.234 seconds
Sanitized Tags:
(0008,0020) Study Date: '20251115' # Shifted
(0008,0030) Study Time: '000000.000000' # Anonymized
(0008,0080) Institution Name: [REMOVED]
(0008,0090) Referring Physician: [REMOVED]
(0010,0010) Patient's Name: [REMOVED]
(0010,0020) Patient ID: 'MRI_STUDY_001'
(0010,0030) Patient's Birth Date: [REMOVED]
(0010,0040) Patient's Sex: [REMOVED]
(0018,0050) Slice Thickness: '5.0' # Preserved
(0020,000D) Study Instance UID: '2.25.987654321...' # Regenerated
PNG - Photographic/Microscopy Images
from pymedsec import scrub_image, load_policy
# Input: PNG with EXIF metadata
with open("microscopy_sample.png", "rb") as f:
original_png = f.read()
policy = load_policy("hipaa_default")
result = scrub_image(original_png, format_hint="png", policy=policy)
print("=== PNG Processing ===")
print(f"Original file size: {len(original_png):,} bytes")
print(f"Format: {result.format}")
print(f"Metadata found: {len(result.original_metadata)} fields")
Expected Input (PNG EXIF):
File: microscopy_sample.png (1,234,567 bytes)
Image Size: 2048x1536 pixels
Color Mode: RGB
EXIF Metadata:
Make: 'Olympus'
Model: 'BX53 Microscope'
DateTime: '2025:09:09 14:30:15'
Software: 'cellSens Standard 2.3'
Artist: 'Lab Technician John Doe'
UserComment: 'Patient ID: 12345678, Sample: Biopsy'
GPS Info: Present (Hospital Location)
Copyright: 'General Hospital Pathology Dept'
ImageDescription: 'H&E stain, 40x magnification'
XResolution: 300.0 dpi
YResolution: 300.0 dpi
Expected Output (Sanitized PNG):
=== PNG Processing ===
Original file size: 1,234,567 bytes
Format: PNG
Metadata found: 12 fields
=== Sanitization Results ===
EXIF Tags Removed: 8
Technical Tags Preserved: 4
PHI References: 3 removed
Processing Time: 0.089 seconds
Sanitized Metadata:
Make: [REMOVED]
Model: [REMOVED]
DateTime: [REMOVED]
Software: [REMOVED]
Artist: [REMOVED]
UserComment: [REMOVED]
GPS Info: [REMOVED]
Copyright: [REMOVED]
ImageDescription: [REMOVED]
XResolution: 300.0 dpi # Preserved (technical)
YResolution: 300.0 dpi # Preserved (technical)
ColorSpace: sRGB # Preserved (technical)
Output file size: 1,198,432 bytes (2.9% reduction)
JPEG - Clinical Photography
from pymedsec import scrub_image
# Input: Clinical photograph with extensive metadata
with open("wound_documentation.jpg", "rb") as f:
original_jpeg = f.read()
result = scrub_image(original_jpeg, format_hint="jpeg", policy=policy)
Expected Input (JPEG EXIF/IPTC):
File: wound_documentation.jpg (856,432 bytes)
Image Size: 4032x3024 pixels
Quality: 92%
EXIF Metadata:
Camera Make: 'Canon'
Camera Model: 'EOS R5'
DateTime Original: '2025:09:09 14:30:15'
GPS Latitude: 40.7128° N
GPS Longitude: 74.0060° W
Lens Model: 'RF24-105mm F4 L IS USM'
ISO Speed: 400
Exposure Time: 1/60
F-Number: f/5.6
IPTC Metadata:
Byline: 'Dr. Sarah Wilson'
Caption: 'Post-surgical wound, day 7, patient 12345678'
Keywords: 'wound, healing, patient care'
Copyright Notice: 'Metropolitan Hospital 2025'
City: 'New York'
Country: 'USA'
XMP Metadata:
Creator: 'Wound Care Team'
Subject: ['medical', 'documentation', 'patient-12345678']
Rights: 'Confidential Medical Record'
Expected Output (Sanitized JPEG):
=== JPEG Processing ===
Original file size: 856,432 bytes
Format: JPEG
Metadata found: 18 fields
=== Sanitization Results ===
EXIF Tags Removed: 8
IPTC Fields Removed: 6
XMP Properties Removed: 4
Technical Tags Preserved: 6
Processing Time: 0.156 seconds
Sanitized Metadata:
Camera Make: [REMOVED]
Camera Model: [REMOVED]
DateTime Original: [REMOVED]
GPS Latitude: [REMOVED]
GPS Longitude: [REMOVED]
Lens Model: [REMOVED]
Byline: [REMOVED]
Caption: [REMOVED]
Keywords: [REMOVED]
Copyright Notice: [REMOVED]
Creator: [REMOVED]
# Technical metadata preserved:
Image Width: 4032 pixels
Image Height: 3024 pixels
Color Space: sRGB
Orientation: 1 (normal)
Resolution: 72 dpi
Compression: JPEG (Quality 92%)
Output file size: 823,145 bytes (3.9% reduction)
TIFF - High-Resolution Pathology
from pymedsec import scrub_image
# Input: High-resolution pathology slide
with open("histology_slide_001.tiff", "rb") as f:
original_tiff = f.read()
result = scrub_image(original_tiff, format_hint="tiff", policy=policy, preserve_technical=True)
Expected Input (TIFF Tags):
File: histology_slide_001.tiff (45,678,234 bytes)
Image Size: 32768x24576 pixels (804 megapixels)
Bit Depth: 24-bit RGB
Compression: LZW
TIFF Tags:
Software: 'Aperio ImageScope v12.4.6'
DateTime: '2025:09:09 14:30:15'
Artist: 'Pathologist Dr. Jane Smith'
Copyright: 'Metro Pathology Lab'
ImageDescription: 'Patient: John Doe, Case: 2025-001234, H&E 20x'
Make: 'Aperio'
Model: 'ScanScope AT2'
DocumentName: 'slide_patient_12345678.svs'
HostComputer: 'PATHOLOGY-WS-001'
# Aperio-specific tags:
Aperio.Filename: 'patient_12345678_slide_001'
Aperio.Date: '09/09/2025'
Aperio.Time: '14:30:15'
Aperio.User: 'jsmith'
Aperio.AppMag: '20'
Aperio.StripeWidth: '2048'
Aperio.ScanScope ID: 'SS1234'
# Technical preservation tags:
XResolution: 0.25 μm/pixel
YResolution: 0.25 μm/pixel
ResolutionUnit: Micrometer
PlanarConfiguration: Chunky
PhotometricInterpretation: RGB
Expected Output (Sanitized TIFF):
=== TIFF Processing ===
Original file size: 45,678,234 bytes
Format: TIFF
Image dimensions: 32768x24576 pixels
Metadata found: 23 fields
=== Sanitization Results ===
Standard TIFF Tags Removed: 8
Aperio-Specific Tags Removed: 7
Technical Tags Preserved: 8
Processing Time: 2.456 seconds
Sanitized Metadata:
Software: [REMOVED]
DateTime: [REMOVED]
Artist: [REMOVED]
Copyright: [REMOVED]
ImageDescription: [REMOVED]
Make: [REMOVED]
Model: [REMOVED]
DocumentName: [REMOVED]
HostComputer: [REMOVED]
# Aperio tags removed:
Aperio.Filename: [REMOVED]
Aperio.Date: [REMOVED]
Aperio.Time: [REMOVED]
Aperio.User: [REMOVED]
Aperio.ScanScope ID: [REMOVED]
# Technical metadata preserved:
Image Width: 32768 pixels
Image Height: 24576 pixels
Bits Per Sample: 8, 8, 8
Compression: LZW
XResolution: 0.25 μm/pixel # Critical for measurements
YResolution: 0.25 μm/pixel # Critical for measurements
ResolutionUnit: Micrometer
PhotometricInterpretation: RGB
Aperio.AppMag: '20' # Magnification preserved
Output file size: 45,456,123 bytes (0.5% reduction)
Multi-format Batch Processing
from pymedsec import scrub_image
from pathlib import Path
# Process multiple image types
image_dir = Path("./medical_images/")
results = {}
for image_file in image_dir.glob("*"):
if image_file.suffix.lower() in ['.dcm', '.png', '.jpg', '.jpeg', '.tiff', '.tif']:
with open(image_file, "rb") as f:
data = f.read()
# Auto-detect format or use extension hint
format_hint = image_file.suffix.lower().replace('.', '')
if format_hint == 'dcm':
result = scrub_dicom(data, policy=policy, pseudo_pid=f"BATCH_{image_file.stem}")
else:
result = scrub_image(data, format_hint=format_hint, policy=policy)
results[image_file.name] = {
'original_size': len(data),
'sanitized_size': len(result.sanitized_data),
'metadata_removed': len(result.removed_metadata),
'format': result.format
}
# Display batch results
for filename, stats in results.items():
print(f"{filename}:")
print(f" Format: {stats['format']}")
print(f" Size: {stats['original_size']:,} → {stats['sanitized_size']:,} bytes")
print(f" Metadata removed: {stats['metadata_removed']} fields")
print(f" Reduction: {((stats['original_size'] - stats['sanitized_size']) / stats['original_size'] * 100):.1f}%")
Expected Batch Output:
mri_brain_001.dcm:
Format: DICOM
Size: 2,847,392 → 2,834,156 bytes
Metadata removed: 23 fields
Reduction: 0.5%
microscopy_sample.png:
Format: PNG
Size: 1,234,567 → 1,198,432 bytes
Metadata removed: 8 fields
Reduction: 2.9%
wound_documentation.jpg:
Format: JPEG
Size: 856,432 → 823,145 bytes
Metadata removed: 18 fields
Reduction: 3.9%
histology_slide_001.tiff:
Format: TIFF
Size: 45,678,234 → 45,456,123 bytes
Metadata removed: 15 fields
Reduction: 0.5%
=== Batch Summary ===
Total files processed: 4
Total original size: 50,616,625 bytes (48.3 MB)
Total sanitized size: 50,311,856 bytes (48.0 MB)
Total metadata fields removed: 64
Average processing time: 0.734 seconds/file
Encryption & KMS Examples
AWS KMS Encryption
from pymedsec import get_kms_client, encrypt_blob
# Setup AWS KMS
kms = get_kms_client("aws",
key_id="alias/medical-images-prod",
region_name="us-east-1")
# Encrypt sanitized data
encrypted_pkg = encrypt_blob(
sanitized_data,
kms_client=kms,
aad={"dataset": "CLINICAL_TRIAL_2025", "modality": "CT", "site": "hospital_01"}
)
print(f"Encryption Algorithm: {encrypted_pkg.header.algorithm}")
print(f"KMS Key: {encrypted_pkg.header.kms_key_ref}")
print(f"Package Size: {len(encrypted_pkg.to_json())} bytes")
print(f"Created: {encrypted_pkg.header.created_at}")
Expected Output:
Encryption Algorithm: AES-256-GCM
KMS Key: arn:aws:kms:us-east-1:123456789012:alias/medical-images-prod
Package Size: 2,856,789 bytes
Created: 2025-09-09T14:30:15.123Z
Wrapped DEK: AQICAHh8sO5...
AAD Context: {"dataset": "CLINICAL_TRIAL_2025", "modality": "CT", "site": "hospital_01"}
HashiCorp Vault KMS
# Setup Vault KMS
kms = get_kms_client("vault",
vault_url="https://vault.company.com:8200",
vault_path="medical/keys/imaging")
# Encrypt with Vault
encrypted_pkg = encrypt_blob(sanitized_data, kms_client=kms)
print(f"Vault Path: {kms.vault_path}")
print(f"Wrapped Key Size: {len(encrypted_pkg.crypto.wrapped_dek)} bytes")
Expected Output:
Vault Path: medical/keys/imaging
Vault Version: v3
Wrapped Key Size: 256 bytes
Encryption Method: transit/encrypt/imaging-key
ML Integration Examples
Secure Dataset Loading
from pymedsec import SecureImageDataset
# Create secure dataset
dataset = SecureImageDataset(
data_dir="./encrypted_scans/",
policy=policy,
kms_client=kms,
cache_size=100 # Cache 100 decrypted images
)
print(f"Dataset size: {len(dataset)} images")
print(f"Memory usage: {dataset.memory_usage_mb} MB")
print(f"Cache hit ratio: {dataset.cache_hit_ratio:.2%}")
# Load first sample
tensor, metadata = dataset[0]
print(f"Tensor shape: {tensor.shape}")
print(f"Tensor dtype: {tensor.dtype}")
print(f"Value range: [{tensor.min():.3f}, {tensor.max():.3f}]")
Expected Output:
Dataset size: 1,247 images
Memory usage: 234.5 MB
Cache hit ratio: 78.5%
Tensor shape: torch.Size([1, 512, 512])
Tensor dtype: torch.float32
Value range: [0.000, 1.000]
Metadata: {'study_id': 'STUDY001', 'modality': 'CT', 'slice_index': 45}
Audit Logging Examples
Basic Audit Operations
from pymedsec.audit import AuditLogger
# Initialize audit logger
logger = AuditLogger(audit_path="/var/log/pymedsec/audit.jsonl")
# Log operations
logger.log_operation("SANITIZE",
outcome="success",
file_hash="sha256:abc123...",
tags_removed=23,
policy="hipaa_default")
logger.log_operation("ENCRYPT",
outcome="success",
kms_key="alias/medical-images",
algorithm="AES-256-GCM")
Expected Audit Log Entries:
{"timestamp": "2025-09-09T14:30:15.123Z", "actor": "radiologist@hospital.com", "operation": "SANITIZE", "outcome": "success", "file_hash": "sha256:abc123...", "tags_removed": 23, "policy": "hipaa_default", "signature": "hmac_sha256:def456..."}
{"timestamp": "2025-09-09T14:30:16.456Z", "actor": "radiologist@hospital.com", "operation": "ENCRYPT", "outcome": "success", "kms_key": "alias/medical-images", "algorithm": "AES-256-GCM", "signature": "hmac_sha256:ghi789..."}
Blockchain Anchored Audit
# Enable blockchain anchoring
import os
import json
os.environ['IMGSEC_BLOCKCHAIN_BACKEND'] = 'ethereum'
os.environ['IMGSEC_ETHEREUM_RPC_URL'] = 'https://mainnet.infura.io/v3/YOUR_KEY'
logger = AuditLogger(audit_path="/var/log/audit_blockchain.jsonl")
# Every 1000 operations, an anchor is created
for i in range(1000):
logger.log_operation("DECRYPT", outcome="success", access_purpose="ml_training")
# Inspect latest blockchain anchor record
with open(logger.log_file, "r", encoding="utf-8") as f:
last_entry = json.loads(f.readlines()[-1])
anchor = last_entry.get("blockchain_anchor", {})
print(f"Last anchor hash: {logger.last_anchor_hash}")
print(f"Blockchain txn: {anchor.get('tx_hash')}")
Expected Output:
Last anchor hash: sha256:blockchain_anchor_abc123...
Blockchain txn: 0x1234567890abcdef...
Anchor block: 18,456,789
Gas used: 21,000
Verification status: CONFIRMED
CLI Usage with Expected Outputs
Sanitization Command
pymedsec sanitize-cmd \
--input patient_001.dcm \
--output clean_001.dcm \
--pseudo-pid STUDY001_001 \
--policy hipaa_default \
--verbose
Expected CLI Output:
PyMedSec Medical Image Sanitizer
=====================================
Input: patient_001.dcm (2.85 MB)
Policy: HIPAA Default Policy
Pseudo PID: STUDY001_001
Sanitization Progress:
Loaded DICOM dataset (512x512x16bit)
Removed 23 PHI tags
Regenerated 4 UIDs
Preserved 156 technical tags
Applied date shifting (+45 days)
Detected burned-in annotation (handled)
Output: clean_001.dcm (2.83 MB)
Size reduction: 1.2%
Compliance hash: sha256:f4a7b2c9...
Processing time: 0.234 seconds
Sanitization completed successfully.
Encryption Command
pymedsec encrypt \
--input clean_001.dcm \
--output secure_001.enc \
--kms-backend aws \
--key-id alias/medical-images \
--dataset-id CLINICAL_TRIAL_2025 \
--verbose
Expected CLI Output:
PyMedSec Medical Image Encryptor
====================================
Input: clean_001.dcm (2.83 MB)
KMS: AWS KMS (us-east-1)
Key: alias/medical-images
Dataset: CLINICAL_TRIAL_2025
Encryption Progress:
Generated 256-bit data key
Encrypted with AES-256-GCM
Wrapped DEK with KMS
Created tamper-evident package
Logged to audit trail
Output: secure_001.enc (2.86 MB)
Package hash: sha256:1a2b3c4d...
Audit entry: 2025-09-09T14:30:15.123Z
Processing time: 0.892 seconds
Encryption completed successfully.
Policy Comparison Table
| Feature | HIPAA Default | GDPR Default | GxP Default | Custom Lab |
|---|---|---|---|---|
| PHI Removal | Complete | Complete | Complete | Selective |
| UID Regeneration | Yes | Yes | Yes | No |
| Date Shifting | 90 days | 90 days | Preserve | 30 days |
| Technical Tags | Preserve | Preserve | Preserve | Preserve |
| Audit Retention | 7 years | 6 years | 15 years | 2 years |
| Encryption | AES-256-GCM | AES-256-GCM | AES-256-GCM | AES-256-GCM |
| Blockchain | Optional | Optional | Required | Disabled |
| OCR Redaction | Strict | Moderate | Strict | Disabled |
| Validation | Standard | Standard | Enhanced | Minimal |
Error Handling Examples
Invalid Policy
try:
policy = load_policy("invalid_policy")
except PolicyNotFoundError as e:
print(f"Error: {e}")
print("Available policies:", list_policies())
Expected Output:
Error: Policy 'invalid_policy' not found
Available policies: ['hipaa_default', 'gdpr_default', 'gxp_default']
KMS Access Error
try:
kms = get_kms_client("aws", key_id="invalid-key")
encrypt_blob(data, kms_client=kms)
except KMSAccessError as e:
print(f"KMS Error: {e}")
print("Check IAM permissions and key existence")
Expected Output:
KMS Error: Access denied to key 'invalid-key'
Check IAM permissions and key existence
Suggested actions:
1. Verify key alias/ARN is correct
2. Check IAM role has kms:Encrypt permission
3. Ensure key is enabled and not deleted
Configuration
PyMedSec uses environment variables and YAML configuration files for flexible deployment.
Environment Variables
| Variable | Description | Default | Required |
|---|---|---|---|
IMGSEC_POLICY |
Path to YAML policy file | - | Yes |
IMGSEC_KMS_BACKEND |
KMS backend (aws|vault|mock) |
mock |
Yes |
IMGSEC_KMS_KEY_REF |
KMS key identifier | - | Yes |
IMGSEC_AUDIT_PATH |
Audit log file path | ./audit.jsonl |
- |
IMGSEC_DEBUG |
Enable debug logging | false |
- |
IMGSEC_NO_PLAINTEXT_DISK |
Forbid plaintext disk writes | false |
- |
IMGSEC_BLOCKCHAIN_BACKEND |
Blockchain backend (mock|ethereum|hyperledger|disabled) |
disabled |
- |
IMGSEC_BLOCKCHAIN_FREQUENCY |
Anchor cadence (every|batch_hourly) |
every |
- |
Policy Configuration
Create a YAML policy file to define security and compliance requirements:
# /etc/pymedsec/hipaa_policy.yaml
schema_version: '1.0'
name: 'HIPAA Compliance Policy'
description: 'Enterprise HIPAA-compliant policy for medical imaging'
sanitization:
dicom:
remove_private_tags: true
regenerate_uids: true
preserve_technical_tags: true
phi_tags_action: 'remove' # remove, replace, or pseudonymize
burned_in_annotation_policy: 'strict' # strict, moderate, or permissive
exif:
strip_all_metadata: true
preserve_orientation: false
preserve_color_space: true
encryption:
algorithm: 'AES-256-GCM'
key_rotation_days: 90
require_kms: true
additional_authenticated_data: ['dataset_id', 'modality', 'timestamp']
audit:
log_all_operations: true
include_file_hashes: true
blockchain_anchoring: false
retention_days: 2557 # 7 years for HIPAA
compliance:
framework: 'hipaa' # hipaa, gdpr, gxp
purpose_limitation: 'medical_research'
data_minimization: true
pseudonymization_required: true
Security Model
Envelope Encryption
PyMedSec uses industry-standard envelope encryption to protect medical images:
- Data Encryption Key (DEK) Generation: Generate a random 256-bit AES key for each image
- Image Encryption: Encrypt the medical image using AES-256-GCM with the DEK
- Key Wrapping: Encrypt the DEK using the master key in KMS/HSM
- Package Creation: Combine encrypted image + wrapped DEK + metadata in tamper-evident package
┌─────────────────────────────────────────────────────────────────┐
│ Encrypted Package Structure │
├─────────────────────────────────────────────────────────────────┤
│ Header: │
│ ├─ version: "1.0" │
│ ├─ algorithm: "AES-256-GCM" │
│ ├─ kms_key_ref: "arn:aws:kms:us-east-1:123:key/abc123" │
│ └─ created_at: "2025-09-09T10:30:00Z" │
├─────────────────────────────────────────────────────────────────┤
│ Crypto: │
│ ├─ wrapped_dek: "AQICAHh...encrypted_key" │
│ ├─ iv: "12_byte_initialization_vector" │
│ ├─ auth_tag: "16_byte_authentication_tag" │
│ └─ aad: {"dataset": "study1", "modality": "CT"} │
├─────────────────────────────────────────────────────────────────┤
│ Data: │
│ └─ ciphertext: "encrypted_medical_image_data" │
├─────────────────────────────────────────────────────────────────┤
│ Integrity: │
│ ├─ package_hash: "sha256:package_content_hash" │
│ ├─ signature: "hmac_sha256_signature" │
│ └─ audit_ref: "audit_log_entry_reference" │
└─────────────────────────────────────────────────────────────────┘
PHI Sanitization
Comprehensive removal and pseudonymization of Protected Health Information:
DICOM Tags Handling
# Automatic PHI tag removal based on DICOM standard
PHI_TAGS = [
(0x0010, 0x0010), # Patient's Name
(0x0010, 0x0020), # Patient ID
(0x0010, 0x0030), # Patient's Birth Date
(0x0010, 0x1040), # Patient's Address
(0x0008, 0x0080), # Institution Name
(0x0008, 0x0090), # Referring Physician's Name
# ... 100+ additional PHI tags
]
# Technical tags preserved for medical utility
TECHNICAL_TAGS = [
(0x0018, 0x0050), # Slice Thickness
(0x0018, 0x0088), # Spacing Between Slices
(0x0020, 0x0032), # Image Position Patient
(0x0028, 0x0030), # Pixel Spacing
# ... imaging parameters
]
Audit Trail
Tamper-evident audit logging with HMAC signatures and optional blockchain anchoring:
{"timestamp": "2025-09-09T10:30:15.123Z", "actor": "radiologist@hospital.com", "operation": "ENCRYPT", "outcome": "success", "file_hash": "sha256:abc123", "kms_key": "alias/medical", "signature": "hmac_sha256_sig"}
{"timestamp": "2025-09-09T10:31:22.456Z", "actor": "ml-pipeline", "operation": "DECRYPT", "outcome": "success", "access_purpose": "model_training", "signature": "hmac_sha256_sig"}
Compliance
HIPAA Compliance
PyMedSec addresses HIPAA Security Rule requirements:
- 164.312(a)(1) - Access Control: KMS-based access control with audit logging
- 164.312(a)(2)(i) - Unique User Identification: Actor tracking in audit logs
- 164.312(b) - Audit Controls: Comprehensive tamper-evident audit trail
- 164.312(c)(1) - Integrity: HMAC signatures and hash verification
- 164.312(d) - Person or Entity Authentication: KMS authentication
- 164.312(e)(1) - Transmission Security: Envelope encryption for data in transit
GDPR Compliance
- Article 25 - Data Protection by Design: Privacy-preserving architecture
- Article 32 - Security of Processing: AES-256-GCM encryption and access controls
- Article 35 - Data Protection Impact Assessment: Validation documentation provided
- Article 17 - Right to Erasure: Secure deletion capabilities
FDA/GxP Compliance
- 21 CFR Part 11 - Electronic Records: Tamper-evident audit trail and electronic signatures
- CLIA - Clinical Laboratory Standards: Quality controls and traceability
Performance
Benchmarks
| Operation | Image Size | Throughput | Memory Usage |
|---|---|---|---|
| DICOM Encryption | 512x512x16bit | 45 MB/s | 128 MB |
| DICOM Decryption | 512x512x16bit | 52 MB/s | 96 MB |
| PHI Sanitization | 1024 tags | 1,200 images/s | 64 MB |
| Tensor Conversion | 512x512 DICOM | 890 images/s | 32 MB |
Benchmarks on AWS c5.2xlarge (8 vCPU, 16 GB RAM)
Scalability
- Horizontal Scaling: Stateless design enables easy horizontal scaling
- Cloud Native: Native integration with AWS, Azure, and GCP KMS services
- Memory Efficient: Zero-copy operations and automatic cleanup
- Batch Processing: Optimized for large-scale medical imaging pipelines
Examples
Healthcare Research Pipeline
import pymedsec
from pathlib import Path
# Setup
policy = pymedsec.load_policy("hipaa_default")
kms = pymedsec.get_kms_client("aws", key_id="alias/research-images")
# Process a batch of DICOM files
for dicom_file in Path("./raw_scans/").glob("*.dcm"):
# Sanitize and encrypt
with open(dicom_file, "rb") as f:
clean_data = pymedsec.scrub_dicom(
f.read(),
policy=policy,
pseudo_pid=f"STUDY001_{dicom_file.stem}"
)
encrypted_pkg = pymedsec.encrypt_blob(
clean_data,
kms_client=kms,
aad={"study": "TRIAL001", "patient": dicom_file.stem}
)
# Save encrypted version
output_file = Path("./secure_scans/") / f"{dicom_file.stem}.enc"
with open(output_file, "w") as f:
f.write(encrypted_pkg.to_json())
ML Training with SecureImageDataset
import torch
from torch.utils.data import DataLoader
from pymedsec import SecureImageDataset, load_policy, get_kms_client
# Setup secure dataset
policy = load_policy("research_policy.yaml")
kms = get_kms_client("aws", key_id="alias/ml-training")
dataset = SecureImageDataset(
data_dir="./encrypted_training_data/",
policy=policy,
kms_client=kms,
transform=torch.transforms.Compose([
torch.transforms.Resize((224, 224)),
torch.transforms.ToTensor(),
torch.transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
])
)
# Train your model
dataloader = DataLoader(dataset, batch_size=32, shuffle=True, num_workers=4)
for epoch in range(10):
for batch_idx, (images, metadata) in enumerate(dataloader):
# Forward pass
outputs = model(images)
loss = criterion(outputs, targets)
# Backward pass
optimizer.zero_grad()
loss.backward()
optimizer.step()
# Images automatically cleared from memory after batch
Command Line Interface
PyMedSec provides CLI commands for batch processing and operations:
# Set up environment
export IMGSEC_POLICY=/etc/pymedsec/hipaa_policy.yaml
export IMGSEC_KMS_BACKEND=aws
export IMGSEC_KMS_KEY_REF=alias/medical-images
export IMGSEC_AUDIT_PATH=/var/log/pymedsec/audit.jsonl
# Sanitize a DICOM file
pymedsec sanitize-cmd \
--input patient_001.dcm \
--output clean_001.dcm \
--pseudo-pid STUDY001_001 \
--format dicom
# Encrypt sanitized image
pymedsec encrypt \
--input clean_001.dcm \
--output secure_001.enc \
--dataset-id CLINICAL_TRIAL_2025 \
--modality CT \
--additional-data '{"site": "hospital_a", "protocol": "v2.1"}'
# Batch processing with parallel workers
pymedsec encrypt \
--input-dir ./sanitized_scans/ \
--output-dir ./encrypted_scans/ \
--workers 8 \
--dataset-id BATCH_PROCESS_001
# Decrypt for analysis
pymedsec decrypt \
--input secure_001.enc \
--output analysis_001.dcm \
--memory-only # Decrypt to memory only, no disk write
# Verify package integrity
pymedsec verify --input secure_001.enc --verbose
# Audit operations
pymedsec audit-log --last 100 --format table
pymedsec audit-status --blockchain
pymedsec audit-verify --start-date 2025-09-01 --end-date 2025-09-09
Development
Setting Up Development Environment
# Clone repository
git clone https://github.com/Faerque/pymedsec.git
cd pymedsec
# Create virtual environment
python -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
# Install in development mode
pip install -e ".[dev,aws,vault,ocr]"
# Install pre-commit hooks
pre-commit install
Running Tests
# Run all tests
make test
# Run with coverage
make test-cov
# Run specific test file
python -m pytest tests/test_crypto.py -v
# Run integration tests
make test-integration
Code Quality
# Format code
make fmt
# Lint code
make lint
# Type checking
make type-check
# Security scanning
make security-scan
Building Documentation
# Build docs locally
make docs
# Serve docs locally
make docs-serve
# Build for deployment
make docs-build
Deployment
Production Deployment Checklist
- KMS Setup: Configure AWS KMS or HashiCorp Vault with proper IAM roles
- Policy Configuration: Create and validate security policies for your environment
- Environment Variables: Set all required environment variables securely
- Audit Logging: Configure persistent audit log storage with proper rotation
- Monitoring: Set up monitoring for encryption/decryption operations and errors
- Backup: Implement backup strategy for encrypted data and audit logs
- Key Rotation: Establish key rotation procedures and schedules
- Incident Response: Create incident response procedures for security events
Docker Deployment
FROM python:3.11-slim
# Install system dependencies
RUN apt-get update && apt-get install -y \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Install PyMedSec
RUN pip install pymedsec[aws,vault]
# Copy configuration
COPY policies/ /etc/pymedsec/policies/
COPY config.yaml /etc/pymedsec/config.yaml
# Set environment variables
ENV IMGSEC_POLICY=/etc/pymedsec/policies/production.yaml
ENV IMGSEC_KMS_BACKEND=aws
ENV IMGSEC_AUDIT_PATH=/var/log/pymedsec/audit.jsonl
# Create non-root user
RUN useradd -m pymedsec
USER pymedsec
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD pymedsec --config-check || exit 1
ENTRYPOINT ["pymedsec"]
Kubernetes Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: pymedsec-processor
spec:
replicas: 3
selector:
matchLabels:
app: pymedsec-processor
template:
metadata:
labels:
app: pymedsec-processor
spec:
serviceAccountName: pymedsec-service-account
containers:
- name: pymedsec
image: your-registry/pymedsec:latest
env:
- name: IMGSEC_POLICY
value: /etc/config/policy.yaml
- name: IMGSEC_KMS_BACKEND
value: aws
- name: IMGSEC_KMS_KEY_REF
valueFrom:
secretKeyRef:
name: kms-config
key: key-id
volumeMounts:
- name: config-volume
mountPath: /etc/config
- name: audit-volume
mountPath: /var/log/pymedsec
resources:
requests:
memory: '256Mi'
cpu: '100m'
limits:
memory: '1Gi'
cpu: '500m'
volumes:
- name: config-volume
configMap:
name: pymedsec-config
- name: audit-volume
persistentVolumeClaim:
claimName: audit-storage
Additional Resources
Documentation
- API Reference
- Security Architecture
- HIPAA Compliance Guide
- GDPR Compliance Guide
- GxP/CLIA Alignment
- Validation & Traceability
Community
- GitHub Issues - Bug reports and feature requests
- GitHub Discussions - Questions and discussions
- Security Policy - Security vulnerability reporting
Related Projects
- PyDICOM - DICOM file handling in Python
- SimpleITK - Medical image analysis toolkit
- MONAI - Medical imaging AI framework
- OHIF Viewer - Web-based medical imaging viewer
Legal & Compliance
License
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Compliance Disclaimer
Important: PyMedSec provides tools that can support HIPAA, GDPR, and GxP compliance but does not by itself ensure compliance. Compliance depends on your deployment environment, policies, procedures, and governance. Organizations must implement appropriate administrative, physical, and technical safeguards according to applicable regulations and their specific use cases.
Security Vulnerability Reporting
If you discover a security vulnerability, please report it privately:
- Private Report: GitHub Private Vulnerability Reporting
- Do NOT create public GitHub issues for security vulnerabilities
- Include: Description, reproduction steps, and potential impact
- Response: We aim to acknowledge reports within 24 hours
Export Control
This software may be subject to export controls. Users are responsible for compliance with applicable export control laws and regulations.
For the healthcare community
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pymedsec-0.2.0.tar.gz.
File metadata
- Download URL: pymedsec-0.2.0.tar.gz
- Upload date:
- Size: 166.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8ff2d5de4914ae2f680c3bc3e05bd73e1d7eca248a9b69560bb4835e7776b72c
|
|
| MD5 |
a9b422c63546912e119c6e9abe52ef22
|
|
| BLAKE2b-256 |
fc3937c85f6ea9464e6743b357d72176aec052bfef5b23bf5d489c73e9f55d56
|
File details
Details for the file pymedsec-0.2.0-py3-none-any.whl.
File metadata
- Download URL: pymedsec-0.2.0-py3-none-any.whl
- Upload date:
- Size: 83.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b713aeed566e94a9f64d8d8d21d9126a857a8cc29b8a261f3d4dc4f228475201
|
|
| MD5 |
eca9d6457efaf1f6e0a32bf4f96bd0ae
|
|
| BLAKE2b-256 |
237f251a2dd286975b317c26d71d2bf2c63fe77e68f479807a58bc5f45d22947
|