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,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from app.models.enums import NotificationDeliveryStatus, SeverityLevel
|
||||
|
||||
|
||||
class NotificationDeliverySummary(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
destination_id: uuid.UUID
|
||||
provider: str
|
||||
status: NotificationDeliveryStatus
|
||||
external_message_id: str | None
|
||||
error_message: str | None
|
||||
|
||||
|
||||
class AlertResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
company_id: uuid.UUID
|
||||
detected_change_id: uuid.UUID
|
||||
title: str
|
||||
summary: str
|
||||
why_it_matters: str
|
||||
severity: SeverityLevel
|
||||
confidence: float
|
||||
read: bool
|
||||
resolved: bool
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class AlertDetailResponse(AlertResponse):
|
||||
deliveries: list[NotificationDeliverySummary]
|
||||
|
||||
|
||||
class AlertUpdate(BaseModel):
|
||||
read: bool | None = None
|
||||
resolved: bool | None = None
|
||||
@@ -0,0 +1,81 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, EmailStr, Field, field_validator
|
||||
|
||||
|
||||
def _validate_password_strength(value: str) -> str:
|
||||
has_letter = any(c.isalpha() for c in value)
|
||||
has_digit = any(c.isdigit() for c in value)
|
||||
if not (has_letter and has_digit):
|
||||
raise ValueError("Password must contain at least one letter and one digit")
|
||||
return value
|
||||
|
||||
|
||||
class RegisterRequest(BaseModel):
|
||||
email: EmailStr
|
||||
password: str = Field(min_length=10, max_length=128)
|
||||
display_name: str = Field(min_length=1, max_length=120)
|
||||
timezone: str = Field(default="America/New_York", max_length=64)
|
||||
turnstile_token: str | None = None
|
||||
|
||||
@field_validator("password")
|
||||
@classmethod
|
||||
def _password_strength(cls, value: str) -> str:
|
||||
return _validate_password_strength(value)
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
email: EmailStr
|
||||
password: str = Field(min_length=1, max_length=128)
|
||||
turnstile_token: str | None = None
|
||||
|
||||
|
||||
class RefreshRequest(BaseModel):
|
||||
refresh_token: str
|
||||
|
||||
|
||||
class LogoutRequest(BaseModel):
|
||||
refresh_token: str
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
access_token: str
|
||||
refresh_token: str
|
||||
token_type: str = "bearer"
|
||||
expires_in_minutes: int
|
||||
|
||||
|
||||
class VerifyEmailRequest(BaseModel):
|
||||
email: EmailStr
|
||||
code: str = Field(min_length=6, max_length=6)
|
||||
|
||||
|
||||
class ResendVerificationRequest(BaseModel):
|
||||
email: EmailStr
|
||||
turnstile_token: str | None = None
|
||||
|
||||
|
||||
class RequestPasswordResetRequest(BaseModel):
|
||||
email: EmailStr
|
||||
turnstile_token: str | None = None
|
||||
|
||||
|
||||
class ConfirmPasswordResetRequest(BaseModel):
|
||||
email: EmailStr
|
||||
code: str = Field(min_length=6, max_length=6)
|
||||
new_password: str = Field(min_length=10, max_length=128)
|
||||
|
||||
@field_validator("new_password")
|
||||
@classmethod
|
||||
def _password_strength(cls, value: str) -> str:
|
||||
return _validate_password_strength(value)
|
||||
|
||||
|
||||
class SecurityEventResponse(BaseModel):
|
||||
event_type: str
|
||||
ip_address: str
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class RunsByDayPoint(BaseModel):
|
||||
date: str
|
||||
successful: int
|
||||
failed: int
|
||||
other: int
|
||||
|
||||
|
||||
class RecentSignal(BaseModel):
|
||||
id: uuid.UUID
|
||||
company_id: uuid.UUID
|
||||
company_name: str
|
||||
change_type: str
|
||||
severity: str
|
||||
confidence_score: float
|
||||
summary: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class DashboardAnalytics(BaseModel):
|
||||
changes_by_type: dict[str, int]
|
||||
alerts_by_severity: dict[str, int]
|
||||
sources_by_status: dict[str, int]
|
||||
runs_by_day: list[RunsByDayPoint]
|
||||
recent_signals: list[RecentSignal]
|
||||
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.models.enums import SourceType
|
||||
|
||||
|
||||
class DiscoverCompanyRequest(BaseModel):
|
||||
name: str = Field(min_length=1, max_length=200)
|
||||
official_website: str | None = Field(default=None, max_length=500)
|
||||
monitoring_focus: str | None = Field(default=None, max_length=2000)
|
||||
competitor_names: list[str] = Field(default_factory=list, max_length=25)
|
||||
alias_names: list[str] = Field(default_factory=list, max_length=25)
|
||||
|
||||
|
||||
class PotentialSource(BaseModel):
|
||||
source_type: SourceType
|
||||
name: str
|
||||
base_url: str | None
|
||||
|
||||
|
||||
class DiscoveredCompanyProfile(BaseModel):
|
||||
name: str
|
||||
official_website: str | None
|
||||
description: str | None
|
||||
monitoring_focus: str | None
|
||||
industry: str | None
|
||||
country: str | None
|
||||
region: str | None
|
||||
headquarters: str | None
|
||||
aliases: list[str]
|
||||
competitors: list[str]
|
||||
public_identifiers: dict[str, str]
|
||||
potential_sources: list[PotentialSource]
|
||||
sources_consulted: list[str]
|
||||
@@ -0,0 +1,26 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from app.models.enums import MonitoringRunStatus, MonitoringRunTrigger
|
||||
|
||||
|
||||
class MonitoringRunResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
company_id: uuid.UUID
|
||||
trigger_type: MonitoringRunTrigger
|
||||
status: MonitoringRunStatus
|
||||
started_at: datetime | None
|
||||
completed_at: datetime | None
|
||||
sources_attempted: int
|
||||
sources_successful: int
|
||||
sources_failed: int
|
||||
items_collected: int
|
||||
changes_detected: int
|
||||
error_summary: str | None
|
||||
created_at: datetime
|
||||
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Self
|
||||
|
||||
from email_validator import EmailNotValidError, validate_email
|
||||
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||
|
||||
from app.models.enums import NotificationType, SeverityLevel
|
||||
from app.models.notification_destination import NotificationDestination
|
||||
|
||||
_PHONE_RE = re.compile(r"^\+?[1-9]\d{7,14}$")
|
||||
|
||||
|
||||
class NotificationDestinationCreate(BaseModel):
|
||||
type: NotificationType
|
||||
destination_value: str = Field(min_length=1, max_length=320)
|
||||
minimum_severity: SeverityLevel = SeverityLevel.MEDIUM
|
||||
enabled: bool = True
|
||||
company_ids: list[uuid.UUID] = Field(
|
||||
min_length=1,
|
||||
description="Which companies this destination receives alerts for. If a destination "
|
||||
"with the same type/value already exists for this user, it's reused (linked to these "
|
||||
"companies too) rather than duplicated.",
|
||||
)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _validate_destination_value(self) -> Self:
|
||||
if self.type == NotificationType.EMAIL:
|
||||
try:
|
||||
validate_email(self.destination_value, check_deliverability=False)
|
||||
except EmailNotValidError as exc:
|
||||
raise ValueError(f"Invalid email address: {exc}") from exc
|
||||
elif self.type == NotificationType.SMS:
|
||||
if not _PHONE_RE.match(self.destination_value):
|
||||
raise ValueError("Phone number must be in E.164 format, e.g. +15551234567")
|
||||
return self
|
||||
|
||||
|
||||
class NotificationDestinationUpdate(BaseModel):
|
||||
destination_value: str | None = Field(default=None, min_length=1, max_length=320)
|
||||
enabled: bool | None = None
|
||||
minimum_severity: SeverityLevel | None = None
|
||||
|
||||
|
||||
class LinkedCompany(BaseModel):
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
|
||||
|
||||
class NotificationDestinationResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
type: NotificationType
|
||||
destination_value: str
|
||||
verified: bool
|
||||
enabled: bool
|
||||
minimum_severity: SeverityLevel
|
||||
created_at: datetime
|
||||
companies: list[LinkedCompany] = Field(default_factory=list)
|
||||
|
||||
@classmethod
|
||||
def from_destination(cls, destination: NotificationDestination) -> Self:
|
||||
return cls(
|
||||
id=destination.id,
|
||||
type=destination.type,
|
||||
destination_value=destination.destination_value,
|
||||
verified=destination.verified,
|
||||
enabled=destination.enabled,
|
||||
minimum_severity=destination.minimum_severity,
|
||||
created_at=destination.created_at,
|
||||
companies=[
|
||||
LinkedCompany(id=link.company.id, name=link.company.name)
|
||||
for link in destination.company_links
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
class NotificationTestResult(BaseModel):
|
||||
success: bool
|
||||
error: str | None = None
|
||||
@@ -0,0 +1,28 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
from app.models.enums import ReportType
|
||||
|
||||
|
||||
class ReportListItem(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
report_type: ReportType
|
||||
title: str
|
||||
executive_summary: str
|
||||
model_provider: str
|
||||
model_name: str
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class ReportResponse(ReportListItem):
|
||||
company_id: uuid.UUID
|
||||
monitoring_run_id: uuid.UUID | None
|
||||
structured_report: dict[str, Any]
|
||||
prompt_version: str
|
||||
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class SnapshotResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
source_id: uuid.UUID
|
||||
snapshot_type: str
|
||||
hash: str
|
||||
text_summary: str | None
|
||||
structured_summary: dict[str, Any]
|
||||
monitoring_run_id: uuid.UUID | None
|
||||
created_at: datetime
|
||||
@@ -0,0 +1,73 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
from app.models.enums import MonitoringFrequency, SourceStatus, SourceType
|
||||
|
||||
# Only these types accept a user-supplied URL via the API. Every other type
|
||||
# (website/github/sec_edgar/job_posting) is populated by discovery instead,
|
||||
# and patent/review only ever activate through a configured fixture key.
|
||||
_USER_CREATABLE_TYPES = {SourceType.CUSTOM_URL, SourceType.RSS}
|
||||
|
||||
|
||||
class SourceCreate(BaseModel):
|
||||
source_type: SourceType
|
||||
name: str = Field(min_length=1, max_length=200)
|
||||
base_url: str = Field(min_length=1, max_length=500)
|
||||
|
||||
@field_validator("source_type")
|
||||
@classmethod
|
||||
def _validate_type(cls, value: SourceType) -> SourceType:
|
||||
if value not in _USER_CREATABLE_TYPES:
|
||||
raise ValueError(
|
||||
f"{value.value} sources are created by discovery, not added directly. "
|
||||
f"Only {', '.join(t.value for t in _USER_CREATABLE_TYPES)} may be added here."
|
||||
)
|
||||
return value
|
||||
|
||||
@field_validator("base_url")
|
||||
@classmethod
|
||||
def _normalize_url(cls, value: str) -> str:
|
||||
value = value.strip()
|
||||
if not (value.startswith("http://") or value.startswith("https://")):
|
||||
value = f"https://{value}"
|
||||
return value
|
||||
|
||||
|
||||
class SourceUpdate(BaseModel):
|
||||
name: str | None = Field(default=None, min_length=1, max_length=200)
|
||||
active: bool | None = None
|
||||
# None means "inherit the company's default cadence" - the default for
|
||||
# every source. Sending frequency_type: null explicitly clears an
|
||||
# existing override back to that default.
|
||||
frequency_type: MonitoringFrequency | None = None
|
||||
interval_minutes: int | None = Field(default=None, ge=1)
|
||||
cron_expression: str | None = Field(default=None, max_length=120)
|
||||
|
||||
|
||||
class SourceResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
source_type: SourceType
|
||||
name: str
|
||||
base_url: str | None
|
||||
active: bool
|
||||
status: SourceStatus
|
||||
trust_score: float
|
||||
last_checked: datetime | None
|
||||
last_successful_check: datetime | None
|
||||
failure_count: int
|
||||
frequency_type: MonitoringFrequency | None
|
||||
interval_minutes: int | None
|
||||
cron_expression: str | None
|
||||
next_check: datetime | None
|
||||
|
||||
|
||||
class SourceTestResult(BaseModel):
|
||||
status: SourceStatus
|
||||
documents_found: int
|
||||
error: str | None
|
||||
@@ -0,0 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class SystemSecretStatus(BaseModel):
|
||||
key: str
|
||||
label: str
|
||||
configured: bool
|
||||
value: str | None
|
||||
|
||||
|
||||
class SetSystemSecretRequest(BaseModel):
|
||||
# Blank clears the stored override, falling back to the server's
|
||||
# .env-configured value again.
|
||||
value: str = Field(default="", max_length=2000)
|
||||
@@ -0,0 +1,41 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
class UnbanRequestPayload(BaseModel):
|
||||
message: str | None = Field(default=None, max_length=2000)
|
||||
|
||||
|
||||
class IpBanResponse(BaseModel):
|
||||
ip_address: str
|
||||
banned_at: datetime
|
||||
reason: str
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class UnbanRequestResponse(BaseModel):
|
||||
id: uuid.UUID
|
||||
ip_address: str
|
||||
message: str | None
|
||||
created_at: datetime
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
|
||||
class BanIpRequest(BaseModel):
|
||||
ip_address: str
|
||||
|
||||
@field_validator("ip_address")
|
||||
@classmethod
|
||||
def _validate_ip(cls, value: str) -> str:
|
||||
try:
|
||||
ipaddress.ip_address(value.strip())
|
||||
except ValueError as exc:
|
||||
raise ValueError("Enter a valid IPv4 or IPv6 address.") from exc
|
||||
return value.strip()
|
||||
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class UserResponse(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: uuid.UUID
|
||||
email: str
|
||||
display_name: str
|
||||
timezone: str
|
||||
is_active: bool
|
||||
is_admin: bool
|
||||
|
||||
|
||||
class MeResponse(UserResponse):
|
||||
auth_mode: str
|
||||
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class UserApiKeyStatus(BaseModel):
|
||||
provider: str
|
||||
label: str
|
||||
configured: bool
|
||||
value: str | None
|
||||
credits: int | None
|
||||
credits_note: str | None
|
||||
free: bool
|
||||
requires_government_id: bool
|
||||
|
||||
|
||||
class SetUserApiKeyRequest(BaseModel):
|
||||
# Blank clears the user's override, falling back to the server's
|
||||
# global key for that provider again.
|
||||
key: str = Field(default="", max_length=500)
|
||||
Reference in New Issue
Block a user