Commit c4a4fc12 authored by Eric Duminil's avatar Eric Duminil
Browse files

Dataclass from Claude

parent 45363a95
#!/usr/bin/env python3
"""
CityGML dataclass for German federal states building data.
"""
import hashlib
import os
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Callable
from urllib.error import URLError
from urllib.request import urlopen
def download_file(url: str, dest_path: Path, chunk_size: int = 8192) -> bool:
"""Download file from URL to destination path."""
try:
with urlopen(url, timeout=30) as response:
dest_path.parent.mkdir(parents=True, exist_ok=True)
with open(dest_path, "wb") as f:
while True:
chunk = response.read(chunk_size)
if not chunk:
break
f.write(chunk)
return True
except (URLError, OSError) as e:
print(f" Error downloading from {url}: {e}")
return False
@dataclass
class CityGML(ABC):
"""
Represents a CityGML file with metadata and download capabilities.
All properties must be defined - no optionals.
Subclasses or instances must implement the verify method.
"""
url: str
path: Path
coordinate_reference_system: str
source: str
bundesland: str
@property
def filename(self) -> str:
"""Extract filename from URL."""
return self.url.split("/")[-1]
def size(self) -> int:
"""Calculate file size if path exists, otherwise return 0."""
if self.path.exists():
return os.path.getsize(self.path)
return 0
def sha256(self) -> str:
"""Calculate SHA-256 hash if path exists, otherwise return empty string."""
if not self.path.exists():
return ""
sha256_hash = hashlib.sha256()
with open(self.path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
sha256_hash.update(chunk)
return sha256_hash.hexdigest()
def date(self) -> datetime:
"""Get file modification datetime if path exists, otherwise return epoch."""
if self.path.exists():
return datetime.fromtimestamp(os.path.getmtime(self.path))
return datetime.fromtimestamp(0)
@abstractmethod
def verify(self, file_path: Path) -> bool:
"""
Verify that the downloaded file is valid.
Args:
file_path: Path to the file to verify
Returns:
True if file is valid, False otherwise
"""
pass
def is_up_to_date(self) -> bool:
"""Check if file exists and is up to date."""
if not self.path.exists():
return False
if self.size() == 0:
return False
return self.verify(self.path)
def download(self, tmp_dir: Path, max_retries: int = 3) -> bool:
"""
Download and verify the file.
Args:
tmp_dir: Temporary directory for downloads
max_retries: Maximum number of retry attempts
Returns:
True if download and verification successful, False otherwise
"""
# Check if already up to date
if self.is_up_to_date():
print(f"✓ {self.filename} already downloaded and verified")
return True
# Remove old file if it exists but is invalid
if self.path.exists():
print(f" Existing file is invalid, will re-download")
self.path.unlink()
tmp_path = tmp_dir / self.filename
for attempt in range(max_retries):
print(f"Downloading {self.filename} (attempt {attempt + 1}/{max_retries})")
print(f" Trying {self.url}")
# Try to download
if not download_file(self.url, tmp_path):
print(f" Failed to download")
if tmp_path.exists():
tmp_path.unlink()
continue
# Verify the downloaded file
print(f" Verifying {self.filename}")
if self.verify(tmp_path):
# Move to final location
self.path.parent.mkdir(parents=True, exist_ok=True)
tmp_path.rename(self.path)
print(f"✓ {self.filename} downloaded and verified successfully")
return True
else:
print(f" Verification failed")
if tmp_path.exists():
tmp_path.unlink()
print(f"✗ Failed to download {self.filename} after {max_retries} attempts")
return False
@dataclass
class CityGMLWithHash(CityGML):
"""CityGML file verified by SHA-256 hash."""
expected_sha256: str = ""
def verify(self, file_path: Path) -> bool:
"""Verify file using SHA-256 hash."""
# Calculate hash for the file to verify
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
for chunk in iter(lambda: f.read(8192), b""):
sha256_hash.update(chunk)
actual_hash = sha256_hash.hexdigest()
if actual_hash != self.expected_sha256:
print(f" Hash mismatch: expected {self.expected_sha256}, got {actual_hash}")
return False
return True
@dataclass
class CityGMLWithSize(CityGML):
"""CityGML file verified by file size."""
expected_size: int = 0
def verify(self, file_path: Path) -> bool:
"""Verify file using size."""
actual_size = os.path.getsize(file_path)
if actual_size != self.expected_size:
print(f" Size mismatch: expected {self.expected_size}, got {actual_size}")
return False
return True
@dataclass
class CityGMLWithDate(CityGML):
"""CityGML file verified by modification date (file must be newer than source)."""
source_date: datetime = datetime.fromtimestamp(0)
def verify(self, file_path: Path) -> bool:
"""Verify file is newer than the source date and non-empty."""
file_mtime = datetime.fromtimestamp(os.path.getmtime(file_path))
if file_mtime <= self.source_date:
print(f" File too old: {file_mtime} <= {self.source_date}")
return False
return True
@dataclass
class CityGMLWithCustomVerify(CityGML):
"""
CityGML file with custom verification function.
The verify_func should be set after instantiation:
obj = CityGMLWithCustomVerify(...)
obj.verify_func = lambda path: os.path.getsize(path) > 1000
"""
verify_func: Callable[[Path], bool] = lambda path: True
def verify(self, file_path: Path) -> bool:
"""Verify file using custom function."""
return self.verify_func(file_path)
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment