Skip to content
Closed
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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ uv run hackproof discover
uv run hackproof scan
uv run hackproof reconcile
uv run hackproof export
```
uv run hackproof export-schemas
`

All dataset exports and public API response models are versioned and validated against schemas published under [pps/web/public/schema/](apps/web/public/schema/):
- **[Schema Index](apps/web/public/schema/index.json)**: Complete catalog of dataset export schemas (v1.0, v1.1, v1.2) and public API response schemas.``

## Checks

Expand Down
5 changes: 4 additions & 1 deletion apps/api/src/hackproof/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import asyncio
import logging

from hackproof.export.jobs import run_export
from hackproof.export.jobs import export_schemas, run_export
from hackproof.github.jobs import run_discover, run_scan
from hackproof.indexer.sync import run_sync
from hackproof.linking.reconcile import run_reconcile
Expand All @@ -23,6 +23,7 @@ def main() -> None:
)
commands.add_parser("reconcile", help="link chain activity to repos and build projects")
commands.add_parser("export", help="write the json and yaml export and its schema")
commands.add_parser("export-schemas", help="export JSON schemas for API models and index")
args = parser.parse_args()

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s")
Expand All @@ -38,3 +39,5 @@ def main() -> None:
asyncio.run(run_reconcile())
case "export":
asyncio.run(run_export())
case "export-schemas":
export_schemas()
78 changes: 69 additions & 9 deletions apps/api/src/hackproof/export/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@
from hackproof.db import open_pool, pool
from hackproof.export.build import build_export
from hackproof.export.models import SCHEMA_VERSION
from hackproof.export.render import json_schema, to_json, to_yaml
from hackproof.export.render import (
API_MODELS,
API_SCHEMA_VERSION,
SITE,
api_json_schema,
json_schema,
to_json,
to_yaml,
)
from hackproof.hackathon import get_hackathon

log = logging.getLogger("hackproof.export")
Expand All @@ -17,7 +25,65 @@

def _write(path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content)
path.write_text(content, encoding="utf-8")


def export_schemas() -> None:
schemas = PUBLIC / "schema"
schemas.mkdir(parents=True, exist_ok=True)

schema = json_schema()
_write(schemas / f"hackathon-export-{SCHEMA_VERSION}.json", json.dumps(schema, indent=2) + "\n")
_write(
schemas / f"hackathon-export-{SCHEMA_VERSION}.yml",
yaml.safe_dump(schema, sort_keys=False, allow_unicode=True, width=100),
)

index_entries = [
{
"name": "hackathon-export",
"category": "export",
"versions": ["1.0", "1.1", "1.2"],
"latest": "1.2",
"files": [
"hackathon-export-1.0.json",
"hackathon-export-1.0.yml",
"hackathon-export-1.1.json",
"hackathon-export-1.1.yml",
"hackathon-export-1.2.json",
"hackathon-export-1.2.yml",
],
"url": f"{SITE}/schema/hackathon-export-{SCHEMA_VERSION}.json",
}
]

for slug, model in API_MODELS.items():
model_schema = api_json_schema(model, slug)
filename = f"{slug}-{API_SCHEMA_VERSION}.schema.json"
_write(schemas / filename, json.dumps(model_schema, indent=2) + "\n")
index_entries.append(
{
"name": slug,
"category": "api",
"model": model.__name__,
"versions": [API_SCHEMA_VERSION],
"latest": API_SCHEMA_VERSION,
"files": [filename],
"url": f"{SITE}/schema/{filename}",
}
)

index_data = {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": f"{SITE}/schema/index.json",
"name": "HackProof Schema Index",
"description": (
"Index of all published schemas for HackProof datasets and public API models."
),
"schemas": index_entries,
}
_write(schemas / "index.json", json.dumps(index_data, indent=2) + "\n")
log.info("wrote %s schemas and index to %s", len(index_entries), schemas)


async def run_export() -> None:
Expand All @@ -30,11 +96,5 @@ async def run_export() -> None:
_write(data / "projects.json", to_json(export))
_write(data / "projects.yml", to_yaml(export))

schema = json_schema()
schemas = PUBLIC / "schema"
_write(schemas / f"hackathon-export-{SCHEMA_VERSION}.json", json.dumps(schema, indent=2) + "\n")
_write(
schemas / f"hackathon-export-{SCHEMA_VERSION}.yml",
yaml.safe_dump(schema, sort_keys=False, allow_unicode=True, width=100),
)
export_schemas()
log.info("wrote %s projects to %s", len(export.projects), data)
37 changes: 37 additions & 0 deletions apps/api/src/hackproof/export/render.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@
from typing import Any

import yaml
from pydantic import BaseModel

from hackproof.export.models import SCHEMA_VERSION, HackathonExport
from hackproof.schemas import (
Builder,
Contract,
CountryCount,
FailedTx,
Integration,
Project,
ProjectDetail,
Stats,
TimelinePoint,
WasmUpload,
)

SITE = "https://hackproof.vercel.app"
SCHEMA_ID = f"{SITE}/schema/hackathon-export-{SCHEMA_VERSION}.json"
API_SCHEMA_VERSION = "1.0"

API_MODELS: dict[str, type[BaseModel]] = {
"stats": Stats,
"project": Project,
"project-detail": ProjectDetail,
"builder": Builder,
"contract": Contract,
"wasm-upload": WasmUpload,
"failed-tx": FailedTx,
"timeline-point": TimelinePoint,
"integration": Integration,
"country-count": CountryCount,
}


def to_json(export: HackathonExport) -> str:
Expand All @@ -23,3 +50,13 @@ def json_schema() -> dict[str, Any]:
"$id": SCHEMA_ID,
**HackathonExport.model_json_schema(),
}


def api_json_schema(
model: type[BaseModel], slug: str, version: str = API_SCHEMA_VERSION
) -> dict[str, Any]:
return {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": f"{SITE}/schema/{slug}-{version}.schema.json",
**model.model_json_schema(),
}
Loading