feat: Canonical status metadata across domains and drops
This commit is contained in:
@ -30,13 +30,14 @@ async def check_domain_availability(request: DomainCheckRequest):
|
||||
|
||||
return DomainCheckResponse(
|
||||
domain=result.domain,
|
||||
status=result.status.value,
|
||||
status=result.status,
|
||||
is_available=result.is_available,
|
||||
registrar=result.registrar,
|
||||
expiration_date=result.expiration_date,
|
||||
creation_date=result.creation_date,
|
||||
name_servers=result.name_servers,
|
||||
error_message=result.error_message,
|
||||
status_source=getattr(result, "check_method", None),
|
||||
checked_at=datetime.utcnow(),
|
||||
)
|
||||
|
||||
@ -61,13 +62,14 @@ async def check_domain_get(domain: str, quick: bool = False):
|
||||
|
||||
return DomainCheckResponse(
|
||||
domain=result.domain,
|
||||
status=result.status.value,
|
||||
status=result.status,
|
||||
is_available=result.is_available,
|
||||
registrar=result.registrar,
|
||||
expiration_date=result.expiration_date,
|
||||
creation_date=result.creation_date,
|
||||
name_servers=result.name_servers,
|
||||
error_message=result.error_message,
|
||||
status_source=getattr(result, "check_method", None),
|
||||
checked_at=datetime.utcnow(),
|
||||
)
|
||||
|
||||
|
||||
@ -175,6 +175,7 @@ async def add_domain(
|
||||
expiration_date=check_result.expiration_date,
|
||||
notify_on_available=domain_data.notify_on_available,
|
||||
last_checked=datetime.utcnow(),
|
||||
last_check_method=check_result.check_method,
|
||||
)
|
||||
db.add(domain)
|
||||
await db.flush()
|
||||
@ -277,6 +278,7 @@ async def refresh_domain(
|
||||
domain.registrar = check_result.registrar
|
||||
domain.expiration_date = _to_naive_utc(check_result.expiration_date)
|
||||
domain.last_checked = datetime.utcnow()
|
||||
domain.last_check_method = check_result.check_method
|
||||
|
||||
# Create check record
|
||||
check = DomainCheck(
|
||||
@ -354,6 +356,7 @@ async def refresh_all_domains(
|
||||
domain.registrar = check_result.registrar
|
||||
domain.expiration_date = _to_naive_utc(check_result.expiration_date)
|
||||
domain.last_checked = datetime.utcnow()
|
||||
domain.last_check_method = check_result.check_method
|
||||
|
||||
# Create check record
|
||||
check = DomainCheck(
|
||||
|
||||
@ -221,7 +221,9 @@ async def api_check_drop_status(
|
||||
.values(
|
||||
availability_status=status_result.status,
|
||||
rdap_status=str(status_result.rdap_status) if status_result.rdap_status else None,
|
||||
last_status_check=datetime.utcnow()
|
||||
last_status_check=datetime.utcnow(),
|
||||
deletion_date=status_result.deletion_date,
|
||||
last_check_method=status_result.check_method,
|
||||
)
|
||||
)
|
||||
await db.commit()
|
||||
@ -235,6 +237,8 @@ async def api_check_drop_status(
|
||||
"should_track": status_result.should_monitor,
|
||||
"message": status_result.message,
|
||||
"deletion_date": status_result.deletion_date.isoformat() if status_result.deletion_date else None,
|
||||
"status_checked_at": datetime.utcnow().isoformat(),
|
||||
"status_source": status_result.check_method,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@ -109,6 +109,11 @@ async def apply_migrations(conn: AsyncConnection) -> None:
|
||||
# 2b) domains indexes (watchlist list/sort/filter)
|
||||
# ---------------------------------------------------------
|
||||
if await _table_exists(conn, "domains"):
|
||||
# Canonical status metadata (optional)
|
||||
if not await _has_column(conn, "domains", "last_check_method"):
|
||||
logger.info("DB migrations: adding column domains.last_check_method")
|
||||
await conn.execute(text("ALTER TABLE domains ADD COLUMN last_check_method VARCHAR(30)"))
|
||||
|
||||
await conn.execute(text("CREATE INDEX IF NOT EXISTS ix_domains_user_id ON domains(user_id)"))
|
||||
await conn.execute(text("CREATE INDEX IF NOT EXISTS ix_domains_status ON domains(status)"))
|
||||
await conn.execute(text("CREATE INDEX IF NOT EXISTS ix_domains_user_created_at ON domains(user_id, created_at)"))
|
||||
@ -130,6 +135,10 @@ async def apply_migrations(conn: AsyncConnection) -> None:
|
||||
# 2d) dropped_domains indexes + de-duplication
|
||||
# ---------------------------------------------------------
|
||||
if await _table_exists(conn, "dropped_domains"):
|
||||
if not await _has_column(conn, "dropped_domains", "last_check_method"):
|
||||
logger.info("DB migrations: adding column dropped_domains.last_check_method")
|
||||
await conn.execute(text("ALTER TABLE dropped_domains ADD COLUMN last_check_method VARCHAR(30)"))
|
||||
|
||||
# Query patterns:
|
||||
# - by time window (dropped_date) + optional tld + keyword
|
||||
# - status updates (availability_status + last_status_check)
|
||||
|
||||
@ -42,6 +42,8 @@ class Domain(Base):
|
||||
# Timestamps
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||
last_checked: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
||||
# How the current status was derived (rdap_iana, whois, dns, etc.)
|
||||
last_check_method: Mapped[str | None] = mapped_column(String(30), nullable=True)
|
||||
|
||||
# Check history relationship
|
||||
checks: Mapped[list["DomainCheck"]] = relationship(
|
||||
@ -54,6 +56,17 @@ class Domain(Base):
|
||||
def __repr__(self) -> str:
|
||||
return f"<Domain {self.name} ({self.status})>"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Canonical status fields (API stability for Terminal consistency)
|
||||
# ------------------------------------------------------------------
|
||||
@property
|
||||
def status_checked_at(self) -> datetime | None:
|
||||
return self.last_checked
|
||||
|
||||
@property
|
||||
def status_source(self) -> str | None:
|
||||
return self.last_check_method
|
||||
|
||||
|
||||
class DomainCheck(Base):
|
||||
"""History of domain availability checks."""
|
||||
|
||||
@ -43,6 +43,7 @@ class DroppedDomain(Base):
|
||||
rdap_status = Column(String(255), nullable=True) # Raw RDAP status string
|
||||
last_status_check = Column(DateTime, nullable=True)
|
||||
deletion_date = Column(DateTime, nullable=True) # When domain will be fully deleted
|
||||
last_check_method = Column(String(30), nullable=True) # rdap_iana, rdap_ch, error, etc.
|
||||
|
||||
__table_args__ = (
|
||||
Index('ix_dropped_domains_tld_date', 'tld', 'dropped_date'),
|
||||
|
||||
@ -170,6 +170,7 @@ async def check_domains_by_frequency(frequency: str):
|
||||
domain.registrar = check_result.registrar
|
||||
domain.expiration_date = check_result.expiration_date
|
||||
domain.last_checked = datetime.utcnow()
|
||||
domain.last_check_method = getattr(check_result, "check_method", None)
|
||||
|
||||
# Create check record for history
|
||||
check = DomainCheck(
|
||||
|
||||
@ -39,9 +39,13 @@ class DomainResponse(BaseModel):
|
||||
is_available: bool
|
||||
registrar: Optional[str]
|
||||
expiration_date: Optional[datetime]
|
||||
deletion_date: Optional[datetime] = None
|
||||
notify_on_available: bool
|
||||
created_at: datetime
|
||||
last_checked: Optional[datetime]
|
||||
# Canonical status metadata (stable across Terminal modules)
|
||||
status_checked_at: Optional[datetime] = None
|
||||
status_source: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
@ -70,13 +74,14 @@ class DomainCheckRequest(BaseModel):
|
||||
class DomainCheckResponse(BaseModel):
|
||||
"""Schema for domain check response."""
|
||||
domain: str
|
||||
status: str
|
||||
status: DomainStatus
|
||||
is_available: bool
|
||||
registrar: Optional[str] = None
|
||||
expiration_date: Optional[datetime] = None
|
||||
creation_date: Optional[datetime] = None
|
||||
name_servers: Optional[List[str]] = None
|
||||
error_message: Optional[str] = None
|
||||
status_source: Optional[str] = None
|
||||
checked_at: datetime
|
||||
|
||||
|
||||
|
||||
@ -396,9 +396,13 @@ async def get_dropped_domains(
|
||||
"length": item.length,
|
||||
"is_numeric": item.is_numeric,
|
||||
"has_hyphen": item.has_hyphen,
|
||||
"availability_status": getattr(item, 'availability_status', 'unknown') or 'unknown',
|
||||
"last_status_check": item.last_status_check.isoformat() if getattr(item, 'last_status_check', None) else None,
|
||||
"deletion_date": item.deletion_date.isoformat() if getattr(item, 'deletion_date', None) else None,
|
||||
# Canonical status fields (keep old key for backwards compat)
|
||||
"availability_status": getattr(item, "availability_status", "unknown") or "unknown",
|
||||
"status": getattr(item, "availability_status", "unknown") or "unknown",
|
||||
"last_status_check": item.last_status_check.isoformat() if getattr(item, "last_status_check", None) else None,
|
||||
"status_checked_at": item.last_status_check.isoformat() if getattr(item, "last_status_check", None) else None,
|
||||
"status_source": getattr(item, "last_check_method", None),
|
||||
"deletion_date": item.deletion_date.isoformat() if getattr(item, "deletion_date", None) else None,
|
||||
}
|
||||
for item in items
|
||||
]
|
||||
@ -578,6 +582,7 @@ async def verify_drops_availability(
|
||||
"rdap_status": str(status_result.rdap_status)[:255] if status_result.rdap_status else None,
|
||||
"last_status_check": now,
|
||||
"deletion_date": status_result.deletion_date,
|
||||
"last_check_method": status_result.check_method,
|
||||
}
|
||||
)
|
||||
|
||||
@ -590,6 +595,7 @@ async def verify_drops_availability(
|
||||
rdap_status=bindparam("rdap_status"),
|
||||
last_status_check=bindparam("last_status_check"),
|
||||
deletion_date=bindparam("deletion_date"),
|
||||
last_check_method=bindparam("last_check_method"),
|
||||
)
|
||||
)
|
||||
await db.execute(stmt, updates)
|
||||
|
||||
Reference in New Issue
Block a user