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(
|
return DomainCheckResponse(
|
||||||
domain=result.domain,
|
domain=result.domain,
|
||||||
status=result.status.value,
|
status=result.status,
|
||||||
is_available=result.is_available,
|
is_available=result.is_available,
|
||||||
registrar=result.registrar,
|
registrar=result.registrar,
|
||||||
expiration_date=result.expiration_date,
|
expiration_date=result.expiration_date,
|
||||||
creation_date=result.creation_date,
|
creation_date=result.creation_date,
|
||||||
name_servers=result.name_servers,
|
name_servers=result.name_servers,
|
||||||
error_message=result.error_message,
|
error_message=result.error_message,
|
||||||
|
status_source=getattr(result, "check_method", None),
|
||||||
checked_at=datetime.utcnow(),
|
checked_at=datetime.utcnow(),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -61,13 +62,14 @@ async def check_domain_get(domain: str, quick: bool = False):
|
|||||||
|
|
||||||
return DomainCheckResponse(
|
return DomainCheckResponse(
|
||||||
domain=result.domain,
|
domain=result.domain,
|
||||||
status=result.status.value,
|
status=result.status,
|
||||||
is_available=result.is_available,
|
is_available=result.is_available,
|
||||||
registrar=result.registrar,
|
registrar=result.registrar,
|
||||||
expiration_date=result.expiration_date,
|
expiration_date=result.expiration_date,
|
||||||
creation_date=result.creation_date,
|
creation_date=result.creation_date,
|
||||||
name_servers=result.name_servers,
|
name_servers=result.name_servers,
|
||||||
error_message=result.error_message,
|
error_message=result.error_message,
|
||||||
|
status_source=getattr(result, "check_method", None),
|
||||||
checked_at=datetime.utcnow(),
|
checked_at=datetime.utcnow(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -175,6 +175,7 @@ async def add_domain(
|
|||||||
expiration_date=check_result.expiration_date,
|
expiration_date=check_result.expiration_date,
|
||||||
notify_on_available=domain_data.notify_on_available,
|
notify_on_available=domain_data.notify_on_available,
|
||||||
last_checked=datetime.utcnow(),
|
last_checked=datetime.utcnow(),
|
||||||
|
last_check_method=check_result.check_method,
|
||||||
)
|
)
|
||||||
db.add(domain)
|
db.add(domain)
|
||||||
await db.flush()
|
await db.flush()
|
||||||
@ -277,6 +278,7 @@ async def refresh_domain(
|
|||||||
domain.registrar = check_result.registrar
|
domain.registrar = check_result.registrar
|
||||||
domain.expiration_date = _to_naive_utc(check_result.expiration_date)
|
domain.expiration_date = _to_naive_utc(check_result.expiration_date)
|
||||||
domain.last_checked = datetime.utcnow()
|
domain.last_checked = datetime.utcnow()
|
||||||
|
domain.last_check_method = check_result.check_method
|
||||||
|
|
||||||
# Create check record
|
# Create check record
|
||||||
check = DomainCheck(
|
check = DomainCheck(
|
||||||
@ -354,6 +356,7 @@ async def refresh_all_domains(
|
|||||||
domain.registrar = check_result.registrar
|
domain.registrar = check_result.registrar
|
||||||
domain.expiration_date = _to_naive_utc(check_result.expiration_date)
|
domain.expiration_date = _to_naive_utc(check_result.expiration_date)
|
||||||
domain.last_checked = datetime.utcnow()
|
domain.last_checked = datetime.utcnow()
|
||||||
|
domain.last_check_method = check_result.check_method
|
||||||
|
|
||||||
# Create check record
|
# Create check record
|
||||||
check = DomainCheck(
|
check = DomainCheck(
|
||||||
|
|||||||
@ -221,7 +221,9 @@ async def api_check_drop_status(
|
|||||||
.values(
|
.values(
|
||||||
availability_status=status_result.status,
|
availability_status=status_result.status,
|
||||||
rdap_status=str(status_result.rdap_status) if status_result.rdap_status else None,
|
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()
|
await db.commit()
|
||||||
@ -235,6 +237,8 @@ async def api_check_drop_status(
|
|||||||
"should_track": status_result.should_monitor,
|
"should_track": status_result.should_monitor,
|
||||||
"message": status_result.message,
|
"message": status_result.message,
|
||||||
"deletion_date": status_result.deletion_date.isoformat() if status_result.deletion_date else None,
|
"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:
|
except Exception as e:
|
||||||
|
|||||||
@ -109,6 +109,11 @@ async def apply_migrations(conn: AsyncConnection) -> None:
|
|||||||
# 2b) domains indexes (watchlist list/sort/filter)
|
# 2b) domains indexes (watchlist list/sort/filter)
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
if await _table_exists(conn, "domains"):
|
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_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_status ON domains(status)"))
|
||||||
await conn.execute(text("CREATE INDEX IF NOT EXISTS ix_domains_user_created_at ON domains(user_id, created_at)"))
|
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
|
# 2d) dropped_domains indexes + de-duplication
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
if await _table_exists(conn, "dropped_domains"):
|
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:
|
# Query patterns:
|
||||||
# - by time window (dropped_date) + optional tld + keyword
|
# - by time window (dropped_date) + optional tld + keyword
|
||||||
# - status updates (availability_status + last_status_check)
|
# - status updates (availability_status + last_status_check)
|
||||||
|
|||||||
@ -42,6 +42,8 @@ class Domain(Base):
|
|||||||
# Timestamps
|
# Timestamps
|
||||||
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
||||||
last_checked: Mapped[datetime | None] = mapped_column(DateTime, nullable=True)
|
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
|
# Check history relationship
|
||||||
checks: Mapped[list["DomainCheck"]] = relationship(
|
checks: Mapped[list["DomainCheck"]] = relationship(
|
||||||
@ -54,6 +56,17 @@ class Domain(Base):
|
|||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"<Domain {self.name} ({self.status})>"
|
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):
|
class DomainCheck(Base):
|
||||||
"""History of domain availability checks."""
|
"""History of domain availability checks."""
|
||||||
|
|||||||
@ -43,6 +43,7 @@ class DroppedDomain(Base):
|
|||||||
rdap_status = Column(String(255), nullable=True) # Raw RDAP status string
|
rdap_status = Column(String(255), nullable=True) # Raw RDAP status string
|
||||||
last_status_check = Column(DateTime, nullable=True)
|
last_status_check = Column(DateTime, nullable=True)
|
||||||
deletion_date = Column(DateTime, nullable=True) # When domain will be fully deleted
|
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__ = (
|
__table_args__ = (
|
||||||
Index('ix_dropped_domains_tld_date', 'tld', 'dropped_date'),
|
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.registrar = check_result.registrar
|
||||||
domain.expiration_date = check_result.expiration_date
|
domain.expiration_date = check_result.expiration_date
|
||||||
domain.last_checked = datetime.utcnow()
|
domain.last_checked = datetime.utcnow()
|
||||||
|
domain.last_check_method = getattr(check_result, "check_method", None)
|
||||||
|
|
||||||
# Create check record for history
|
# Create check record for history
|
||||||
check = DomainCheck(
|
check = DomainCheck(
|
||||||
|
|||||||
@ -39,9 +39,13 @@ class DomainResponse(BaseModel):
|
|||||||
is_available: bool
|
is_available: bool
|
||||||
registrar: Optional[str]
|
registrar: Optional[str]
|
||||||
expiration_date: Optional[datetime]
|
expiration_date: Optional[datetime]
|
||||||
|
deletion_date: Optional[datetime] = None
|
||||||
notify_on_available: bool
|
notify_on_available: bool
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
last_checked: Optional[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:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
@ -70,13 +74,14 @@ class DomainCheckRequest(BaseModel):
|
|||||||
class DomainCheckResponse(BaseModel):
|
class DomainCheckResponse(BaseModel):
|
||||||
"""Schema for domain check response."""
|
"""Schema for domain check response."""
|
||||||
domain: str
|
domain: str
|
||||||
status: str
|
status: DomainStatus
|
||||||
is_available: bool
|
is_available: bool
|
||||||
registrar: Optional[str] = None
|
registrar: Optional[str] = None
|
||||||
expiration_date: Optional[datetime] = None
|
expiration_date: Optional[datetime] = None
|
||||||
creation_date: Optional[datetime] = None
|
creation_date: Optional[datetime] = None
|
||||||
name_servers: Optional[List[str]] = None
|
name_servers: Optional[List[str]] = None
|
||||||
error_message: Optional[str] = None
|
error_message: Optional[str] = None
|
||||||
|
status_source: Optional[str] = None
|
||||||
checked_at: datetime
|
checked_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -396,9 +396,13 @@ async def get_dropped_domains(
|
|||||||
"length": item.length,
|
"length": item.length,
|
||||||
"is_numeric": item.is_numeric,
|
"is_numeric": item.is_numeric,
|
||||||
"has_hyphen": item.has_hyphen,
|
"has_hyphen": item.has_hyphen,
|
||||||
"availability_status": getattr(item, 'availability_status', 'unknown') or 'unknown',
|
# Canonical status fields (keep old key for backwards compat)
|
||||||
"last_status_check": item.last_status_check.isoformat() if getattr(item, 'last_status_check', None) else None,
|
"availability_status": getattr(item, "availability_status", "unknown") or "unknown",
|
||||||
"deletion_date": item.deletion_date.isoformat() if getattr(item, 'deletion_date', None) else None,
|
"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
|
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,
|
"rdap_status": str(status_result.rdap_status)[:255] if status_result.rdap_status else None,
|
||||||
"last_status_check": now,
|
"last_status_check": now,
|
||||||
"deletion_date": status_result.deletion_date,
|
"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"),
|
rdap_status=bindparam("rdap_status"),
|
||||||
last_status_check=bindparam("last_status_check"),
|
last_status_check=bindparam("last_status_check"),
|
||||||
deletion_date=bindparam("deletion_date"),
|
deletion_date=bindparam("deletion_date"),
|
||||||
|
last_check_method=bindparam("last_check_method"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
await db.execute(stmt, updates)
|
await db.execute(stmt, updates)
|
||||||
|
|||||||
Reference in New Issue
Block a user