Spaces:
Paused
Paused
| """ | |
| Package operation model for tracking async pip operations. | |
| """ | |
| import uuid | |
| from django.conf import settings | |
| from django.db import models | |
| class PackageOperation(models.Model): | |
| """ | |
| Tracks async package installation/uninstallation operations. | |
| Used to provide progress feedback in the UI. | |
| """ | |
| class Operation(models.TextChoices): | |
| INSTALL = "install", "Install" | |
| UNINSTALL = "uninstall", "Uninstall" | |
| BULK_INSTALL = "bulk_install", "Bulk Install" | |
| class Status(models.TextChoices): | |
| PENDING = "pending", "Pending" | |
| RUNNING = "running", "Running" | |
| SUCCESS = "success", "Success" | |
| FAILED = "failed", "Failed" | |
| id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) | |
| environment = models.ForeignKey( | |
| "core.Environment", | |
| on_delete=models.CASCADE, | |
| related_name="package_operations", | |
| ) | |
| operation = models.CharField( | |
| max_length=20, | |
| choices=Operation.choices, | |
| ) | |
| package_spec = models.CharField( | |
| max_length=500, | |
| help_text="Package specification (e.g., 'requests==2.31.0') or requirements content for bulk install", | |
| ) | |
| status = models.CharField( | |
| max_length=20, | |
| choices=Status.choices, | |
| default=Status.PENDING, | |
| ) | |
| output = models.TextField( | |
| blank=True, | |
| help_text="pip stdout", | |
| ) | |
| error = models.TextField( | |
| blank=True, | |
| help_text="pip stderr", | |
| ) | |
| task_id = models.CharField( | |
| max_length=100, | |
| blank=True, | |
| help_text="django-q2 task ID for tracking", | |
| ) | |
| created_at = models.DateTimeField(auto_now_add=True) | |
| started_at = models.DateTimeField(null=True, blank=True) | |
| completed_at = models.DateTimeField(null=True, blank=True) | |
| created_by = models.ForeignKey( | |
| settings.AUTH_USER_MODEL, | |
| on_delete=models.SET_NULL, | |
| null=True, | |
| blank=True, | |
| related_name="package_operations", | |
| ) | |
| class Meta: | |
| db_table = "package_operations" | |
| ordering = ["-created_at"] | |
| verbose_name = "package operation" | |
| verbose_name_plural = "package operations" | |
| def __str__(self): | |
| return f"{self.get_operation_display()} {self.package_spec[:50]} ({self.status})" | |
| def is_finished(self) -> bool: | |
| """Check if the operation has completed (success or failed).""" | |
| return self.status in (self.Status.SUCCESS, self.Status.FAILED) | |
| def is_successful(self) -> bool: | |
| """Check if the operation completed successfully.""" | |
| return self.status == self.Status.SUCCESS | |
| def duration(self): | |
| """Return the duration of the operation if completed.""" | |
| if self.started_at and self.completed_at: | |
| return self.completed_at - self.started_at | |
| return None | |
| def duration_display(self) -> str: | |
| """Return a human-readable duration string.""" | |
| duration = self.duration | |
| if not duration: | |
| return "-" | |
| total_seconds = int(duration.total_seconds()) | |
| if total_seconds < 60: | |
| return f"{total_seconds}s" | |
| minutes = total_seconds // 60 | |
| seconds = total_seconds % 60 | |
| return f"{minutes}m {seconds}s" | |