Skip to content
This repository was archived by the owner on Mar 15, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ jobs:
- name: Run mypy type checker
run: uv run mypy src/

- name: Check AdCP schemas are up to date
run: |
uv run python scripts/generate_schemas.py
if ! git diff --exit-code src/creative_agent/schemas_generated/; then
echo "❌ Generated schemas are out of sync!"
echo "Run: python scripts/generate_schemas.py"
exit 1
fi

- name: Run smoke tests
run: uv run pytest tests/smoke/ -v --no-cov -m smoke

Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ htmlcov/
.cache
nosetests.xml
coverage.xml
coverage.json
*.cover
*.py.cover
.hypothesis/
Expand Down Expand Up @@ -210,3 +211,6 @@ __marimo__/
/test_*.py
/test_*.json
/setup_bucket_policy.py

# Schema generation temporary files
temp_resolved_schemas/
3 changes: 3 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,16 @@ repos:
hooks:
- id: ruff
args: [--fix]
exclude: ^(scripts/|src/creative_agent/schemas_generated/)
- id: ruff-format
exclude: ^src/creative_agent/schemas_generated/

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.18.2
hooks:
- id: mypy
files: ^src/
exclude: ^src/creative_agent/schemas_generated/
additional_dependencies:
- pydantic>=2.0.0
- types-pillow>=10.0.0
Expand Down
1 change: 0 additions & 1 deletion coverage.json

This file was deleted.

1 change: 1 addition & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ exclude = (?x)(
| ^\.git/
| ^__pycache__/
| \.pyc$
| ^src/creative_agent/schemas_generated/
)

# Per-module settings - keep tests less strict
Expand Down
24 changes: 24 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ dev = [
"pytest-cov>=6.2.1",
"pytest-mock>=3.14.1",
"ruff>=0.8.0",
# Schema generation
"datamodel-code-generator>=0.26.0",
"jsonref>=1.1.0",
# Type stubs
"boto3-stubs[s3]>=1.35.0",
"types-pillow>=10.0.0",
Expand All @@ -52,6 +55,7 @@ exclude = [
".venv",
"build",
"dist",
"src/creative_agent/schemas_generated",
]

[tool.ruff.lint]
Expand Down Expand Up @@ -110,9 +114,29 @@ ignore = [
"ANN", # type annotations (less strict in tests)
"PLR2004", # magic values (ok in tests)
]
"scripts/*" = [
"PTH123", # open() is acceptable in scripts
"S603", # subprocess with list is safe
"ANN201", # type annotations less critical in scripts
]

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"

[tool.coverage.run]
branch = true
source = ["src/creative_agent"]
omit = [
"tests/*",
"*/__pycache__/*",
"*/.venv/*",
"src/creative_agent/schemas_generated/*",
]

[tool.coverage.report]
precision = 2
show_missing = true
skip_covered = false
1 change: 1 addition & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ omit =
tests/*
*/__pycache__/*
*/.venv/*
src/creative_agent/schemas_generated/*

[coverage:report]
precision = 2
Expand Down
110 changes: 110 additions & 0 deletions scripts/convert_to_adcp_dicts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
"""Convert AssetRequirement and FormatRequirements to AdCP-compliant dicts."""

import re
from pathlib import Path


def convert_asset_requirement(match: re.Match) -> str:
"""Convert AssetRequirement(...) to AdCP dict format."""
content = match.group(1)

# Extract fields
asset_role = re.search(r'asset_role="([^"]+)"', content)
asset_type = re.search(r'asset_type="([^"]+)"', content)
required = re.search(r"required=(\w+)", content)

# Build dict
result = "{\n"

# asset_id = asset_role
if asset_role:
result += f' "asset_id": "{asset_role.group(1)}",\n'
result += f' "asset_type": "{asset_type.group(1) if asset_type else "text"}",\n'
result += f' "asset_role": "{asset_role.group(1)}",\n'

if required:
result += f' "required": {required.group(1)},\n'

# Collect other fields into requirements dict
req_dict = {}
for line in content.split("\n"):
line = line.strip()
if not line:
continue
# Skip already handled fields
if any(x in line for x in ["asset_role=", "asset_type=", "required="]):
continue
# Extract field name and value
if "=" in line:
field_match = re.match(r"(\w+)=(.+?)(?:,|$)", line)
if field_match:
field_name = field_match.group(1)
field_value = field_match.group(2).strip(",")
req_dict[field_name] = field_value

if req_dict:
result += ' "requirements": {\n'
for name, value in req_dict.items():
result += f' "{name}": {value},\n'
result += " },\n"

result += " }"
return result


def convert_format_requirements(match: re.Match) -> str:
"""Convert FormatRequirements(...) to dict."""
content = match.group(1)

result = "{\n"
for line in content.split("\n"):
line = line.strip()
if not line or line == ")":
continue
# Extract field name and value
field_match = re.match(r"(\w+)=(.+?)(?:,|$)", line)
if field_match:
field_name = field_match.group(1)
field_value = field_match.group(2).strip(",")
result += f' "{field_name}": {field_value},\n'
result += " }"
return result


def main():
file_path = Path(__file__).parent.parent / "src/creative_agent/data/standard_formats.py"
content = file_path.read_text()

# Remove imports
content = re.sub(
r"from \.\.schemas import AssetRequirement, CreativeFormat, FormatRequirements",
"from ..schemas import CreativeFormat",
content,
)

# Convert AssetRequirement
content = re.sub(
r"AssetRequirement\(((?:[^()]|\([^)]*\))*)\)",
convert_asset_requirement,
content,
flags=re.MULTILINE | re.DOTALL,
)

# Convert FormatRequirements
content = re.sub(
r"FormatRequirements\(((?:[^()]|\([^)]*\))*)\)",
convert_format_requirements,
content,
flags=re.MULTILINE | re.DOTALL,
)

# Fix dimensions field (move to requirements)
# This is complex, so we'll do it manually after

file_path.write_text(content)
print(f"✓ Converted {file_path}")


if __name__ == "__main__":
main()
Loading