Skip to content
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
10 changes: 10 additions & 0 deletions apps/api/src/hackproof/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,16 @@
);
CREATE INDEX IF NOT EXISTS repo_integrations_partner ON repo_integrations (partner);

CREATE TABLE IF NOT EXISTS repo_tools (
full_name TEXT NOT NULL,
tool TEXT NOT NULL,
method TEXT NOT NULL,
evidence TEXT NOT NULL,
path TEXT NOT NULL,
PRIMARY KEY (full_name, tool, method, evidence)
);
CREATE INDEX IF NOT EXISTS repo_tools_tool ON repo_tools (tool);

CREATE TABLE IF NOT EXISTS links (
full_name TEXT NOT NULL,
subject_kind TEXT NOT NULL,
Expand Down
34 changes: 30 additions & 4 deletions apps/api/src/hackproof/export/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
Chain,
Contract,
Contributor,
EcosystemTool,
Evidence,
HackathonExport,
IntegrationEvidence,
Expand Down Expand Up @@ -89,6 +90,13 @@
ORDER BY partner, method, evidence
"""

TOOLS_SQL = """
SELECT full_name, tool, method, evidence, path
FROM repo_tools
WHERE full_name = ANY($1)
ORDER BY tool, method, evidence
"""


def _evidence_url(kind: str, subject: str) -> str | None:
match kind:
Expand All @@ -109,18 +117,34 @@ def _group(rows: Iterable[asyncpg.Record], key: str = "full_name") -> dict[str,
return grouped


def _partners(
catalog: Catalog, rows: list[asyncpg.Record], repo_url: str
) -> list[IntegrationPartner]:
def _found(
rows: list[asyncpg.Record], key: str, repo_url: str
) -> dict[str, list[IntegrationEvidence]]:
found: dict[str, list[IntegrationEvidence]] = defaultdict(list)
for row in rows:
found[row["partner"]].append(
found[row[key]].append(
IntegrationEvidence(
method=row["method"],
detail=row["evidence"],
repo_file_url=repo_file_url(repo_url, row["path"]),
)
)
return found


def _tools(catalog: Catalog, rows: list[asyncpg.Record], repo_url: str) -> list[EcosystemTool]:
found = _found(rows, "tool", repo_url)
return [
EcosystemTool(slug=tool.slug, name=tool.name, website=tool.site, evidence=found[tool.slug])
for tool in catalog.tools
if tool.slug in found
]


def _partners(
catalog: Catalog, rows: list[asyncpg.Record], repo_url: str
) -> list[IntegrationPartner]:
found = _found(rows, "partner", repo_url)
return [
IntegrationPartner(
slug=partner.slug,
Expand Down Expand Up @@ -148,6 +172,7 @@ async def build_export(
wasm = _group(await connection.fetch(WASM_SQL, names, *span))
evidence = _group(await connection.fetch(EVIDENCE_SQL, names))
integrations = _group(await connection.fetch(INTEGRATIONS_SQL, names))
tools = _group(await connection.fetch(TOOLS_SQL, names))
catalog = get_catalog()
awards = {award.project_id: award.place for award in hackathon.awards}

Expand All @@ -174,6 +199,7 @@ async def build_export(
track=row["declared_track"] or "genesis",
track_declared=row["declared_track"] is not None,
integration_partners=_partners(catalog, integrations[row["full_name"]], row["url"]),
ecosystem_tools=_tools(catalog, tools[row["full_name"]], row["url"]),
owner=Owner(login=row["owner"], avatar_url=row["owner_avatar"], country=row["country"]),
contributors=[
Contributor(
Expand Down
16 changes: 16 additions & 0 deletions apps/api/src/hackproof/export/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ class ProjectFile(FileModel):
default_factory=dict, description="Stellar package to declared version, null when unknown"
)
integrations: list[FileIntegration] = Field(default_factory=list)
tools: list[FileIntegration] = Field(
default_factory=list, description="Stellar developer tools the repo builds with"
)
chain: FileChain
contributors: list[FileContributor]

Expand Down Expand Up @@ -177,6 +180,19 @@ def project_file(event: str, project: Project) -> ProjectFile:
)
for partner in project.integration_partners
],
tools=[
FileIntegration(
slug=tool.slug,
name=tool.name,
evidence=[
FileIntegrationEvidence(
method=item.method, detail=item.detail, file_url=item.repo_file_url
)
for item in tool.evidence
],
)
for tool in project.ecosystem_tools
],
chain=FileChain(
contracts=chain.contract_count,
wasm_uploads=chain.wasm_upload_count,
Expand Down
10 changes: 10 additions & 0 deletions apps/api/src/hackproof/export/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ class IntegrationEvidence(Model):
repo_file_url: str = Field(description="GitHub file where it was found")


class EcosystemTool(Model):
slug: str = Field(description="Stable tool id, listed in data/integrations.yaml")
name: str = Field(description="Tool name")
website: str = Field(description="Tool website")
evidence: list[IntegrationEvidence] = Field(description="Where the tool was found")


class IntegrationPartner(Model):
slug: str = Field(description="Stable partner id, listed in data/integrations.yaml")
name: str = Field(description="Partner name")
Expand Down Expand Up @@ -152,6 +159,9 @@ class Project(Model):
integration_partners: list[IntegrationPartner] = Field(
description="Stellar ecosystem partners the repo integrates, from the SCF integration list"
)
ecosystem_tools: list[EcosystemTool] = Field(
description="Stellar developer tools the repo builds with, such as Scaffold Stellar"
)
owner: Owner
contributors: list[Contributor]
chain: Chain
Expand Down
1 change: 1 addition & 0 deletions apps/api/src/hackproof/github/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,5 @@ class RepoScan(BaseModel):
dependencies: DependencyFingerprint
stellar_added_in_window: bool | None
integrations: list[Detection]
tools: list[Detection] = Field(default_factory=list)
contributors: list[Contributor]
8 changes: 7 additions & 1 deletion apps/api/src/hackproof/github/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from hackproof.github.track import declared_track
from hackproof.hackathon import Discovery
from hackproof.integrations.catalog import Catalog, get_catalog
from hackproof.integrations.detect import detect
from hackproof.integrations.detect import detect, detect_tools

TEXT_SUFFIXES = {
".rs", ".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".toml", ".json",
Expand Down Expand Up @@ -130,6 +130,7 @@ async def scan(self, repo: Candidate) -> RepoScan | None:
dependencies=dependencies,
stellar_added_in_window=added,
integrations=detect(files, self._catalog, repo.owner),
tools=detect_tools(files, self._catalog, repo.owner),
contributors=contributors_from(people),
)

Expand Down Expand Up @@ -175,6 +176,11 @@ async def store_scan(connection: asyncpg.Connection, scan: RepoScan) -> None:
"INSERT INTO repo_integrations VALUES ($1, $2, $3, $4, $5)",
[(scan.full_name, i.slug, i.method, i.evidence, i.path) for i in scan.integrations],
)
await connection.execute("DELETE FROM repo_tools WHERE full_name = $1", scan.full_name)
await connection.executemany(
"INSERT INTO repo_tools VALUES ($1, $2, $3, $4, $5)",
[(scan.full_name, t.slug, t.method, t.evidence, t.path) for t in scan.tools],
)
await connection.execute(
"""
UPDATE repos SET dependencies = $2, stellar_added_in_window = $3,
Expand Down
12 changes: 10 additions & 2 deletions apps/api/src/hackproof/integrations/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,26 @@
from hackproof.config import get_settings


class Partner(BaseModel):
class Product(BaseModel):
slug: str
name: str
category: str
site: str
packages: dict[str, list[str]] = Field(default_factory=dict)
endpoints: list[str] = Field(default_factory=list)
github_owners: list[str] = Field(default_factory=list)


class Partner(Product):
category: str


class Tool(Product):
pass


class Catalog(BaseModel):
partners: list[Partner]
tools: list[Tool] = Field(default_factory=list)

def partner(self, slug: str) -> Partner:
return next(partner for partner in self.partners if partner.slug == slug)
Expand Down
15 changes: 12 additions & 3 deletions apps/api/src/hackproof/integrations/detect.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import re
from collections.abc import Sequence
from pathlib import PurePosixPath

from hackproof.github.dependencies import ECOSYSTEM, PARSERS
from hackproof.integrations.catalog import Catalog, Partner
from hackproof.integrations.catalog import Catalog, Product
from hackproof.integrations.models import Detection, Method

PROSE_SUFFIXES = {".md", ".txt"}
Expand All @@ -12,7 +13,7 @@ def _normalize(ecosystem: str, name: str) -> str:
return name.lower().replace("_", "-") if ecosystem == "python" else name


def _declares(ecosystem: str, partner: Partner, declared: dict[str, str | None]) -> list[str]:
def _declares(ecosystem: str, partner: Product, declared: dict[str, str | None]) -> list[str]:
wanted = [_normalize(ecosystem, name) for name in partner.packages.get(ecosystem, [])]
found = []
for name in declared:
Expand All @@ -30,9 +31,17 @@ def _endpoint(host: str) -> re.Pattern[str]:


def detect(files: dict[str, str], catalog: Catalog, owner: str) -> list[Detection]:
return _detect(files, catalog.partners, owner)


def detect_tools(files: dict[str, str], catalog: Catalog, owner: str) -> list[Detection]:
return _detect(files, catalog.tools, owner)


def _detect(files: dict[str, str], products: Sequence[Product], owner: str) -> list[Detection]:
partners = [
partner
for partner in catalog.partners
for partner in products
if owner.lower() not in {name.lower() for name in partner.github_owners}
]
endpoints = {
Expand Down
38 changes: 37 additions & 1 deletion apps/api/src/hackproof/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
r.created_at, r.topics, COALESCE(r.declared_track, 'genesis') AS track,
ARRAY(SELECT i.partner FROM repo_integrations i WHERE i.full_name = p.full_name
GROUP BY i.partner ORDER BY i.partner) AS integrations,
ARRAY(SELECT t.tool FROM repo_tools t WHERE t.full_name = p.full_name
GROUP BY t.tool ORDER BY t.tool) AS tools,
count(*) OVER () AS total
FROM projects p
LEFT JOIN repos r ON r.full_name = p.full_name
Expand All @@ -31,6 +33,9 @@
SELECT 1 FROM repo_integrations i
WHERE i.full_name = p.full_name AND i.partner = ANY($8)))
AND ($9::text[] IS NULL OR p.project_id = ANY($9))
AND ($10::text[] IS NULL OR EXISTS (
SELECT 1 FROM repo_tools t
WHERE t.full_name = p.full_name AND t.tool = ANY($10)))
ORDER BY array_position(
ARRAY['verified', 'likely', 'possible', 'unattributed'], p.tier),
p.contracts DESC, p.classic_ops DESC, r.stars DESC NULLS LAST, p.last_activity DESC
Expand Down Expand Up @@ -157,6 +162,20 @@
ORDER BY partner, method, evidence
"""

TOOLS_SQL = """
SELECT t.tool AS slug, count(DISTINCT t.full_name) AS projects
FROM repo_tools t
JOIN projects p ON p.full_name = t.full_name AND p.tier IN ('verified', 'likely')
GROUP BY t.tool
"""

TOOL_USES_SQL = """
SELECT tool AS slug, method, evidence, path
FROM repo_tools
WHERE full_name = $1
ORDER BY tool, method, evidence
"""

COUNTRIES_SQL = """
SELECT u.country, count(DISTINCT c.login) AS contributors
FROM repo_contributors c
Expand Down Expand Up @@ -192,14 +211,15 @@ async def list_page(
tiers: list[str] | None = None,
track: str | None = None,
integrations: list[str] | None = None,
tools: list[str] | None = None,
winners: bool = False,
limit: int = 50,
offset: int = 0,
) -> tuple[list[dict[str, Any]], int]:
awards = places()
winner_ids = list(awards) if winners else None
args = (
(q, account, project_id, tiers, limit, offset, track, integrations, winner_ids)
(q, account, project_id, tiers, limit, offset, track, integrations, winner_ids, tools)
if resource == "projects"
else (*window(), q, account, project_id, limit, offset)
)
Expand Down Expand Up @@ -241,6 +261,22 @@ async def fetch_integration_uses(full_name: str) -> list[dict[str, Any]]:
return [{**row, "name": names[row["slug"]]} for row in map(dict, rows) if row["slug"] in names]


async def fetch_tool_uses(full_name: str) -> list[dict[str, Any]]:
async with pool().acquire() as connection:
rows = await connection.fetch(TOOL_USES_SQL, full_name)
names = {tool.slug: tool.name for tool in get_catalog().tools}
return [{**row, "name": names[row["slug"]]} for row in map(dict, rows) if row["slug"] in names]


async def fetch_tools() -> list[dict[str, Any]]:
async with pool().acquire() as connection:
counts = {row["slug"]: row["projects"] for row in await connection.fetch(TOOLS_SQL)}
return [
{**tool.model_dump(include={"slug", "name", "site"}), "projects": counts.get(tool.slug, 0)}
for tool in get_catalog().tools
]


async def fetch_integrations() -> list[dict[str, Any]]:
async with pool().acquire() as connection:
counts = {row["slug"]: row["projects"] for row in await connection.fetch(INTEGRATIONS_SQL)}
Expand Down
11 changes: 11 additions & 0 deletions apps/api/src/hackproof/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
Builder,
Contract,
CountryCount,
EcosystemTool,
FailedTx,
Integration,
Page,
Expand Down Expand Up @@ -52,12 +53,18 @@ async def integrations() -> list[Integration]:
return [Integration(**row) for row in await queries.fetch_integrations()]


@router.get("/tools")
async def tools() -> list[EcosystemTool]:
return [EcosystemTool(**row) for row in await queries.fetch_tools()]


@router.get("/projects")
async def projects(
q: Search = None,
tier: Tiers = None,
track: Annotated[Literal["genesis", "scale"] | None, Query()] = None,
integration: Annotated[list[str] | None, Query()] = None,
tool: Annotated[list[str] | None, Query()] = None,
limit: Limit = 50,
offset: Offset = 0,
) -> Page[Project]:
Expand All @@ -67,6 +74,7 @@ async def projects(
tiers=list(tier) if tier else None,
track=track,
integrations=integration,
tools=tool,
limit=limit,
offset=offset,
)
Expand Down Expand Up @@ -106,6 +114,9 @@ async def project(project_id: str) -> ProjectDetail:
if found[0]["full_name"]
else []
),
tool_uses=(
await queries.fetch_tool_uses(found[0]["full_name"]) if found[0]["full_name"] else []
),
contributors=(
await queries.fetch_contributors(found[0]["full_name"]) if found[0]["full_name"] else []
),
Expand Down
Loading
Loading