Skip to content
Open
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ uv run hackproof reconcile
uv run hackproof export
```

Published dataset and API response JSON Schemas are indexed in [`apps/web/public/schema/index.json`](apps/web/public/schema/index.json).

## Checks

```sh
Expand Down
73 changes: 73 additions & 0 deletions apps/api/src/hackproof/export/files.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import re
from collections import Counter
from datetime import datetime
Expand All @@ -7,16 +8,40 @@
import yaml
from pydantic import BaseModel, ConfigDict, Field

from hackproof import schemas as api_schemas
from hackproof.export.models import HackathonExport, Project, Tier, Track
from hackproof.hackathon import Place

SCHEMA_FILE = "schema-hackathon-project.yml"
EVENT_FILE = "_event.yml"
EVENT_SCHEMA_FILE = "schema-event.yml"
SCHEMA_VERSION = "1.0"
SOURCE_URL = "https://github.com/devodii/hackproof"
SCHEMA_BASE_URL = f"{SOURCE_URL}/schema"
SCHEMA_COMMENT = f"# yaml-language-server: $schema=../{SCHEMA_FILE}\n"
EVENT_SCHEMA_COMMENT = f"# yaml-language-server: $schema=../{EVENT_SCHEMA_FILE}\n"

API_SCHEMA_MODELS: tuple[tuple[str, type[BaseModel]], ...] = (
("stats", api_schemas.Stats),
("project", api_schemas.Project),
("contributor", api_schemas.Contributor),
("link", api_schemas.Link),
("contract", api_schemas.Contract),
("wasm-upload", api_schemas.WasmUpload),
("failed-tx", api_schemas.FailedTx),
("integration-use", api_schemas.IntegrationUse),
("project-detail", api_schemas.ProjectDetail),
("builder", api_schemas.Builder),
("timeline-point", api_schemas.TimelinePoint),
("integration", api_schemas.Integration),
("ecosystem-tool", api_schemas.EcosystemTool),
("country-count", api_schemas.CountryCount),
("project-page", api_schemas.Page[api_schemas.Project]),
("contract-page", api_schemas.Page[api_schemas.Contract]),
("wasm-upload-page", api_schemas.Page[api_schemas.WasmUpload]),
("failed-tx-page", api_schemas.Page[api_schemas.FailedTx]),
)


class FileModel(BaseModel):
model_config = ConfigDict(extra="forbid")
Expand Down Expand Up @@ -250,3 +275,51 @@ def write_project_files(export: HackathonExport, root: Path) -> int:
)

return len(export.projects)


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


def write_api_schemas(schema_dir: Path) -> dict[str, Any]:
schema_dir.mkdir(parents=True, exist_ok=True)
entries: list[dict[str, str]] = [
{
"name": "hackathon-export",
"version": SCHEMA_VERSION,
"file": f"hackathon-export-{SCHEMA_VERSION}.json",
"$id": f"{SCHEMA_BASE_URL}/hackathon-export-{SCHEMA_VERSION}.json",
},
{
"name": "hackathon-project",
"version": SCHEMA_VERSION,
"file": SCHEMA_FILE,
"$id": f"{SCHEMA_BASE_URL}/{SCHEMA_FILE}",
},
{
"name": "event",
"version": SCHEMA_VERSION,
"file": EVENT_SCHEMA_FILE,
"$id": f"{SCHEMA_BASE_URL}/{EVENT_SCHEMA_FILE}",
},
]
for slug, model in API_SCHEMA_MODELS:
filename = f"{slug}.schema.json"
schema = api_schema(slug, model)
(schema_dir / filename).write_text(f"{json.dumps(schema, indent=2)}\n")
entries.append(
{
"name": slug,
"version": SCHEMA_VERSION,
"file": filename,
"$id": schema["$id"],
}
)
index = {"version": SCHEMA_VERSION, "schemas": entries}
(schema_dir / "index.json").write_text(f"{json.dumps(index, indent=2)}\n")
return index
4 changes: 3 additions & 1 deletion apps/api/src/hackproof/export/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
from hackproof.config import ROOT
from hackproof.db import open_pool, pool
from hackproof.export.build import build_export
from hackproof.export.files import write_project_files
from hackproof.export.files import write_api_schemas, write_project_files
from hackproof.hackathon import get_hackathon

log = logging.getLogger("hackproof.export")

DATA = ROOT / "data"
WEB_SCHEMA_DIR = ROOT / "apps" / "web" / "public" / "schema"


async def run_export() -> None:
Expand All @@ -18,4 +19,5 @@ async def run_export() -> None:
export = await build_export(connection, hackathon)

count = write_project_files(export, DATA)
write_api_schemas(WEB_SCHEMA_DIR)
log.info("wrote %s project files to %s", count, DATA / str(export.hackathon.starts_at.year))
162 changes: 162 additions & 0 deletions apps/api/tests/test_export_files.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import json
from datetime import UTC, datetime

import jsonschema
import pytest
import yaml

from hackproof import schemas as api_schemas
from hackproof.export.build import build_export
from hackproof.export.files import (
API_SCHEMA_MODELS,
EVENT_FILE,
EVENT_SCHEMA_FILE,
SCHEMA_FILE,
Expand All @@ -12,6 +17,7 @@
file_schema,
project_file,
to_file_yaml,
write_api_schemas,
write_project_files,
)
from hackproof.hackathon import get_hackathon
Expand Down Expand Up @@ -115,3 +121,159 @@ async def test_the_event_manifest_validates_and_names_the_event(scratch_db, tmp_
assert manifest["slug"] == export.hackathon.slug
assert manifest["network"] == "testnet"
assert manifest["ends_at"] > manifest["starts_at"]


def test_api_schemas_and_index_validate_sample_responses(tmp_path):
now = datetime(2026, 10, 12, 12, 0, tzinfo=UTC)
sample_project = api_schemas.Project(
project_id="repo:team/app",
full_name="team/app",
url="https://github.com/team/app",
footprint="contracts",
tier="verified",
contracts=1,
classic_ops=2,
assets=0,
builders=1,
wasm_uploads=1,
failed_txs=0,
first_activity=now,
last_activity=now,
description="Sample project",
homepage=None,
stars=5,
forks=1,
language="Rust",
license="MIT",
owner="team",
owner_avatar="https://avatars/team",
country="US",
dependencies=None,
window_commits=7,
total_commits=10,
stellar_added_in_window=True,
created_at=now,
topics=["stellar"],
track="genesis",
integrations=["privy"],
tools=[],
award="first",
)
sample_contributor = api_schemas.Contributor(
login="ana",
url="https://github.com/ana",
avatar_url="https://avatars/ana",
contributions=12,
country="PT",
)
sample_link = api_schemas.Link(
subject_kind="contract",
subject="CXYZ",
method="contract_in_repo",
path="README.md",
)
sample_contract = api_schemas.Contract(
contract_id="CXYZ",
deployer="GDEPLOYER",
wasm_hash="w1",
tx_hash="tx1",
ledger=100,
deployed_at=now,
project_id="repo:team/app",
)
sample_wasm = api_schemas.WasmUpload(
wasm_hash="w1",
uploader="GDEPLOYER",
tx_hash="tx1",
size=1024,
source_repo="team/app",
full_name="team/app",
contracts=1,
uploaded_at=now,
)
sample_failed = api_schemas.FailedTx(
tx_hash="tx_fail",
source_account="GDEPLOYER",
kind="soroban",
result_code="tx_failed",
ledger=101,
created_at=now,
)
sample_integration_use = api_schemas.IntegrationUse(
slug="privy",
name="Privy",
method="package",
evidence="@privy-io/node",
path="package.json",
)
samples = {
"stats": api_schemas.Stats(
projects=10,
verified_projects=5,
likely_projects=3,
possible_projects=2,
unattributed_accounts=1,
contracts=8,
builders=6,
contributors=15,
countries=4,
wasm_uploads=9,
failed_txs=2,
),
"project": sample_project,
"contributor": sample_contributor,
"link": sample_link,
"contract": sample_contract,
"wasm-upload": sample_wasm,
"failed-tx": sample_failed,
"integration-use": sample_integration_use,
"project-detail": api_schemas.ProjectDetail(
project=sample_project,
links=[sample_link],
integration_uses=[sample_integration_use],
tool_uses=[],
contributors=[sample_contributor],
contracts=[sample_contract],
wasm_uploads=[sample_wasm],
failed_txs=[sample_failed],
),
"builder": api_schemas.Builder(
address="GDEPLOYER",
projects=[sample_project],
contracts=[sample_contract],
wasm_uploads=[sample_wasm],
failed_txs=[sample_failed],
),
"timeline-point": api_schemas.TimelinePoint(hour=now, contracts=3),
"integration": api_schemas.Integration(
slug="privy",
name="Privy",
category="wallets",
site="https://privy.io",
projects=4,
),
"ecosystem-tool": api_schemas.EcosystemTool(
slug="scaffold-stellar",
name="Scaffold Stellar",
site="https://scaffoldstellar.org",
projects=6,
),
"country-count": api_schemas.CountryCount(country="PT", contributors=3),
"project-page": api_schemas.Page[api_schemas.Project](items=[sample_project], total=1),
"contract-page": api_schemas.Page[api_schemas.Contract](items=[sample_contract], total=1),
"wasm-upload-page": api_schemas.Page[api_schemas.WasmUpload](items=[sample_wasm], total=1),
"failed-tx-page": api_schemas.Page[api_schemas.FailedTx](items=[sample_failed], total=1),
}

index = write_api_schemas(tmp_path)
loaded_index = json.loads((tmp_path / "index.json").read_text())
assert loaded_index == index
indexed_names = {entry["name"] for entry in loaded_index["schemas"]}
assert {"hackathon-export", "hackathon-project", "event"} <= indexed_names

for slug, _ in API_SCHEMA_MODELS:
schema_path = tmp_path / f"{slug}.schema.json"
schema = json.loads(schema_path.read_text())
assert schema["$id"].endswith(f"/{slug}.schema.json")
assert schema["version"] == "1.0"
jsonschema.validate(samples[slug].model_dump(mode="json"), schema)