Source code for gitlab_overviewer.models.project

  1"""GitLab Project model implementation.
  2
  3Implements :any:`/specs/spec_model_mapping` §2.2 for Project entity mapping.
  4"""
  5
  6from __future__ import annotations
  7
  8from datetime import datetime
  9import json
 10from typing import Any, Dict, List
 11
 12import logging
 13
 14
 15from pydantic import BaseModel, Field, field_validator
 16
 17from ..rendering.renderer_base import Renderer
 18from .group import Group
 19
 20logger = logging.getLogger(__name__)
 21
 22
[docs] 23class Project(BaseModel): 24 """GitLab Project domain model. 25 26 Implements :any:`/specs/spec_model_mapping` §2.2, mapping the following fields: 27 28 * id (int) 29 * name (str) 30 * path_with_namespace (str, optional) 31 * default_branch (str, optional) 32 * last_activity_at (datetime) - Parsed to UTC 33 * namespace (Group) - namespace parsed as group if possible. 34 """ 35 36 id: int 37 name: str 38 description: str | None = None 39 web_url: str | None = None 40 path: str | None = None 41 path_with_namespace: str | None = None 42 last_activity_at: datetime | None = None 43 default_branch: str | None = None 44 visibility: str | None = None 45 readme_url: str | None = None 46 avatar_url: str | None = None 47 namespace: Group | None = None 48
[docs] 49 @field_validator("id", mode="before") 50 @classmethod 51 def validate_int_fields(cls, v: Any) -> int | None: 52 """Convert string/integer mismatches for numeric fields.""" 53 if v is None: 54 return None 55 return Renderer.safe_int(v, v)
56
[docs] 57 @field_validator("namespace", mode="before") 58 @classmethod 59 def validate_group_fields(cls, v: Any) -> Group | None: 60 """Convert group-dict to group.""" 61 if v is None: 62 return None 63 try: 64 import logging 65 66 logging.debug(f"Validating namespace: {v}") 67 g = Group(**v) 68 logging.debug(f"Validated namespace: {g}") 69 return g 70 except Exception: 71 return None
72
[docs] 73 @field_validator("last_activity_at", mode="before") 74 @classmethod 75 def validate_datetime_fields(cls, v: Any) -> datetime | None: 76 """Convert string dates to datetime if needed.""" 77 if v is None: 78 return None 79 if isinstance(v, str): 80 parsed_date = Renderer.parse_iso_date(v) 81 if parsed_date is not None: 82 return parsed_date 83 # Try common date formats as fallback 84 try: 85 for fmt in ["%Y-%m-%d", "%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"]: 86 try: 87 return datetime.strptime(v, fmt) 88 except ValueError: 89 continue 90 except Exception: 91 pass 92 # If all parsing fails, return None 93 return None 94 return v
95
[docs] 96 def get_avatar_html(self, width: int = 16) -> str: 97 """Generate HTML for avatar image if available.""" 98 if self.avatar_url: 99 return f"![Avatar]({self.avatar_url}){{width={width}px}} " 100 return ""
101
[docs] 102 @classmethod 103 def from_api_json(cls, data: dict[str, Any]) -> Project: 104 """Create Project from API JSON with robust type handling.""" 105 processed_data = {} 106 for key, value in data.items(): 107 if key in ["id"]: 108 processed_data[key] = Renderer.safe_int(value, value) 109 elif key in ["last_activity_at"]: 110 if isinstance(value, str): 111 parsed_date = Renderer.parse_iso_date(value) 112 processed_data[key] = ( 113 parsed_date if parsed_date is not None else value 114 ) 115 else: 116 processed_data[key] = value 117 else: 118 processed_data[key] = value 119 return cls(**processed_data)
120
[docs] 121 def model_dump(self, **kwargs) -> dict[str, Any]: 122 """Override to include computed fields.""" 123 data = super().model_dump(**kwargs) 124 # Add computed fields 125 data["avatar_html"] = self.get_avatar_html() 126 return data
127 128 model_config = { 129 "extra": "ignore", 130 "populate_by_name": True, 131 }
132 133 134__all__ = ["Project"]