diff --git a/schemas/cache/1.0.0/deployment.json b/schemas/cache/1.0.0/deployment.json new file mode 100644 index 00000000..c617943e --- /dev/null +++ b/schemas/cache/1.0.0/deployment.json @@ -0,0 +1,91 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/core/deployment.json", + "title": "Deployment", + "description": "A signal deployment to a specific destination platform with activation status and key", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "const": "platform", + "description": "Discriminator indicating this is a platform-based deployment" + }, + "platform": { + "type": "string", + "description": "Platform identifier for DSPs" + }, + "account": { + "type": "string", + "description": "Account identifier if applicable" + }, + "is_live": { + "type": "boolean", + "description": "Whether signal is currently active on this destination" + }, + "activation_key": { + "$ref": "activation-key.json", + "description": "The key to use for targeting. Only present if is_live=true AND requester has access to this destination." + }, + "estimated_activation_duration_minutes": { + "type": "number", + "description": "Estimated time to activate if not live, or to complete activation if in progress", + "minimum": 0 + }, + "deployed_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when activation completed (if is_live=true)" + } + }, + "required": [ + "type", + "platform", + "is_live" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "agent", + "description": "Discriminator indicating this is an agent URL-based deployment" + }, + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL identifying the destination agent" + }, + "account": { + "type": "string", + "description": "Account identifier if applicable" + }, + "is_live": { + "type": "boolean", + "description": "Whether signal is currently active on this destination" + }, + "activation_key": { + "$ref": "activation-key.json", + "description": "The key to use for targeting. Only present if is_live=true AND requester has access to this destination." + }, + "estimated_activation_duration_minutes": { + "type": "number", + "description": "Estimated time to activate if not live, or to complete activation if in progress", + "minimum": 0 + }, + "deployed_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp when activation completed (if is_live=true)" + } + }, + "required": [ + "type", + "agent_url", + "is_live" + ], + "additionalProperties": false + } + ] +} \ No newline at end of file diff --git a/schemas/cache/1.0.0/destination.json b/schemas/cache/1.0.0/destination.json new file mode 100644 index 00000000..562a2655 --- /dev/null +++ b/schemas/cache/1.0.0/destination.json @@ -0,0 +1,53 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "/schemas/v1/core/destination.json", + "title": "Destination", + "description": "A destination platform where signals can be activated (DSP, sales agent, etc.)", + "oneOf": [ + { + "type": "object", + "properties": { + "type": { + "const": "platform", + "description": "Discriminator indicating this is a platform-based destination" + }, + "platform": { + "type": "string", + "description": "Platform identifier for DSPs (e.g., 'the-trade-desk', 'amazon-dsp')" + }, + "account": { + "type": "string", + "description": "Optional account identifier on the platform" + } + }, + "required": [ + "type", + "platform" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "const": "agent", + "description": "Discriminator indicating this is an agent URL-based destination" + }, + "agent_url": { + "type": "string", + "format": "uri", + "description": "URL identifying the destination agent (for sales agents, etc.)" + }, + "account": { + "type": "string", + "description": "Optional account identifier on the agent" + } + }, + "required": [ + "type", + "agent_url" + ], + "additionalProperties": false + } + ] +} \ No newline at end of file diff --git a/scripts/generate_models_simple.py b/scripts/generate_models_simple.py index 5f706bf0..a02feee6 100755 --- a/scripts/generate_models_simple.py +++ b/scripts/generate_models_simple.py @@ -66,6 +66,102 @@ def escape_string_for_python(text: str) -> str: return text.strip() +def generate_discriminated_union(schema: dict, base_name: str) -> str: + """ + Generate Pydantic models for a discriminated union (oneOf with type discriminator). + + Creates a base model for each variant and a union type for the parent. + For example, Destination = PlatformDestination | AgentDestination + """ + lines = [] + + # Add schema description as a comment + if "description" in schema: + desc = escape_string_for_python(schema["description"]) + lines.append(f"# {desc}") + lines.append("") + + variant_names = [] + + # Generate a model for each variant in oneOf + for i, variant in enumerate(schema.get("oneOf", [])): + # Try to get discriminator value for better naming + discriminator_value = None + if "properties" in variant and "type" in variant["properties"]: + type_prop = variant["properties"]["type"] + if "const" in type_prop: + discriminator_value = type_prop["const"] + + # Generate variant name + if discriminator_value: + variant_name = f"{discriminator_value.capitalize()}{base_name}" + else: + variant_name = f"{base_name}Variant{i+1}" + + variant_names.append(variant_name) + + # Generate the variant model + lines.append(f"class {variant_name}(BaseModel):") + + # Add description if available + if "description" in variant: + desc = variant["description"].replace("\\", "\\\\").replace('"""', '\\"\\"\\"') + desc = desc.replace("\n", " ").replace("\r", "") + desc = re.sub(r"\s+", " ", desc).strip() + lines.append(f' """{desc}"""') + lines.append("") + + # Add properties + if "properties" in variant and variant["properties"]: + for prop_name, prop_schema in variant["properties"].items(): + safe_name, needs_alias = sanitize_field_name(prop_name) + prop_type = get_python_type(prop_schema) + desc = prop_schema.get("description", "") + if desc: + desc = escape_string_for_python(desc) + + is_required = prop_name in variant.get("required", []) + + if is_required: + if desc and needs_alias: + lines.append( + f' {safe_name}: {prop_type} = Field(alias="{prop_name}", description="{desc}")' + ) + elif desc: + lines.append(f' {safe_name}: {prop_type} = Field(description="{desc}")') + elif needs_alias: + lines.append(f' {safe_name}: {prop_type} = Field(alias="{prop_name}")') + else: + lines.append(f" {safe_name}: {prop_type}") + else: + if desc and needs_alias: + lines.append( + f' {safe_name}: {prop_type} | None = Field(None, alias="{prop_name}", description="{desc}")' + ) + elif desc: + lines.append( + f' {safe_name}: {prop_type} | None = Field(None, description="{desc}")' + ) + elif needs_alias: + lines.append( + f' {safe_name}: {prop_type} | None = Field(None, alias="{prop_name}")' + ) + else: + lines.append(f" {safe_name}: {prop_type} | None = None") + else: + lines.append(" pass") + + lines.append("") + lines.append("") + + # Create union type + union_type = " | ".join(variant_names) + lines.append(f"# Union type for {schema.get('title', base_name)}") + lines.append(f"{base_name} = {union_type}") + + return "\n".join(lines) + + def generate_model_for_schema(schema_file: Path) -> str: """Generate Pydantic model code for a single schema inline.""" with open(schema_file) as f: @@ -74,6 +170,10 @@ def generate_model_for_schema(schema_file: Path) -> str: # Start with model name model_name = snake_to_pascal(schema_file.stem) + # Check if this is a oneOf discriminated union + if "oneOf" in schema and "properties" not in schema: + return generate_discriminated_union(schema, model_name) + # Check if this is a simple type alias (enum or primitive type without properties) if "properties" not in schema: # This is a type alias, not a model class @@ -155,6 +255,13 @@ def get_python_type(schema: dict) -> str: ref = schema["$ref"] return snake_to_pascal(ref.replace(".json", "")) + # Handle const (discriminator values) + if "const" in schema: + const_value = schema["const"] + if isinstance(const_value, str): + return f'Literal["{const_value}"]' + return f"Literal[{const_value}]" + schema_type = schema.get("type") if schema_type == "string": @@ -387,6 +494,8 @@ def main(): "protocol-envelope.json", "response.json", "promoted-products.json", + "destination.json", + "deployment.json", # Enum types (need type aliases) "channels.json", "delivery-type.json", @@ -436,6 +545,7 @@ def main(): "", "# These types are referenced in schemas but don't have schema files", "# Defining them as type aliases to maintain type safety", + "ActivationKey = dict[str, Any]", "PackageRequest = dict[str, Any]", "PushNotificationConfig = dict[str, Any]", "ReportingCapabilities = dict[str, Any]", diff --git a/src/adcp/types/generated.py b/src/adcp/types/generated.py index f8bf1a6e..a9c0c416 100644 --- a/src/adcp/types/generated.py +++ b/src/adcp/types/generated.py @@ -23,6 +23,7 @@ # These types are referenced in schemas but don't have schema files # Defining them as type aliases to maintain type safety +ActivationKey = dict[str, Any] PackageRequest = dict[str, Any] PushNotificationConfig = dict[str, Any] ReportingCapabilities = dict[str, Any] @@ -125,9 +126,22 @@ class BrandManifest(BaseModel): metadata: dict[str, Any] | None = Field(None, description="Additional brand metadata") -# Type alias for Brand Manifest Reference # Brand manifest provided either as an inline object or a URL string pointing to a hosted manifest -BrandManifestRef = Any + +class BrandManifestRefVariant1(BaseModel): + """Inline brand manifest object""" + + pass + + +class BrandManifestRefVariant2(BaseModel): + """URL to a hosted brand manifest JSON file. The manifest at this URL must conform to the brand-manifest.json schema.""" + + pass + + +# Union type for Brand Manifest Reference +BrandManifestRef = BrandManifestRefVariant1 | BrandManifestRefVariant2 class Format(BaseModel): @@ -255,9 +269,22 @@ class PerformanceFeedback(BaseModel): applied_at: str | None = Field(None, description="ISO 8601 timestamp when feedback was applied to optimization algorithms") -# Type alias for Start Timing # Campaign start timing: 'asap' or ISO 8601 date-time -StartTiming = Any + +class StartTimingVariant1(BaseModel): + """Start campaign as soon as possible""" + + pass + + +class StartTimingVariant2(BaseModel): + """Scheduled start date/time in ISO 8601 format""" + + pass + + +# Union type for Start Timing +StartTiming = StartTimingVariant1 | StartTimingVariant2 class SubAsset(BaseModel): @@ -314,6 +341,50 @@ class PromotedProducts(BaseModel): manifest_query: str | None = Field(None, description="Natural language query to select products from the brand manifest (e.g., 'all Kraft Heinz pasta sauces', 'organic products under $20')") +# A destination platform where signals can be activated (DSP, sales agent, etc.) + +class PlatformDestination(BaseModel): + type: Literal["platform"] = Field(description="Discriminator indicating this is a platform-based destination") + platform: str = Field(description="Platform identifier for DSPs (e.g., 'the-trade-desk', 'amazon-dsp')") + account: str | None = Field(None, description="Optional account identifier on the platform") + + +class AgentDestination(BaseModel): + type: Literal["agent"] = Field(description="Discriminator indicating this is an agent URL-based destination") + agent_url: str = Field(description="URL identifying the destination agent (for sales agents, etc.)") + account: str | None = Field(None, description="Optional account identifier on the agent") + + +# Union type for Destination +Destination = PlatformDestination | AgentDestination + + +# A signal deployment to a specific destination platform with activation status and key + +class PlatformDeployment(BaseModel): + type: Literal["platform"] = Field(description="Discriminator indicating this is a platform-based deployment") + platform: str = Field(description="Platform identifier for DSPs") + account: str | None = Field(None, description="Account identifier if applicable") + is_live: bool = Field(description="Whether signal is currently active on this destination") + activation_key: ActivationKey | None = Field(None, description="The key to use for targeting. Only present if is_live=true AND requester has access to this destination.") + estimated_activation_duration_minutes: float | None = Field(None, description="Estimated time to activate if not live, or to complete activation if in progress") + deployed_at: str | None = Field(None, description="Timestamp when activation completed (if is_live=true)") + + +class AgentDeployment(BaseModel): + type: Literal["agent"] = Field(description="Discriminator indicating this is an agent URL-based deployment") + agent_url: str = Field(description="URL identifying the destination agent") + account: str | None = Field(None, description="Account identifier if applicable") + is_live: bool = Field(description="Whether signal is currently active on this destination") + activation_key: ActivationKey | None = Field(None, description="The key to use for targeting. Only present if is_live=true AND requester has access to this destination.") + estimated_activation_duration_minutes: float | None = Field(None, description="Estimated time to activate if not live, or to complete activation if in progress") + deployed_at: str | None = Field(None, description="Timestamp when activation completed (if is_live=true)") + + +# Union type for Deployment +Deployment = PlatformDeployment | AgentDeployment + + # Type alias for Advertising Channels # Standard advertising channels supported by AdCP Channels = Literal["display", "video", "audio", "native", "dooh", "ctv", "podcast", "retail", "social"] @@ -354,9 +425,46 @@ class PromotedProducts(BaseModel): PricingModel = Literal["cpm", "vcpm", "cpc", "cpcv", "cpv", "cpp", "flat_rate"] -# Type alias for Pricing Option # A pricing model option offered by a publisher for a product. Each pricing model has its own schema with model-specific requirements. -PricingOption = Any + +class PricingOptionVariant1(BaseModel): + pass + + +class PricingOptionVariant2(BaseModel): + pass + + +class PricingOptionVariant3(BaseModel): + pass + + +class PricingOptionVariant4(BaseModel): + pass + + +class PricingOptionVariant5(BaseModel): + pass + + +class PricingOptionVariant6(BaseModel): + pass + + +class PricingOptionVariant7(BaseModel): + pass + + +class PricingOptionVariant8(BaseModel): + pass + + +class PricingOptionVariant9(BaseModel): + pass + + +# Union type for Pricing Option +PricingOption = PricingOptionVariant1 | PricingOptionVariant2 | PricingOptionVariant3 | PricingOptionVariant4 | PricingOptionVariant5 | PricingOptionVariant6 | PricingOptionVariant7 | PricingOptionVariant8 | PricingOptionVariant9 # Type alias for Standard Format IDs