Initial commit: CI Agent competitive-intelligence monitoring app
FastAPI + Celery + Next.js + Postgres/Redis app with company monitoring, source collection, LLM-based change analysis, enrichment, and account security (Turnstile, escalating lockout, email verification).
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any, Self
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
from app.models.company import Company
|
||||
from app.models.enums import CompanyStatus, EnrichmentStatus, MonitoringFrequency, SeverityLevel
|
||||
|
||||
|
||||
def _normalize_website(value: str | None) -> str | None:
|
||||
if value is None or value.strip() == "":
|
||||
return None
|
||||
value = value.strip()
|
||||
if not (value.startswith("http://") or value.startswith("https://")):
|
||||
value = f"https://{value}"
|
||||
return value
|
||||
|
||||
|
||||
class CompanyCreate(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=200)
|
||||
official_website: str | None = Field(default=None, max_length=500)
|
||||
description: str | None = Field(default=None, max_length=4000)
|
||||
monitoring_focus: str | None = Field(default=None, max_length=2000)
|
||||
industry: str | None = Field(default=None, max_length=120)
|
||||
country: str | None = Field(default=None, max_length=120)
|
||||
region: str | None = Field(default=None, max_length=120)
|
||||
headquarters: str | None = Field(default=None, max_length=200)
|
||||
public_identifiers: dict[str, str] = Field(default_factory=dict)
|
||||
competitor_names: list[str] = Field(default_factory=list, max_length=25)
|
||||
alias_names: list[str] = Field(default_factory=list, max_length=25)
|
||||
|
||||
frequency_type: MonitoringFrequency = MonitoringFrequency.WEEKLY
|
||||
interval_minutes: int | None = Field(default=None, ge=1)
|
||||
cron_expression: str | None = Field(default=None, max_length=120)
|
||||
timezone: str = Field(default="America/New_York", max_length=64)
|
||||
severity_threshold: SeverityLevel = SeverityLevel.MEDIUM
|
||||
|
||||
@field_validator("official_website")
|
||||
@classmethod
|
||||
def _validate_website(cls, value: str | None) -> str | None:
|
||||
return _normalize_website(value)
|
||||
|
||||
|
||||
class CompanyUpdate(BaseModel):
|
||||
name: str | None = Field(default=None, min_length=1, max_length=200)
|
||||
official_website: str | None = Field(default=None, max_length=500)
|
||||
description: str | None = Field(default=None, max_length=4000)
|
||||
monitoring_focus: str | None = Field(default=None, max_length=2000)
|
||||
industry: str | None = Field(default=None, max_length=120)
|
||||
country: str | None = Field(default=None, max_length=120)
|
||||
region: str | None = Field(default=None, max_length=120)
|
||||
headquarters: str | None = Field(default=None, max_length=200)
|
||||
public_identifiers: dict[str, str] | None = None
|
||||
competitor_names: list[str] | None = Field(default=None, max_length=25)
|
||||
alias_names: list[str] | None = Field(default=None, max_length=25)
|
||||
|
||||
@field_validator("official_website")
|
||||
@classmethod
|
||||
def _validate_website(cls, value: str | None) -> str | None:
|
||||
return _normalize_website(value)
|
||||
|
||||
|
||||
class MonitorConfigurationResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
frequency_type: MonitoringFrequency
|
||||
interval_minutes: int | None
|
||||
cron_expression: str | None
|
||||
timezone: str
|
||||
enabled: bool
|
||||
next_run: datetime | None
|
||||
last_run: datetime | None
|
||||
severity_threshold: SeverityLevel
|
||||
|
||||
|
||||
class MonitorConfigurationUpdate(BaseModel):
|
||||
frequency_type: MonitoringFrequency | None = None
|
||||
interval_minutes: int | None = Field(default=None, ge=1)
|
||||
cron_expression: str | None = Field(default=None, max_length=120)
|
||||
timezone: str | None = Field(default=None, max_length=64)
|
||||
enabled: bool | None = None
|
||||
severity_threshold: SeverityLevel | None = None
|
||||
|
||||
|
||||
class CompanyEnrichmentResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
status: EnrichmentStatus
|
||||
data: dict[str, Any] = Field(default_factory=dict)
|
||||
errors: dict[str, str] = Field(default_factory=dict)
|
||||
credits_spent: int | None
|
||||
fetched_at: datetime | None
|
||||
|
||||
|
||||
class CompanyResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
slug: str
|
||||
official_website: str | None
|
||||
description: str | None
|
||||
monitoring_focus: str | None
|
||||
industry: str | None
|
||||
country: str | None
|
||||
region: str | None
|
||||
headquarters: str | None
|
||||
public_identifiers: dict[str, str] = Field(default_factory=dict)
|
||||
status: CompanyStatus
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
aliases: list[str] = Field(default_factory=list)
|
||||
competitors: list[str] = Field(default_factory=list)
|
||||
monitor_configuration: MonitorConfigurationResponse | None = None
|
||||
# None until the onboarding-time enrichment task finishes (or if
|
||||
# NINJAPEAR_API_KEY was never configured) - see enrichment_service.py.
|
||||
enrichment: CompanyEnrichmentResponse | None = None
|
||||
# Report/alert counts wire up once those models exist (Phases 7-8).
|
||||
report_count: int = 0
|
||||
unresolved_alert_count: int = 0
|
||||
|
||||
@classmethod
|
||||
def from_company(
|
||||
cls,
|
||||
company: Company,
|
||||
*,
|
||||
report_count: int = 0,
|
||||
unresolved_alert_count: int = 0,
|
||||
) -> Self:
|
||||
return cls(
|
||||
id=company.id,
|
||||
name=company.name,
|
||||
slug=company.slug,
|
||||
official_website=company.official_website,
|
||||
description=company.description,
|
||||
monitoring_focus=company.monitoring_focus,
|
||||
industry=company.industry,
|
||||
country=company.country,
|
||||
region=company.region,
|
||||
headquarters=company.headquarters,
|
||||
public_identifiers=company.public_identifiers,
|
||||
status=company.status,
|
||||
created_at=company.created_at,
|
||||
updated_at=company.updated_at,
|
||||
aliases=[a.alias for a in company.aliases],
|
||||
competitors=[c.name for c in company.competitors],
|
||||
monitor_configuration=(
|
||||
MonitorConfigurationResponse.model_validate(company.monitor_configuration)
|
||||
if company.monitor_configuration
|
||||
else None
|
||||
),
|
||||
enrichment=(
|
||||
CompanyEnrichmentResponse.model_validate(company.enrichment)
|
||||
if company.enrichment
|
||||
else None
|
||||
),
|
||||
report_count=report_count,
|
||||
unresolved_alert_count=unresolved_alert_count,
|
||||
)
|
||||
Reference in New Issue
Block a user