Coverage for src / idx_api / models / vision_job.py: 83%

35 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2025-12-28 11:09 -0700

1"""Vision extraction job tracking model.""" 

2 

3from datetime import datetime 

4from typing import Optional 

5 

6from sqlalchemy import DateTime, Float, Integer, String, Text 

7from sqlalchemy.orm import Mapped, mapped_column 

8 

9from idx_api.models.base import Base 

10 

11 

12class VisionExtractionJob(Base): 

13 """Track vision extraction job runs for monitoring and history. 

14 

15 Each row represents a single extraction job run, recording: 

16 - Start/end times and duration 

17 - Properties and images processed 

18 - Success/failure counts 

19 - Configuration used 

20 """ 

21 

22 __tablename__ = "vision_extraction_jobs" 

23 

24 id: Mapped[int] = mapped_column(primary_key=True) 

25 

26 # Job timing 

27 started_at: Mapped[datetime] = mapped_column( 

28 DateTime(timezone=True), nullable=False, default=datetime.utcnow 

29 ) 

30 ended_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), nullable=True) 

31 duration_seconds: Mapped[Optional[float]] = mapped_column(Float, nullable=True) 

32 

33 # Status: pending, running, completed, failed, cancelled 

34 status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending") 

35 

36 # Scope 

37 brokerage_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) 

38 limit_properties: Mapped[Optional[int]] = mapped_column(Integer, nullable=True) 

39 

40 # Progress counters 

41 total_properties: Mapped[int] = mapped_column(Integer, default=0) 

42 processed_properties: Mapped[int] = mapped_column(Integer, default=0) 

43 total_images: Mapped[int] = mapped_column(Integer, default=0) 

44 processed_images: Mapped[int] = mapped_column(Integer, default=0) 

45 failed_images: Mapped[int] = mapped_column(Integer, default=0) 

46 tags_created: Mapped[int] = mapped_column(Integer, default=0) 

47 

48 # Configuration snapshot 

49 vision_model: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) 

50 skip_visual_embeddings: Mapped[bool] = mapped_column(default=False) 

51 

52 # Error tracking 

53 last_error: Mapped[Optional[str]] = mapped_column(Text, nullable=True) 

54 error_count: Mapped[int] = mapped_column(Integer, default=0) 

55 

56 # Who triggered it 

57 triggered_by: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) 

58 

59 def __repr__(self) -> str: 

60 return f"<VisionExtractionJob(id={self.id}, status='{self.status}', processed={self.processed_images}/{self.total_images})>" 

61 

62 @property 

63 def progress_percent(self) -> float: 

64 """Calculate progress percentage.""" 

65 if self.total_images == 0: 

66 return 0.0 

67 return (self.processed_images / self.total_images) * 100 

68 

69 @property 

70 def images_per_second(self) -> float: 

71 """Calculate processing rate.""" 

72 if not self.duration_seconds or self.duration_seconds == 0: 

73 return 0.0 

74 return self.processed_images / self.duration_seconds