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
95 changes: 95 additions & 0 deletions .github/scripts/seed_supabase.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""
seed_supabase.py — push listings.json into the Supabase `hackathons` table (issue #11).

Maps each listing to a table row and upserts on the `id` primary key, so re-runs
are idempotent. The sync Action (#13) imports `build_row` and `upsert` from here
rather than reimplementing the mapping.

Geo columns (`lat`, `lng`, `geo_status`) are left unset; the geocoder (#12) fills them.

Requires two environment variables:
SUPABASE_URL e.g. https://<ref>.supabase.co
SUPABASE_SERVICE_KEY service_role key — bypasses RLS, so writes are allowed

The service key is never logged. Response bodies are echoed on failure; PostgREST
does not include credentials in error payloads.

Run with: python .github/scripts/seed_supabase.py
"""

import json
import os
import sys

import requests

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
LISTINGS_FILE = os.path.join(SCRIPT_DIR, "listings.json")

TABLE = "hackathons"
CHUNK_SIZE = 100
TIMEOUT = 30


def build_row(listing):
"""Translate one listings.json entry into a `hackathons` table row.

`company_name` is stored as `host`. Absent `deadline` stays NULL; absent
`featured` becomes False so the column is never NULL for the site's filters.
"""
return {
"id": listing["id"],
"host": listing["company_name"],
"title": listing["title"],
"url": listing["url"],
"locations": listing.get("locations", []),
"format": listing.get("format"),
"prize": listing.get("prize"),
"state": listing.get("state"),
"active": listing.get("active", True),
"is_visible": listing.get("is_visible", True),
"date_posted": listing.get("date_posted"),
"date_updated": listing.get("date_updated"),
"source": listing.get("source"),
"deadline": listing.get("deadline"),
"featured": bool(listing.get("featured", False)),
}


def upsert(rows, base_url, service_key):
"""Upsert rows into `hackathons`, chunked. Raises on any non-2xx response."""
endpoint = f"{base_url.rstrip('/')}/rest/v1/{TABLE}"
headers = {
"apikey": service_key,
"Authorization": f"Bearer {service_key}",
"Content-Type": "application/json",
"Prefer": "resolution=merge-duplicates,return=minimal",
}

for start in range(0, len(rows), CHUNK_SIZE):
chunk = rows[start : start + CHUNK_SIZE]
response = requests.post(endpoint, headers=headers, json=chunk, timeout=TIMEOUT)
if not response.ok:
raise RuntimeError(
f"upsert failed at row {start} with HTTP {response.status_code}: {response.text}"
)
print(f"upserted rows {start}-{start + len(chunk) - 1}")


def main():
base_url = os.environ.get("SUPABASE_URL")
service_key = os.environ.get("SUPABASE_SERVICE_KEY")
if not base_url or not service_key:
sys.exit("SUPABASE_URL and SUPABASE_SERVICE_KEY must be set")

with open(LISTINGS_FILE, encoding="utf-8") as handle:
listings = json.load(handle)

rows = [build_row(listing) for listing in listings]
upsert(rows, base_url, service_key)
print(f"seeded {len(rows)} listings into {TABLE}")


if __name__ == "__main__":
main()
36 changes: 36 additions & 0 deletions .github/scripts/test_scripts.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import util
import auto_extract as ax
import deadline_watcher as dw
import seed_supabase as seed


class ParseDeadlineDate(unittest.TestCase):
Expand Down Expand Up @@ -129,5 +130,40 @@ def test_rejects_not_found_or_bad_date(self):
self.assertIsNone(dw._accept(self._base(deadline="sometime in August")))


class BuildRow(unittest.TestCase):
def _listing(self, **overrides):
base = {
"id": "c0182709-2212-41a1-94ef-47689f05192f",
"company_name": "MIT",
"title": "HackMIT 2026",
"url": "https://hackmit.org/",
}
base.update(overrides)
return base

def test_renames_company_name_to_host(self):
row = seed.build_row(self._listing())
self.assertEqual(row["host"], "MIT")
self.assertNotIn("company_name", row)

def test_defaults_for_absent_optional_fields(self):
row = seed.build_row(self._listing())
self.assertEqual(row["locations"], [])
self.assertIsNone(row["deadline"])
self.assertFalse(row["featured"])
self.assertTrue(row["active"])
self.assertTrue(row["is_visible"])

def test_passes_through_deadline_and_featured(self):
row = seed.build_row(self._listing(deadline="2026-07-05", featured=True))
self.assertEqual(row["deadline"], "2026-07-05")
self.assertTrue(row["featured"])

def test_omits_geo_columns_for_geocoder(self):
row = seed.build_row(self._listing())
for column in ("lat", "lng", "geo_status"):
self.assertNotIn(column, row)


if __name__ == "__main__":
unittest.main()
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
.github/scripts/__pycache__/
digest.md
deadline_proposals.md

# env files
.env
.env.*
!.env.example

# node
node_modules/
6 changes: 6 additions & 0 deletions site/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Supabase — public, browser-safe values only.
# Vite inlines any VITE_* var into the shipped JS bundle at build time.
# Never put SUPABASE_SERVICE_KEY here or in .env: it bypasses row-level security.

VITE_SUPABASE_URL=https://your-project-ref.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key
Loading