first commit
This commit is contained in:
2
backend/app/schemas/__init__.py
Normal file
2
backend/app/schemas/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
"""Pydantic schemas used to keep API contracts explicit and review-friendly."""
|
||||
|
||||
BIN
backend/app/schemas/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/admin.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/auth.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/auth.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/common.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/common.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/editor.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/editor.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/health.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/health.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/marketing_card.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/marketing_card.cpython-312.pyc
Normal file
Binary file not shown.
BIN
backend/app/schemas/__pycache__/runtime.cpython-312.pyc
Normal file
BIN
backend/app/schemas/__pycache__/runtime.cpython-312.pyc
Normal file
Binary file not shown.
178
backend/app/schemas/admin.py
Normal file
178
backend/app/schemas/admin.py
Normal file
@@ -0,0 +1,178 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from app.domain.admin import Experiment, ExperimentRelease, Goal, SdkConfig, Site, Variant
|
||||
from app.schemas.common import ApiModel, ItemListResponse
|
||||
|
||||
|
||||
class SiteRead(ApiModel):
|
||||
id: str
|
||||
site_key: str
|
||||
name: str
|
||||
primary_domain: str
|
||||
status: str
|
||||
site_settings: dict[str, Any] | list[Any] | None = None
|
||||
|
||||
|
||||
class SiteListResponse(ItemListResponse):
|
||||
items: list[SiteRead]
|
||||
|
||||
|
||||
class ExperimentRead(ApiModel):
|
||||
id: str
|
||||
site_id: str
|
||||
experiment_key: str
|
||||
name: str
|
||||
module_type: str
|
||||
status: str
|
||||
start_at: datetime | None = None
|
||||
end_at: datetime | None = None
|
||||
targeting_config: dict[str, Any] | list[Any] | None = None
|
||||
|
||||
|
||||
class ExperimentListResponse(ItemListResponse):
|
||||
items: list[ExperimentRead]
|
||||
|
||||
|
||||
class ExperimentCreate(ApiModel):
|
||||
site_id: str
|
||||
name: str
|
||||
module_type: str = "visual"
|
||||
status: str = "draft"
|
||||
start_at: datetime | None = None
|
||||
end_at: datetime | None = None
|
||||
targeting_config: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class ExperimentUpdate(ApiModel):
|
||||
name: str | None = None
|
||||
module_type: str | None = None
|
||||
status: str | None = None
|
||||
start_at: datetime | None = None
|
||||
end_at: datetime | None = None
|
||||
targeting_config: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class VariantRead(ApiModel):
|
||||
id: str
|
||||
experiment_id: str
|
||||
variant_key: str
|
||||
name: str
|
||||
traffic_weight: int
|
||||
content_config: dict[str, Any] | list[Any] | None = None
|
||||
|
||||
|
||||
class VariantListResponse(ItemListResponse):
|
||||
items: list[VariantRead]
|
||||
|
||||
|
||||
class VariantCreate(ApiModel):
|
||||
experiment_id: str
|
||||
name: str
|
||||
traffic_weight: int = 0
|
||||
content_config: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class VariantUpdate(ApiModel):
|
||||
name: str | None = None
|
||||
traffic_weight: int | None = None
|
||||
content_config: dict[str, Any] | None = None
|
||||
|
||||
|
||||
class ReleaseRead(ApiModel):
|
||||
id: str
|
||||
experiment_id: str
|
||||
version_no: int
|
||||
status: str
|
||||
runtime_payload: dict[str, Any] | list[Any] | None = None
|
||||
|
||||
|
||||
class ReleaseListResponse(ItemListResponse):
|
||||
items: list[ReleaseRead]
|
||||
|
||||
|
||||
class BuildReleaseRequest(ApiModel):
|
||||
experiment_id: str
|
||||
|
||||
|
||||
class ReleaseLifecycleResponse(ApiModel):
|
||||
id: str
|
||||
status: str
|
||||
version_no: int
|
||||
|
||||
|
||||
class GoalRead(ApiModel):
|
||||
id: str
|
||||
site_id: str
|
||||
goal_key: str
|
||||
name: str
|
||||
goal_type: str
|
||||
match_rule: dict[str, Any] | list[Any] | None = None
|
||||
|
||||
|
||||
class GoalListResponse(ItemListResponse):
|
||||
items: list[GoalRead]
|
||||
|
||||
|
||||
class SdkConfigRead(ApiModel):
|
||||
id: str
|
||||
site_id: str
|
||||
sdk_key: str
|
||||
status: str
|
||||
origin_url: str | None = None
|
||||
cdn_url: str | None = None
|
||||
sdk_config: dict[str, Any] | list[Any] | None = None
|
||||
|
||||
|
||||
class SdkConfigListResponse(ItemListResponse):
|
||||
items: list[SdkConfigRead]
|
||||
|
||||
|
||||
def site_to_read_model(site: Site) -> SiteRead:
|
||||
return SiteRead(
|
||||
id=site.id,
|
||||
site_key=site.site_key,
|
||||
name=site.name,
|
||||
primary_domain=site.primary_domain,
|
||||
status=site.status,
|
||||
site_settings=site.settings,
|
||||
)
|
||||
|
||||
|
||||
def experiment_to_read_model(experiment: Experiment) -> ExperimentRead:
|
||||
return ExperimentRead(**asdict(experiment))
|
||||
|
||||
|
||||
def variant_to_read_model(variant: Variant) -> VariantRead:
|
||||
return VariantRead(**asdict(variant))
|
||||
|
||||
|
||||
def release_to_read_model(release: ExperimentRelease) -> ReleaseRead:
|
||||
return ReleaseRead(**asdict(release))
|
||||
|
||||
|
||||
def goal_to_read_model(goal: Goal) -> GoalRead:
|
||||
return GoalRead(**asdict(goal))
|
||||
|
||||
|
||||
def sdk_config_to_read_model(sdk_config: SdkConfig) -> SdkConfigRead:
|
||||
return SdkConfigRead(**asdict(sdk_config))
|
||||
|
||||
|
||||
class ActivityLogRead(ApiModel):
|
||||
id: int
|
||||
action: str
|
||||
action_label: str = ""
|
||||
collection: str
|
||||
collection_label: str = ""
|
||||
item: str
|
||||
timestamp: datetime | None = None
|
||||
actor_email: str | None = None
|
||||
actor_id: str | None = None
|
||||
|
||||
|
||||
class ActivityLogListResponse(ApiModel):
|
||||
items: list[ActivityLogRead]
|
||||
40
backend/app/schemas/auth.py
Normal file
40
backend/app/schemas/auth.py
Normal file
@@ -0,0 +1,40 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.schemas.common import ApiModel
|
||||
|
||||
|
||||
class PermissionContextRead(ApiModel):
|
||||
is_admin: bool = False
|
||||
can_manage_sites: bool = False
|
||||
can_manage_experiments: bool = False
|
||||
can_manage_variants: bool = False
|
||||
can_manage_releases: bool = False
|
||||
can_manage_goals: bool = False
|
||||
can_manage_sdk_configs: bool = False
|
||||
can_use_editor: bool = False
|
||||
can_read_runtime: bool = False
|
||||
raw_permissions: list[str] = []
|
||||
|
||||
|
||||
class AuthenticatedUser(ApiModel):
|
||||
"""Normalized current-user payload used by FastAPI.
|
||||
|
||||
We keep the shape close to current frontend needs so migration can happen
|
||||
incrementally without losing role/group context.
|
||||
"""
|
||||
|
||||
id: str
|
||||
email: str | None = None
|
||||
first_name: str | None = None
|
||||
status: str | None = None
|
||||
fb_token: str | None = None
|
||||
role: dict[str, Any] | None = None
|
||||
user_group: dict[str, Any] | None = None
|
||||
domain_permissions: list[str] = []
|
||||
permissions: PermissionContextRead
|
||||
|
||||
|
||||
class AuthMeResponse(ApiModel):
|
||||
user: AuthenticatedUser
|
||||
23
backend/app/schemas/common.py
Normal file
23
backend/app/schemas/common.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
|
||||
class ApiModel(BaseModel):
|
||||
"""Base schema with loose extra handling for Directus-backed records.
|
||||
|
||||
Directus can include additional fields such as accountability metadata or
|
||||
relational expansions. Allowing extra keys keeps the DTO layer resilient
|
||||
while we gradually tighten contracts collection by collection.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(extra="allow")
|
||||
|
||||
|
||||
class ItemListResponse(ApiModel):
|
||||
"""Common list envelope so admin routes all respond in the same shape."""
|
||||
|
||||
items: list[Any]
|
||||
|
||||
76
backend/app/schemas/editor.py
Normal file
76
backend/app/schemas/editor.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from app.schemas.common import ApiModel
|
||||
|
||||
|
||||
class EditorChangeRead(ApiModel):
|
||||
id: str
|
||||
variant_id: str
|
||||
change_type: str
|
||||
selector_type: str
|
||||
selector_value: str
|
||||
sort_order: int
|
||||
payload: dict[str, Any] | list[Any] | None = None
|
||||
|
||||
|
||||
class EditorChangeWrite(ApiModel):
|
||||
id: str | None = None
|
||||
change_type: str
|
||||
selector_type: str = "css"
|
||||
selector_value: str
|
||||
sort_order: int = 0
|
||||
payload: dict[str, Any] | list[Any] | None = None
|
||||
|
||||
|
||||
class EditorChangeListResponse(ApiModel):
|
||||
items: list[EditorChangeRead]
|
||||
|
||||
|
||||
class SaveVariantChangesRequest(ApiModel):
|
||||
items: list[EditorChangeWrite]
|
||||
|
||||
|
||||
class EditorSessionCreateRequest(ApiModel):
|
||||
variant_id: str
|
||||
base_url: str
|
||||
mode: str = "edit"
|
||||
|
||||
|
||||
class EditorSessionUpdateRequest(ApiModel):
|
||||
status: str | None = None
|
||||
draft_changes: list[dict[str, Any]] | None = None
|
||||
|
||||
|
||||
class EditorSessionRead(ApiModel):
|
||||
id: str
|
||||
variant_id: str
|
||||
mode: str
|
||||
base_url: str
|
||||
actor_id: str
|
||||
actor_email: str | None = None
|
||||
status: str
|
||||
draft_changes: list[dict[str, Any]]
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class BuildPreviewRequest(ApiModel):
|
||||
variant_id: str
|
||||
items: list[EditorChangeWrite]
|
||||
|
||||
|
||||
class PreviewOperationRead(ApiModel):
|
||||
selector_type: str
|
||||
selector_value: str
|
||||
action: str
|
||||
payload: dict[str, Any] | list[Any] | None = None
|
||||
|
||||
|
||||
class BuildPreviewResponse(ApiModel):
|
||||
variant_id: str
|
||||
generated_at: datetime
|
||||
operations: list[PreviewOperationRead]
|
||||
|
||||
20
backend/app/schemas/health.py
Normal file
20
backend/app/schemas/health.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from app.schemas.common import ApiModel
|
||||
|
||||
|
||||
class HealthStatusResponse(ApiModel):
|
||||
status: str
|
||||
|
||||
|
||||
class ReadinessDependency(ApiModel):
|
||||
name: str
|
||||
configured: bool
|
||||
detail: str
|
||||
|
||||
|
||||
class ReadinessStatusResponse(ApiModel):
|
||||
status: str
|
||||
app_env: str
|
||||
app_name: str
|
||||
dependencies: list[ReadinessDependency]
|
||||
22
backend/app/schemas/marketing_card.py
Normal file
22
backend/app/schemas/marketing_card.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.schemas.common import ApiModel, ItemListResponse
|
||||
|
||||
|
||||
class MarketingCardRead(ApiModel):
|
||||
id: str
|
||||
card_name: str | None = None
|
||||
card_code: str | None = None
|
||||
origin: str | None = None
|
||||
landing_page: str | None = None
|
||||
coupon_code_url: str | None = None
|
||||
start_date: str | None = None
|
||||
end_date: str | None = None
|
||||
ose_user_updated: str | None = None
|
||||
|
||||
|
||||
class MarketingCardListResponse(ItemListResponse):
|
||||
items: list[MarketingCardRead]
|
||||
|
||||
86
backend/app/schemas/runtime.py
Normal file
86
backend/app/schemas/runtime.py
Normal file
@@ -0,0 +1,86 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.schemas.common import ApiModel
|
||||
|
||||
|
||||
class RuntimeVariantCandidateInput(ApiModel):
|
||||
id: str
|
||||
variant_key: str
|
||||
traffic_weight: int
|
||||
content_config: dict[str, Any] | list[Any] | None = None
|
||||
|
||||
|
||||
class RuntimeExperimentInput(ApiModel):
|
||||
experiment_id: str
|
||||
experiment_key: str
|
||||
status: str
|
||||
site_key: str | None = None
|
||||
assignment_salt: str | None = None
|
||||
release_id: str | None = None
|
||||
release_version: int | None = None
|
||||
payload: dict[str, Any] | list[Any] | None = None
|
||||
variants: list[RuntimeVariantCandidateInput] = []
|
||||
|
||||
|
||||
class RuntimeBootstrapRequest(ApiModel):
|
||||
site_id: str | None = None
|
||||
site_key: str | None = None
|
||||
url: str
|
||||
visitor_id: str
|
||||
user_agent: str | None = None
|
||||
|
||||
|
||||
class RuntimeBootstrapResponse(ApiModel):
|
||||
site_id: str | None = None
|
||||
site_key: str | None = None
|
||||
url: str
|
||||
visitor_id: str
|
||||
candidate_experiments: list[RuntimeExperimentInput] = []
|
||||
|
||||
|
||||
class RuntimeAssignRequest(ApiModel):
|
||||
visitor_id: str
|
||||
experiment: RuntimeExperimentInput
|
||||
|
||||
|
||||
class RuntimeAssignResponse(ApiModel):
|
||||
experiment_id: str
|
||||
experiment_key: str
|
||||
variant_id: str
|
||||
variant_key: str
|
||||
bucket: int
|
||||
reason: str
|
||||
|
||||
|
||||
class RuntimePayloadRequest(ApiModel):
|
||||
visitor_id: str
|
||||
experiment: RuntimeExperimentInput
|
||||
|
||||
|
||||
class RuntimePayloadResponse(ApiModel):
|
||||
experiment_id: str
|
||||
experiment_key: str
|
||||
release_id: str | None = None
|
||||
release_version: int | None = None
|
||||
assigned_variant_id: str | None = None
|
||||
assigned_variant_key: str | None = None
|
||||
payload: dict[str, Any] | list[Any] | None = None
|
||||
|
||||
|
||||
class RuntimeEventRequest(ApiModel):
|
||||
site_id: str | None = None
|
||||
site_key: str | None = None
|
||||
experiment_id: str | None = None
|
||||
experiment_key: str | None = None
|
||||
variant_id: str | None = None
|
||||
variant_key: str | None = None
|
||||
visitor_id: str
|
||||
event_name: str
|
||||
payload: dict[str, Any] | list[Any] | None = None
|
||||
|
||||
|
||||
class RuntimeEventResponse(ApiModel):
|
||||
accepted: bool
|
||||
event_name: str
|
||||
Reference in New Issue
Block a user