| import os | |
| import shutil | |
| import uuid | |
| from django.conf import settings | |
| from django.db import models | |
| from django.utils import timezone | |
| from jobscheduler.models import Job | |
| class ModelVersion(models.Model): | |
| STORAGE_TIERS = [ | |
| ("hot", "Hot (Local)"), | |
| ("warm", "Warm (Cloud)"), | |
| ("cold", "Cold (Archive)"), | |
| ] | |
| id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) | |
| run = models.ForeignKey( | |
| Job, on_delete=models.CASCADE, related_name="versioning_model_versions" | |
| ) | |
| version = models.CharField(max_length=100) | |
| version_number = models.IntegerField() | |
| artifact_path = models.CharField(max_length=500) | |
| compressed_artifact_path = models.CharField(max_length=500, blank=True) | |
| compression_method = models.CharField(max_length=100, blank=True) | |
| compression_config = models.JSONField(default=dict, blank=True) | |
| artifact_size_mb = models.FloatField(default=0.0) | |
| compressed_size_mb = models.FloatField(default=0.0) | |
| storage_tier = models.CharField(max_length=20, choices=STORAGE_TIERS, default="hot") | |
| is_published_to_hub = models.BooleanField(default=False) | |
| hub_repo_url = models.URLField(max_length=500, blank=True, null=True) | |
| metrics = models.JSONField(default=dict) | |
| git_commit = models.CharField(max_length=40, null=True, blank=True) | |
| config_snapshot_id = models.UUIDField(null=True, blank=True) | |
| validation_passed = models.BooleanField(default=False) | |
| is_production = models.BooleanField(default=False) | |
| created_at = models.DateTimeField(auto_now_add=True) | |
| updated_at = models.DateTimeField(auto_now=True) | |
| class Meta: | |
| ordering = ["-version_number"] | |
| unique_together = [["run", "version_number"]] | |
| indexes = [ | |
| models.Index(fields=["run", "-version_number"]), | |
| models.Index(fields=["is_published_to_hub"]), | |
| models.Index(fields=["storage_tier"]), | |
| ] | |
| def __str__(self): | |
| return f"v{self.version_number} ({self.version})" | |
| def delete_local_artifact(self): | |
| if os.path.exists(self.artifact_path): | |
| shutil.rmtree(self.artifact_path) | |
| if self.compressed_artifact_path and os.path.exists( | |
| self.compressed_artifact_path | |
| ): | |
| os.remove(self.compressed_artifact_path) | |
| def archive_to_cold_storage(self, bucket_name: str): | |
| """ | |
| Move the artifact (and compressed artifact) to an S3 bucket, | |
| update storage tier to 'cold', and remove local copies. | |
| """ | |
| import boto3 | |
| s3 = boto3.client("s3") | |
| # Upload original artifact directory as a tar.gz | |
| import io | |
| import os | |
| import tarfile | |
| tar_buffer = io.BytesIO() | |
| with tarfile.open(fileobj=tar_buffer, mode="w:gz") as tar: | |
| tar.add(self.artifact_path, arcname=os.path.basename(self.artifact_path)) | |
| tar_buffer.seek(0) | |
| key = f"run_{self.run_id}/v{self.version_number}/artifact.tar.gz" | |
| s3.upload_fileobj(tar_buffer, bucket_name, key) | |
| # If compressed artifact exists and is separate, also upload it | |
| if self.compressed_artifact_path and os.path.exists( | |
| self.compressed_artifact_path | |
| ): | |
| comp_key = f"run_{self.run_id}/v{self.version_number}/{os.path.basename(self.compressed_artifact_path)}" | |
| with open(self.compressed_artifact_path, "rb") as f: | |
| s3.upload_fileobj(f, bucket_name, comp_key) | |
| # Delete local artifacts | |
| self.delete_local_artifact() | |
| self.storage_tier = "cold" | |
| self.save(update_fields=["storage_tier"]) | |
| def restore_from_cold_storage(self, bucket_name: str): | |
| """ | |
| Download the artifact from cold storage (S3) back to local disk, | |
| set storage tier to 'hot'. | |
| """ | |
| import os | |
| import boto3 | |
| s3 = boto3.client("s3") | |
| # Restore the original artifact | |
| artifact_key = f"run_{self.run_id}/v{self.version_number}/artifact.tar.gz" | |
| local_tar = os.path.join(self.artifact_path, "artifact.tar.gz") | |
| os.makedirs(self.artifact_path, exist_ok=True) | |
| s3.download_file(bucket_name, artifact_key, local_tar) | |
| # Extract | |
| import tarfile | |
| with tarfile.open(local_tar, "r:gz") as tar: | |
| tar.extractall(self.artifact_path) | |
| os.remove(local_tar) | |
| # Restore compressed artifact if it existed | |
| if self.compressed_artifact_path: | |
| comp_filename = os.path.basename(self.compressed_artifact_path) | |
| comp_key = f"run_{self.run_id}/v{self.version_number}/{comp_filename}" | |
| try: | |
| s3.download_file(bucket_name, comp_key, self.compressed_artifact_path) | |
| except Exception: | |
| pass # compressed artifact may be missing, ignore | |
| self.storage_tier = "hot" | |
| self.save(update_fields=["storage_tier"]) | |
| class ModelVersionDiff(models.Model): | |
| id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) | |
| from_version = models.ForeignKey( | |
| ModelVersion, on_delete=models.CASCADE, related_name="diffs_from" | |
| ) | |
| to_version = models.ForeignKey( | |
| ModelVersion, on_delete=models.CASCADE, related_name="diffs_to" | |
| ) | |
| diff_artifact = models.CharField(max_length=500) | |
| diff_size_mb = models.FloatField(default=0.0) | |
| original_size_saved_mb = models.FloatField(default=0.0) | |
| created_at = models.DateTimeField(auto_now_add=True) | |
| class Meta: | |
| unique_together = [["from_version", "to_version"]] | |
| def __str__(self): | |
| return f"Diff v{self.from_version.version_number}→v{self.to_version.version_number}" | |
| class ModelLineage(models.Model): | |
| """Tracks parent‑child relationships between model versions.""" | |
| id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) | |
| parent = models.ForeignKey( | |
| ModelVersion, | |
| on_delete=models.CASCADE, | |
| related_name="children", | |
| null=True, | |
| blank=True, | |
| ) | |
| child = models.ForeignKey( | |
| ModelVersion, | |
| on_delete=models.CASCADE, | |
| related_name="parents", | |
| ) | |
| relationship_type = models.CharField( | |
| max_length=50, | |
| default="fine_tuned", | |
| choices=[ | |
| ("fine_tuned", "Fine-Tuned"), | |
| ("compressed", "Compressed"), | |
| ("quantized", "Quantized"), | |
| ("distilled", "Distilled"), | |
| ("merged", "Merged"), | |
| ("continued", "Continued Training"), | |
| ], | |
| ) | |
| notes = models.TextField(blank=True) | |
| created_at = models.DateTimeField(auto_now_add=True) | |
| class Meta: | |
| unique_together = [["parent", "child", "relationship_type"]] | |
| def __str__(self): | |
| return f"v{self.parent.version_number} → v{self.child.version_number} ({self.relationship_type})" | |
| def get_lineage_graph( | |
| model_version_id: str, direction: str = "both", max_depth: int = 5 | |
| ): | |
| """ | |
| Return a clean lineage graph for a model version. | |
| direction: 'ancestors', 'descendants', or 'both' | |
| """ | |
| visited = set() | |
| nodes = [] | |
| edges = [] | |
| def traverse_ancestors(current_id, depth=0): | |
| if depth > max_depth or current_id in visited: | |
| return | |
| visited.add(current_id) | |
| try: | |
| mv = ModelVersion.objects.get(id=current_id) | |
| except ModelVersion.DoesNotExist: | |
| return | |
| if not any(n["id"] == str(mv.id) for n in nodes): | |
| nodes.append( | |
| { | |
| "id": str(mv.id), | |
| "version": mv.version, | |
| "version_number": mv.version_number, | |
| "metrics": mv.metrics, | |
| "created_at": mv.created_at.isoformat(), | |
| "depth": depth, | |
| } | |
| ) | |
| links = ModelLineage.objects.filter(child_id=current_id).select_related( | |
| "parent" | |
| ) | |
| for link in links: | |
| parent_id = str(link.parent_id) | |
| edges.append( | |
| { | |
| "from": parent_id, | |
| "to": str(current_id), | |
| "type": link.relationship_type, | |
| } | |
| ) | |
| traverse_ancestors(parent_id, depth + 1) | |
| def traverse_descendants(current_id, depth=0): | |
| if depth > max_depth or current_id in visited: | |
| return | |
| visited.add(current_id) | |
| try: | |
| mv = ModelVersion.objects.get(id=current_id) | |
| except ModelVersion.DoesNotExist: | |
| return | |
| if not any(n["id"] == str(mv.id) for n in nodes): | |
| nodes.append( | |
| { | |
| "id": str(mv.id), | |
| "version": mv.version, | |
| "version_number": mv.version_number, | |
| "metrics": mv.metrics, | |
| "created_at": mv.created_at.isoformat(), | |
| "depth": depth, | |
| } | |
| ) | |
| links = ModelLineage.objects.filter(parent_id=current_id).select_related( | |
| "child" | |
| ) | |
| for link in links: | |
| child_id = str(link.child_id) | |
| edges.append( | |
| { | |
| "from": str(current_id), | |
| "to": child_id, | |
| "type": link.relationship_type, | |
| } | |
| ) | |
| traverse_descendants(child_id, depth + 1) | |
| if direction in ("ancestors", "both"): | |
| visited.clear() | |
| traverse_ancestors(model_version_id) | |
| if direction in ("descendants", "both"): | |
| visited.clear() | |
| traverse_descendants(model_version_id) | |
| return {"nodes": nodes, "edges": edges} | |
| # model_versioning/models.py — add these classes after the existing ModelLineage class | |
| class ModelApproval(models.Model): | |
| """ | |
| Governance approval workflow for a model version. | |
| pending → approved → rejected (or back to pending for re‑review) | |
| """ | |
| STATUS_CHOICES = [ | |
| ("pending", "Pending Review"), | |
| ("in_review", "In Review"), | |
| ("approved", "Approved"), | |
| ("rejected", "Rejected"), | |
| ("changes_requested", "Changes Requested"), | |
| ] | |
| id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) | |
| version = models.OneToOneField( | |
| ModelVersion, on_delete=models.CASCADE, related_name="approval" | |
| ) | |
| status = models.CharField(max_length=30, choices=STATUS_CHOICES, default="pending") | |
| requested_by = models.ForeignKey( | |
| settings.AUTH_USER_MODEL, | |
| on_delete=models.CASCADE, | |
| related_name="versioning_approval_requests", | |
| ) | |
| reviewer = models.ForeignKey( | |
| settings.AUTH_USER_MODEL, | |
| on_delete=models.SET_NULL, | |
| null=True, | |
| blank=True, | |
| related_name="approval_reviews", | |
| ) | |
| notes = models.TextField(blank=True) | |
| review_notes = models.TextField(blank=True) | |
| created_at = models.DateTimeField(auto_now_add=True) | |
| updated_at = models.DateTimeField(auto_now=True) | |
| class Meta: | |
| ordering = ["-created_at"] | |
| def __str__(self): | |
| return f"Approval for {self.version}: {self.status}" | |
| def approve(self, reviewer, notes=""): | |
| self.status = "approved" | |
| self.reviewer = reviewer | |
| self.review_notes = notes | |
| self.save() | |
| self.version.is_published_to_hub = True | |
| self.version.save(update_fields=["is_published_to_hub"]) | |
| def reject(self, reviewer, notes=""): | |
| self.status = "rejected" | |
| self.reviewer = reviewer | |
| self.review_notes = notes | |
| self.save() | |
| class ModelAccessControl(models.Model): | |
| """ | |
| Fine‑grained access control for a model version. | |
| """ | |
| ACCESS_LEVELS = [ | |
| ("read", "Read"), | |
| ("download", "Download"), | |
| ("deploy", "Deploy"), | |
| ("manage", "Manage"), | |
| ] | |
| id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) | |
| version = models.ForeignKey( | |
| ModelVersion, on_delete=models.CASCADE, related_name="access_controls" | |
| ) | |
| user = models.ForeignKey( | |
| settings.AUTH_USER_MODEL, | |
| on_delete=models.CASCADE, | |
| related_name="versioning_model_access", | |
| ) | |
| access_level = models.CharField( | |
| max_length=20, choices=ACCESS_LEVELS, default="read" | |
| ) | |
| granted_by = models.ForeignKey( | |
| settings.AUTH_USER_MODEL, | |
| on_delete=models.SET_NULL, | |
| null=True, | |
| blank=True, | |
| related_name="versioning_granted_access", | |
| ) | |
| granted_at = models.DateTimeField(auto_now_add=True) | |
| expires_at = models.DateTimeField(null=True, blank=True) | |
| class Meta: | |
| unique_together = [["version", "user"]] | |
| ordering = ["-granted_at"] | |
| def __str__(self): | |
| return f"{self.user.username} → {self.access_level} on {self.version}" | |
| def is_expired(self): | |
| if self.expires_at and timezone.now() > self.expires_at: | |
| return True | |
| return False | |
| class ModelSecurityScan(models.Model): | |
| """ | |
| Security scan result for a model version. | |
| Inspired by JFrog's AI Catalogue security scanning. | |
| """ | |
| SEVERITY_CHOICES = [ | |
| ("low", "Low"), | |
| ("medium", "Medium"), | |
| ("high", "High"), | |
| ("critical", "Critical"), | |
| ] | |
| SCAN_TYPES = [ | |
| ("vulnerability", "Vulnerability Scan"), | |
| ("license", "License Compliance"), | |
| ("malware", "Malware Scan"), | |
| ("secrets", "Secrets Detection"), | |
| ("dependency", "Dependency Scan"), | |
| ] | |
| id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) | |
| version = models.ForeignKey( | |
| ModelVersion, on_delete=models.CASCADE, related_name="security_scans" | |
| ) | |
| scan_type = models.CharField( | |
| max_length=30, choices=SCAN_TYPES, default="vulnerability" | |
| ) | |
| status = models.CharField( | |
| max_length=30, | |
| default="pending", | |
| choices=[ | |
| ("pending", "Pending"), | |
| ("running", "Running"), | |
| ("completed", "Completed"), | |
| ("failed", "Failed"), | |
| ], | |
| ) | |
| severity = models.CharField( | |
| max_length=20, choices=SEVERITY_CHOICES, null=True, blank=True | |
| ) | |
| findings = models.JSONField(default=dict, blank=True) | |
| passed = models.BooleanField(default=False) | |
| report_url = models.URLField(blank=True, max_length=500) | |
| scanned_at = models.DateTimeField(auto_now_add=True) | |
| class Meta: | |
| ordering = ["-scanned_at"] | |
| def __str__(self): | |
| return f"Scan {self.scan_type} for {self.version}: {self.status}" | |
| class ModelUsageLog(models.Model): | |
| """ | |
| Audit trail for model usage — downloads, deployments, API calls. | |
| """ | |
| ACTION_TYPES = [ | |
| ("downloaded", "Downloaded"), | |
| ("deployed", "Deployed"), | |
| ("inference", "Inference API Call"), | |
| ("viewed", "Viewed"), | |
| ("shared", "Shared"), | |
| ("exported", "Exported"), | |
| ] | |
| id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) | |
| version = models.ForeignKey( | |
| ModelVersion, on_delete=models.CASCADE, related_name="usage_logs" | |
| ) | |
| user = models.ForeignKey( | |
| settings.AUTH_USER_MODEL, | |
| on_delete=models.CASCADE, | |
| related_name="versioning_model_usage", | |
| ) | |
| action = models.CharField(max_length=20, choices=ACTION_TYPES) | |
| metadata = models.JSONField(default=dict, blank=True) | |
| ip_address = models.GenericIPAddressField(null=True, blank=True) | |
| created_at = models.DateTimeField(auto_now_add=True, db_index=True) | |
| class Meta: | |
| ordering = ["-created_at"] | |
| indexes = [ | |
| models.Index(fields=["version", "-created_at"]), | |
| models.Index(fields=["user", "-created_at"]), | |
| ] | |
| def __str__(self): | |
| return f"{self.user.username} {self.action} {self.version}" | |
| class ModelBranch(models.Model): | |
| """A named branch for model versions (like git branch).""" | |
| id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) | |
| name = models.CharField(max_length=150) | |
| root_version = models.ForeignKey( | |
| "ModelVersion", on_delete=models.CASCADE, related_name="branches" | |
| ) | |
| created_by = models.ForeignKey( | |
| settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True | |
| ) | |
| created_at = models.DateTimeField(auto_now_add=True) | |
| class Meta: | |
| unique_together = [["name", "root_version"]] | |
| def __str__(self): | |
| return f"{self.name} (from {self.root_version})" | |
| class ModelCommit(models.Model): | |
| """A snapshot commit in a branch, linking versions with parent/merge info.""" | |
| id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) | |
| branch = models.ForeignKey( | |
| ModelBranch, on_delete=models.CASCADE, related_name="commits" | |
| ) | |
| version = models.ForeignKey( | |
| "ModelVersion", on_delete=models.CASCADE, related_name="commits" | |
| ) | |
| parent = models.ForeignKey( | |
| "self", | |
| on_delete=models.SET_NULL, | |
| null=True, | |
| blank=True, | |
| related_name="children", | |
| ) | |
| merge_parent = models.ForeignKey( | |
| "self", | |
| on_delete=models.SET_NULL, | |
| null=True, | |
| blank=True, | |
| related_name="merge_children", | |
| help_text="Second parent for merge commits", | |
| ) | |
| message = models.TextField(blank=True) | |
| created_by = models.ForeignKey( | |
| settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True | |
| ) | |
| created_at = models.DateTimeField(auto_now_add=True) | |
| class Meta: | |
| ordering = ["created_at"] | |
Xet Storage Details
- Size:
- 17.6 kB
- Xet hash:
- 143890c3543dc9187376ca30dcc7fc7a15f20c8b5fbf785ed61a5ead69b8fd69
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.