Spaces:
Sleeping
Sleeping
File size: 9,209 Bytes
f74bce1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | #!/usr/bin/env python3
"""Create a public-safe Devpost final submission receipt."""
from __future__ import annotations
import argparse
import json
import re
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from urllib.parse import parse_qsl, urlparse
ROOT = Path(__file__).resolve().parents[1]
SCHEMA = "proofframe.devpost_submission_receipt.v1"
DEFAULT_JSON = ROOT / "docs" / "assets" / "devpost-submission-receipt.json"
DEFAULT_MD = ROOT / "docs" / "assets" / "devpost-submission-receipt.md"
SAFE_QUERY_KEYS = {"ref_content", "ref_feature", "utm_source", "utm_medium", "utm_campaign"}
SECRET_QUERY_RE = re.compile(r"(?i)(token|cookie|key|secret|signature|session|auth|credential)")
def utc_now() -> str:
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
def normalize_text(value: str | None) -> str:
return (value or "").strip()
def is_placeholder(value: str | None) -> bool:
text = normalize_text(value).lower()
return not text or text.startswith(("tbd", "<", "pending", "todo"))
def parse_submitted_at(value: str | None) -> tuple[str | None, str | None]:
if is_placeholder(value):
return None, "Submitted timestamp is missing."
text = normalize_text(value)
try:
parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
except ValueError:
return None, "Submitted timestamp must be ISO-8601, for example 2026-08-03T21:00:00Z."
if parsed.tzinfo is None:
return None, "Submitted timestamp must include a timezone."
return parsed.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z"), None
def validate_project_url(value: str | None) -> tuple[str | None, list[str]]:
if is_placeholder(value):
return None, ["Public Devpost project URL is missing."]
text = normalize_text(value)
parsed = urlparse(text)
findings: list[str] = []
if parsed.scheme != "https":
findings.append("Devpost project URL must use https.")
if parsed.netloc.lower() != "devpost.com":
findings.append("Devpost project URL must use devpost.com.")
if not parsed.path.startswith("/software/") or len(parsed.path.rstrip("/").split("/")) < 3:
findings.append("Devpost project URL must point to /software/<project-slug>.")
if parsed.username or parsed.password or parsed.fragment:
findings.append("Devpost project URL must not include userinfo or fragments.")
query_keys = {key for key, _ in parse_qsl(parsed.query, keep_blank_values=True)}
unsafe_query = sorted(key for key in query_keys if key not in SAFE_QUERY_KEYS or SECRET_QUERY_RE.search(key))
if unsafe_query:
findings.append("Devpost project URL contains unsafe query parameters: " + ", ".join(unsafe_query))
clean_url = parsed._replace(query="", fragment="").geturl().rstrip("/")
return clean_url, findings
def validate_confirmation(value: str | None) -> tuple[str | None, str | None]:
text = normalize_text(value)
if len(text) < 10:
return None, "Confirmation note should briefly state that Devpost accepted/submitted the project."
if SECRET_QUERY_RE.search(text):
return None, "Confirmation note must not contain token/cookie/key/session-like text."
return text, None
def validate_screenshot(root: Path, value: str | None) -> tuple[str | None, str | None]:
if is_placeholder(value):
return None, None
path = Path(normalize_text(value))
if path.is_absolute():
try:
relative = path.resolve().relative_to(root.resolve())
except ValueError:
return None, "Screenshot path must stay inside the project."
else:
relative = path
if not str(relative).startswith("docs/assets/"):
return None, "Screenshot path must be under docs/assets/."
if relative.suffix.lower() not in {".png", ".jpg", ".jpeg", ".webp"}:
return None, "Screenshot must be a png, jpg, jpeg, or webp file."
if not (root / relative).exists():
return None, "Screenshot path does not exist."
return str(relative), None
def build_receipt(
*,
root: Path = ROOT,
project_url: str | None = None,
submitted_at: str | None = None,
confirmation_note: str | None = None,
screenshot: str | None = None,
) -> dict[str, Any]:
root = root.resolve()
clean_url, url_findings = validate_project_url(project_url)
clean_submitted_at, submitted_error = parse_submitted_at(submitted_at)
clean_confirmation, confirmation_error = validate_confirmation(confirmation_note)
clean_screenshot, screenshot_error = validate_screenshot(root, screenshot)
findings: list[dict[str, str]] = []
findings.extend({"field": "project_url", "detail": detail} for detail in url_findings)
if submitted_error:
findings.append({"field": "submitted_at", "detail": submitted_error})
if confirmation_error:
findings.append({"field": "confirmation_note", "detail": confirmation_error})
if screenshot_error:
findings.append({"field": "screenshot", "detail": screenshot_error})
ok = not findings
return {
"schema": SCHEMA,
"created_at": utc_now(),
"mode": "submitted" if ok else "pending_submission",
"ok": ok,
"project": "ProofFrame",
"event": "Backblaze Generative Media Hackathon",
"project_url": clean_url,
"submitted_at": clean_submitted_at,
"confirmation_note": clean_confirmation,
"screenshot": clean_screenshot,
"findings": findings,
"secret_policy": "Receipt stores only public Devpost URL, timestamp, and confirmation text; no cookies, browser sessions, tokens, or private form data.",
"next_actions": [
"Submit the project in Devpost after strict final gates are green.",
'Run python scripts/devpost_submission_receipt.py --project-url "$PROOFFRAME_DEVPOST_PROJECT_URL" --submitted-at "$PROOFFRAME_DEVPOST_SUBMITTED_AT" --confirmation-note "Devpost accepted/submitted the ProofFrame project."',
"Mark T042 done only after this receipt is ok.",
"Rerun python scripts/final_submission_control.py --strict-final.",
],
}
def render_markdown(report: dict[str, Any]) -> str:
lines = [
"# ProofFrame Devpost Submission Receipt",
"",
f"Mode: `{report['mode']}`",
f"OK: `{str(report['ok']).lower()}`",
f"Created: `{report['created_at']}`",
f"Project URL: {report['project_url'] or '`pending`'}",
f"Submitted at: `{report['submitted_at'] or 'pending'}`",
"",
report["secret_policy"],
"",
"## Confirmation",
"",
report["confirmation_note"] or "Pending final Devpost submission.",
"",
"## Findings",
"",
]
if report["findings"]:
lines.extend(f"- `{item['field']}`: {item['detail']}" for item in report["findings"])
else:
lines.append("- None")
lines.extend(["", "## Next Actions", ""])
lines.extend(f"- {action}" for action in report["next_actions"])
return "\n".join(lines) + "\n"
def write_outputs(report: dict[str, Any], json_path: Path, markdown_path: Path) -> None:
json_path.parent.mkdir(parents=True, exist_ok=True)
markdown_path.parent.mkdir(parents=True, exist_ok=True)
json_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
markdown_path.write_text(render_markdown(report), encoding="utf-8")
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Create a public-safe Devpost submission receipt.")
parser.add_argument("--project-url", help="Public Devpost /software/<slug> URL after final submission.")
parser.add_argument("--submitted-at", help="ISO-8601 submitted timestamp, for example 2026-08-03T21:00:00Z.")
parser.add_argument("--confirmation-note", help="Short public-safe confirmation note.")
parser.add_argument("--screenshot", help="Optional public-safe screenshot under docs/assets/.")
parser.add_argument("--json-out", type=Path, default=DEFAULT_JSON)
parser.add_argument("--markdown-out", type=Path, default=DEFAULT_MD)
parser.add_argument("--strict-final", action="store_true", help="Exit nonzero unless the receipt is complete.")
return parser
def main() -> None:
args = build_parser().parse_args()
report = build_receipt(
project_url=args.project_url,
submitted_at=args.submitted_at,
confirmation_note=args.confirmation_note,
screenshot=args.screenshot,
)
write_outputs(report, args.json_out, args.markdown_out)
print(
json.dumps(
{
"ok": report["ok"],
"mode": report["mode"],
"json": str(args.json_out),
"markdown": str(args.markdown_out),
"project_url": report["project_url"],
"findings": len(report["findings"]),
},
indent=2,
)
)
if args.strict_final and not report["ok"]:
raise SystemExit(2)
if __name__ == "__main__":
main()
|