Spaces:
Running
Running
Commit ·
7e44512
1
Parent(s): 94f3b9f
formatted files
Browse files
main.py
CHANGED
|
@@ -13,25 +13,28 @@ from datetime import datetime
|
|
| 13 |
# Global database connection
|
| 14 |
db_conn = None
|
| 15 |
|
|
|
|
| 16 |
@asynccontextmanager
|
| 17 |
async def lifespan(app: FastAPI):
|
| 18 |
"""Manage application lifecycle - initialize DB on startup."""
|
| 19 |
global db_conn
|
| 20 |
url = os.getenv("DATABASE_URL")
|
| 21 |
auth_token = os.getenv("API_KEY")
|
| 22 |
-
|
| 23 |
if not url or not auth_token:
|
| 24 |
raise RuntimeError("DATABASE_URL and API_KEY environment variables must be set")
|
| 25 |
-
|
| 26 |
try:
|
| 27 |
-
db_conn = libsql.connect(
|
|
|
|
|
|
|
| 28 |
db_conn.sync()
|
| 29 |
print("Database connection established successfully")
|
| 30 |
except Exception as e:
|
| 31 |
raise RuntimeError(f"Failed to connect to database: {e}")
|
| 32 |
-
|
| 33 |
yield
|
| 34 |
-
|
| 35 |
# Cleanup on shutdown
|
| 36 |
if db_conn:
|
| 37 |
try:
|
|
@@ -39,11 +42,12 @@ async def lifespan(app: FastAPI):
|
|
| 39 |
except Exception as e:
|
| 40 |
print(f"Error closing database connection: {e}")
|
| 41 |
|
|
|
|
| 42 |
app = FastAPI(
|
| 43 |
title="Zigistry API",
|
| 44 |
description="API for searching and browsing Zig packages and programs",
|
| 45 |
version="1.0.0",
|
| 46 |
-
lifespan=lifespan
|
| 47 |
)
|
| 48 |
|
| 49 |
# CORS middleware
|
|
@@ -55,14 +59,15 @@ app.add_middleware(
|
|
| 55 |
allow_headers=["*"],
|
| 56 |
)
|
| 57 |
|
|
|
|
| 58 |
def get_default_branch_info(conn, repo_id: str, default_branch: str) -> Dict[str, Any]:
|
| 59 |
"""Get information about the default branch (build.zig.zon file).
|
| 60 |
-
|
| 61 |
Args:
|
| 62 |
conn: Database connection
|
| 63 |
repo_id: Repository identifier
|
| 64 |
default_branch: Name of the default branch
|
| 65 |
-
|
| 66 |
Returns:
|
| 67 |
Dictionary with default branch information including dependencies and minimum zig version
|
| 68 |
"""
|
|
@@ -81,19 +86,19 @@ def get_default_branch_info(conn, repo_id: str, default_branch: str) -> Dict[str
|
|
| 81 |
ORDER BY published_at DESC
|
| 82 |
LIMIT 1
|
| 83 |
"""
|
| 84 |
-
|
| 85 |
cursor = conn.execute(sql, (repo_id,))
|
| 86 |
release = cursor.fetchone()
|
| 87 |
-
|
| 88 |
if not release:
|
| 89 |
return {
|
| 90 |
"minimum_zig_version": "0.0.0",
|
| 91 |
"dependencies": [],
|
| 92 |
-
"branch_name": default_branch
|
| 93 |
}
|
| 94 |
-
|
| 95 |
release_id, version, published_at, min_zig_ver, readme_url, is_prerelease = release
|
| 96 |
-
|
| 97 |
# Get dependencies for this release
|
| 98 |
deps_sql = """
|
| 99 |
SELECT name, url, hash, lazy, path
|
|
@@ -102,37 +107,38 @@ def get_default_branch_info(conn, repo_id: str, default_branch: str) -> Dict[str
|
|
| 102 |
"""
|
| 103 |
cursor = conn.execute(deps_sql, (release_id,))
|
| 104 |
dep_rows = cursor.fetchall()
|
| 105 |
-
|
| 106 |
dependencies = [
|
| 107 |
{
|
| 108 |
"name": row[0],
|
| 109 |
"url": row[1],
|
| 110 |
"hash": row[2],
|
| 111 |
"lazy": bool(row[3]) if row[3] else False,
|
| 112 |
-
"path": row[4]
|
| 113 |
}
|
| 114 |
for row in dep_rows
|
| 115 |
]
|
| 116 |
-
|
| 117 |
return {
|
| 118 |
"minimum_zig_version": min_zig_ver or "0.0.0",
|
| 119 |
"dependencies": dependencies,
|
| 120 |
"branch_name": default_branch,
|
| 121 |
"latest_version": version,
|
| 122 |
-
"readme_url": readme_url
|
| 123 |
}
|
| 124 |
|
|
|
|
| 125 |
def get_version_info(conn, repo_id: str, version: str) -> Dict[str, Any]:
|
| 126 |
"""Get information about a specific version/release.
|
| 127 |
-
|
| 128 |
Args:
|
| 129 |
conn: Database connection
|
| 130 |
repo_id: Repository identifier
|
| 131 |
version: Version string
|
| 132 |
-
|
| 133 |
Returns:
|
| 134 |
Dictionary with version-specific information
|
| 135 |
-
|
| 136 |
Raises:
|
| 137 |
HTTPException: If version not found
|
| 138 |
"""
|
|
@@ -147,18 +153,18 @@ def get_version_info(conn, repo_id: str, version: str) -> Dict[str, Any]:
|
|
| 147 |
FROM releases
|
| 148 |
WHERE repo_id = ? AND version = ?
|
| 149 |
"""
|
| 150 |
-
|
| 151 |
cursor = conn.execute(sql, (repo_id, version))
|
| 152 |
release = cursor.fetchone()
|
| 153 |
-
|
| 154 |
if not release:
|
| 155 |
raise HTTPException(
|
| 156 |
-
status_code=404,
|
| 157 |
-
detail=f"Version '{version}' not found for repository '{repo_id}'"
|
| 158 |
)
|
| 159 |
-
|
| 160 |
release_id, ver, published_at, min_zig_ver, readme_url, is_prerelease = release
|
| 161 |
-
|
| 162 |
# Get dependencies for this version
|
| 163 |
deps_sql = """
|
| 164 |
SELECT name, url, hash, lazy, path
|
|
@@ -167,36 +173,41 @@ def get_version_info(conn, repo_id: str, version: str) -> Dict[str, Any]:
|
|
| 167 |
"""
|
| 168 |
cursor = conn.execute(deps_sql, (release_id,))
|
| 169 |
dep_rows = cursor.fetchall()
|
| 170 |
-
|
| 171 |
dependencies = [
|
| 172 |
{
|
| 173 |
"name": row[0],
|
| 174 |
"url": row[1],
|
| 175 |
"hash": row[2],
|
| 176 |
"lazy": bool(row[3]) if row[3] else False,
|
| 177 |
-
"path": row[4]
|
| 178 |
}
|
| 179 |
for row in dep_rows
|
| 180 |
]
|
| 181 |
-
|
| 182 |
return {
|
| 183 |
"version": ver,
|
| 184 |
"published_at": str(published_at) if published_at else None,
|
| 185 |
"minimum_zig_version": min_zig_ver or "0.0.0",
|
| 186 |
"readme_url": readme_url,
|
| 187 |
"is_prerelease": bool(is_prerelease),
|
| 188 |
-
"dependencies": dependencies
|
| 189 |
}
|
| 190 |
|
|
|
|
| 191 |
def row_to_repo_dict(row: tuple) -> Dict[str, Any]:
|
| 192 |
"""Convert database row to repository dictionary.
|
| 193 |
-
|
| 194 |
Handles the common transformation from SQL result to API response format.
|
| 195 |
"""
|
| 196 |
platform_raw = (row[3] or "").lower()
|
| 197 |
-
provider =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
repo_id = row[0]
|
| 199 |
-
repo_name = repo_id.split(
|
| 200 |
|
| 201 |
return {
|
| 202 |
"id": row[0],
|
|
@@ -223,6 +234,7 @@ def row_to_repo_dict(row: tuple) -> Dict[str, Any]:
|
|
| 223 |
"dependents_count": row[18] if row[18] is not None else 0,
|
| 224 |
}
|
| 225 |
|
|
|
|
| 226 |
def get_type_filter(search_type: str) -> str:
|
| 227 |
"""Generate SQL WHERE clause filter based on search type."""
|
| 228 |
if search_type == "package":
|
|
@@ -232,27 +244,27 @@ def get_type_filter(search_type: str) -> str:
|
|
| 232 |
else: # all
|
| 233 |
return "AND (pkg.repo_id IS NOT NULL OR prog.repo_id IS NOT NULL)"
|
| 234 |
|
|
|
|
| 235 |
def search_repos(
|
| 236 |
-
conn,
|
| 237 |
-
query: str,
|
| 238 |
-
search_type: str = "all",
|
| 239 |
-
limit: int = 50
|
| 240 |
) -> List[Dict[str, Any]]:
|
| 241 |
"""Search repositories by query string.
|
| 242 |
-
|
| 243 |
Args:
|
| 244 |
conn: Database connection
|
| 245 |
query: Search query string
|
| 246 |
search_type: Filter by 'package', 'program', or 'all'
|
| 247 |
limit: Maximum number of results to return
|
| 248 |
-
|
| 249 |
Returns:
|
| 250 |
List of repository dictionaries with default branch minimum_zig_version
|
| 251 |
"""
|
| 252 |
search_term_like = f"%{query}%"
|
| 253 |
# FTS5 query with prefix matching for each word
|
| 254 |
-
fts_query = " ".join(
|
| 255 |
-
|
|
|
|
|
|
|
| 256 |
type_filter = get_type_filter(search_type)
|
| 257 |
|
| 258 |
sql = f"""
|
|
@@ -302,26 +314,25 @@ def search_repos(
|
|
| 302 |
rd.stargazer_count DESC
|
| 303 |
LIMIT ?5
|
| 304 |
"""
|
| 305 |
-
|
| 306 |
starts_with = f"{query}%"
|
| 307 |
# Parameters matches ?1 (FTS), ?2 (LIKE), ?3 (Exact), ?4 (Starts with), ?5 (Limit)
|
| 308 |
cursor = conn.execute(sql, (fts_query, search_term_like, query, starts_with, limit))
|
| 309 |
rows = cursor.fetchall()
|
| 310 |
-
|
| 311 |
return [row_to_repo_dict(row) for row in rows]
|
| 312 |
|
|
|
|
| 313 |
def get_latest_repos(
|
| 314 |
-
conn,
|
| 315 |
-
search_type: str = "all",
|
| 316 |
-
limit: int = 10
|
| 317 |
) -> List[Dict[str, Any]]:
|
| 318 |
"""Get latest repositories ordered by creation date.
|
| 319 |
-
|
| 320 |
Args:
|
| 321 |
conn: Database connection
|
| 322 |
search_type: Filter by 'package', 'program', or 'all'
|
| 323 |
limit: Maximum number of results to return
|
| 324 |
-
|
| 325 |
Returns:
|
| 326 |
List of repository dictionaries with default branch minimum_zig_version
|
| 327 |
"""
|
|
@@ -362,26 +373,24 @@ def get_latest_repos(
|
|
| 362 |
(SELECT COUNT(*) FROM repo_dependents WHERE repo_id = rd.id) as dependents_count
|
| 363 |
FROM repo_data rd
|
| 364 |
"""
|
| 365 |
-
|
| 366 |
cursor = conn.execute(sql, (limit,))
|
| 367 |
rows = cursor.fetchall()
|
| 368 |
-
|
| 369 |
return [row_to_repo_dict(row) for row in rows]
|
| 370 |
|
|
|
|
| 371 |
def get_scroll_repos(
|
| 372 |
-
conn,
|
| 373 |
-
search_type: str = "all",
|
| 374 |
-
per_page: int = 20,
|
| 375 |
-
page: int = 1
|
| 376 |
) -> List[Dict[str, Any]]:
|
| 377 |
"""Get paginated repositories ordered by star count.
|
| 378 |
-
|
| 379 |
Args:
|
| 380 |
conn: Database connection
|
| 381 |
search_type: Filter by 'package', 'program', or 'all'
|
| 382 |
per_page: Number of items per page (max 20)
|
| 383 |
page: Page number (1-indexed)
|
| 384 |
-
|
| 385 |
Returns:
|
| 386 |
List of repository dictionaries with default branch minimum_zig_version
|
| 387 |
"""
|
|
@@ -424,54 +433,58 @@ def get_scroll_repos(
|
|
| 424 |
(SELECT COUNT(*) FROM repo_dependents WHERE repo_id = rd.id) as dependents_count
|
| 425 |
FROM repo_data rd
|
| 426 |
"""
|
| 427 |
-
|
| 428 |
cursor = conn.execute(sql, (actual_per_page, offset))
|
| 429 |
rows = cursor.fetchall()
|
| 430 |
-
|
| 431 |
return [row_to_repo_dict(row) for row in rows]
|
| 432 |
|
|
|
|
| 433 |
def parse_repo_id(repo_id: str) -> tuple[str, str]:
|
| 434 |
"""Parse repo_id into owner and repo name.
|
| 435 |
-
|
| 436 |
Args:
|
| 437 |
repo_id: Repository ID in format 'owner/repo' or 'provider/owner/repo'
|
| 438 |
-
|
| 439 |
Returns:
|
| 440 |
Tuple of (owner_name, repo_name)
|
| 441 |
-
|
| 442 |
Raises:
|
| 443 |
HTTPException: If repo_id format is invalid
|
| 444 |
"""
|
| 445 |
-
parts = repo_id.split(
|
| 446 |
-
|
| 447 |
if len(parts) == 3:
|
| 448 |
return parts[1], parts[2]
|
| 449 |
elif len(parts) == 2:
|
| 450 |
return parts[0], parts[1]
|
| 451 |
else:
|
| 452 |
raise HTTPException(
|
| 453 |
-
status_code=400,
|
| 454 |
-
detail="Invalid repo_id format. Expected 'provider/owner/repo' or 'owner/repo'"
|
| 455 |
)
|
| 456 |
|
| 457 |
-
|
|
|
|
|
|
|
|
|
|
| 458 |
"""Get detailed information about a repository.
|
| 459 |
-
|
| 460 |
Args:
|
| 461 |
conn: Database connection
|
| 462 |
repo_id: Repository identifier
|
| 463 |
version: Optional specific version to fetch details for
|
| 464 |
-
|
| 465 |
Returns:
|
| 466 |
Dictionary with complete repository details including:
|
| 467 |
- If version is None: default branch information with dependencies
|
| 468 |
- If version is specified: that specific version's information with dependencies
|
| 469 |
-
|
| 470 |
Raises:
|
| 471 |
HTTPException: If repository or version not found
|
| 472 |
"""
|
| 473 |
owner_name, repo_name = parse_repo_id(repo_id)
|
| 474 |
-
|
| 475 |
# Get repository details
|
| 476 |
repo_sql = """
|
| 477 |
SELECT
|
|
@@ -481,24 +494,42 @@ def get_repo_details(conn, repo_id: str, version: Optional[str] = None) -> Dict[
|
|
| 481 |
license, primary_language, minimum_zig_version
|
| 482 |
FROM repos WHERE id = ?
|
| 483 |
"""
|
| 484 |
-
|
| 485 |
cursor = conn.execute(repo_sql, (repo_id,))
|
| 486 |
repo_row = cursor.fetchone()
|
| 487 |
-
|
| 488 |
if not repo_row:
|
| 489 |
raise HTTPException(status_code=404, detail="Repository not found")
|
| 490 |
-
|
| 491 |
(
|
| 492 |
-
r_id,
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
) = repo_row
|
| 497 |
-
|
| 498 |
# Map platform to provider
|
| 499 |
platform_raw = (platform or "").lower()
|
| 500 |
-
provider_id =
|
| 501 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
# Get all releases for the releases list
|
| 503 |
releases_sql = """
|
| 504 |
SELECT version
|
|
@@ -508,12 +539,12 @@ def get_repo_details(conn, repo_id: str, version: Optional[str] = None) -> Dict[
|
|
| 508 |
"""
|
| 509 |
cursor = conn.execute(releases_sql, (repo_id,))
|
| 510 |
releases_list = [row[0] for row in cursor.fetchall() if row[0]]
|
| 511 |
-
|
| 512 |
# Get dependents (same for all versions)
|
| 513 |
dependents_sql = "SELECT dependent FROM repo_dependents WHERE repo_id = ?"
|
| 514 |
cursor = conn.execute(dependents_sql, (repo_id,))
|
| 515 |
dependents = [row[0] for row in cursor.fetchall()]
|
| 516 |
-
|
| 517 |
# Build response with either version-specific or default branch info
|
| 518 |
response = {
|
| 519 |
"id": r_id,
|
|
@@ -541,103 +572,125 @@ def get_repo_details(conn, repo_id: str, version: Optional[str] = None) -> Dict[
|
|
| 541 |
"releases": releases_list,
|
| 542 |
"dependents": dependents,
|
| 543 |
}
|
| 544 |
-
|
| 545 |
if version:
|
| 546 |
# Get specific version info
|
| 547 |
version_info = get_version_info(conn, repo_id, version)
|
| 548 |
-
response.update(
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
|
|
|
|
|
|
| 557 |
else:
|
| 558 |
# Get default branch info
|
| 559 |
branch_info = get_default_branch_info(conn, repo_id, default_branch)
|
| 560 |
-
response.update(
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
|
|
|
|
|
|
|
|
|
| 569 |
return response
|
| 570 |
|
|
|
|
| 571 |
def check_db_connection():
|
| 572 |
"""Verify database connection is available."""
|
| 573 |
if not db_conn:
|
| 574 |
raise HTTPException(status_code=503, detail="Database not initialized")
|
| 575 |
|
|
|
|
| 576 |
# API Endpoints
|
| 577 |
|
|
|
|
| 578 |
@app.get("/search/packages", tags=["Search"])
|
| 579 |
async def search_packages_endpoint(
|
| 580 |
q: str = Query(..., min_length=1, description="Search query"),
|
| 581 |
-
limit: int = Query(50, ge=1, le=100, description="Maximum results")
|
| 582 |
):
|
| 583 |
"""Search for Zig packages. Returns default branch minimum_zig_version for each result."""
|
| 584 |
check_db_connection()
|
| 585 |
return search_repos(db_conn, q, search_type="package", limit=limit)
|
| 586 |
|
|
|
|
| 587 |
@app.get("/search/programs", tags=["Search"])
|
| 588 |
async def search_programs_endpoint(
|
| 589 |
q: str = Query(..., min_length=1, description="Search query"),
|
| 590 |
-
limit: int = Query(50, ge=1, le=100, description="Maximum results")
|
| 591 |
):
|
| 592 |
"""Search for Zig programs. Returns default branch minimum_zig_version for each result."""
|
| 593 |
check_db_connection()
|
| 594 |
return search_repos(db_conn, q, search_type="program", limit=limit)
|
| 595 |
|
|
|
|
| 596 |
@app.get("/packages/latest", tags=["Packages"])
|
| 597 |
async def get_latest_packages_endpoint(
|
| 598 |
-
limit: int = Query(10, ge=1, le=50, description="Number of packages")
|
| 599 |
):
|
| 600 |
"""Get latest Zig packages. Returns default branch minimum_zig_version for each result."""
|
| 601 |
check_db_connection()
|
| 602 |
return get_latest_repos(db_conn, search_type="package", limit=limit)
|
| 603 |
|
|
|
|
| 604 |
@app.get("/programs/latest", tags=["Programs"])
|
| 605 |
async def get_latest_programs_endpoint(
|
| 606 |
-
limit: int = Query(10, ge=1, le=50, description="Number of programs")
|
| 607 |
):
|
| 608 |
"""Get latest Zig programs. Returns default branch minimum_zig_version for each result."""
|
| 609 |
check_db_connection()
|
| 610 |
return get_latest_repos(db_conn, search_type="program", limit=limit)
|
| 611 |
|
|
|
|
| 612 |
@app.get("/packages/scroll", tags=["Packages"])
|
| 613 |
async def scroll_packages_endpoint(
|
| 614 |
per_page: int = Query(20, ge=1, le=20, description="Items per page"),
|
| 615 |
-
page: int = Query(1, ge=1, description="Page number")
|
| 616 |
):
|
| 617 |
"""Get paginated list of packages sorted by stars. Returns default branch minimum_zig_version for each result."""
|
| 618 |
check_db_connection()
|
| 619 |
-
return get_scroll_repos(
|
|
|
|
|
|
|
|
|
|
| 620 |
|
| 621 |
@app.get("/programs/scroll", tags=["Programs"])
|
| 622 |
async def scroll_programs_endpoint(
|
| 623 |
per_page: int = Query(20, ge=1, le=20, description="Items per page"),
|
| 624 |
-
page: int = Query(1, ge=1, description="Page number")
|
| 625 |
):
|
| 626 |
"""Get paginated list of programs sorted by stars. Returns default branch minimum_zig_version for each result."""
|
| 627 |
check_db_connection()
|
| 628 |
-
return get_scroll_repos(
|
|
|
|
|
|
|
|
|
|
| 629 |
|
| 630 |
@app.get("/packages", tags=["Packages"])
|
| 631 |
async def get_package_details_endpoint(
|
| 632 |
-
q: str = Query(
|
| 633 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 634 |
):
|
| 635 |
"""
|
| 636 |
Get detailed information about a package.
|
| 637 |
-
|
| 638 |
- Without version parameter: Returns default branch information with dependencies
|
| 639 |
- With version parameter: Returns that specific version's information with dependencies
|
| 640 |
-
|
| 641 |
Examples:
|
| 642 |
- /packages?q=gh/rohanvashisht1234/zorsig (default branch)
|
| 643 |
- /packages?q=gh/rohanvashisht1234/zorsig&version=0.0.1 (specific version)
|
|
@@ -645,17 +698,22 @@ async def get_package_details_endpoint(
|
|
| 645 |
check_db_connection()
|
| 646 |
return get_repo_details(db_conn, q, version)
|
| 647 |
|
|
|
|
| 648 |
@app.get("/programs", tags=["Programs"])
|
| 649 |
async def get_program_details_endpoint(
|
| 650 |
-
q: str = Query(
|
| 651 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 652 |
):
|
| 653 |
"""
|
| 654 |
Get detailed information about a program.
|
| 655 |
-
|
| 656 |
- Without version parameter: Returns default branch information with dependencies
|
| 657 |
- With version parameter: Returns that specific version's information with dependencies
|
| 658 |
-
|
| 659 |
Examples:
|
| 660 |
- /programs?q=gh/owner/program (default branch)
|
| 661 |
- /programs?q=gh/owner/program&version=1.0.0 (specific version)
|
|
@@ -663,14 +721,14 @@ async def get_program_details_endpoint(
|
|
| 663 |
check_db_connection()
|
| 664 |
return get_repo_details(db_conn, q, version)
|
| 665 |
|
|
|
|
| 666 |
@app.get("/health", tags=["System"])
|
| 667 |
async def health_check():
|
| 668 |
"""Check API health status."""
|
| 669 |
-
return {
|
| 670 |
-
|
| 671 |
-
"database": "connected" if db_conn else "disconnected"
|
| 672 |
-
}
|
| 673 |
|
| 674 |
if __name__ == "__main__":
|
| 675 |
import uvicorn
|
| 676 |
-
|
|
|
|
|
|
| 13 |
# Global database connection
|
| 14 |
db_conn = None
|
| 15 |
|
| 16 |
+
|
| 17 |
@asynccontextmanager
|
| 18 |
async def lifespan(app: FastAPI):
|
| 19 |
"""Manage application lifecycle - initialize DB on startup."""
|
| 20 |
global db_conn
|
| 21 |
url = os.getenv("DATABASE_URL")
|
| 22 |
auth_token = os.getenv("API_KEY")
|
| 23 |
+
|
| 24 |
if not url or not auth_token:
|
| 25 |
raise RuntimeError("DATABASE_URL and API_KEY environment variables must be set")
|
| 26 |
+
|
| 27 |
try:
|
| 28 |
+
db_conn = libsql.connect(
|
| 29 |
+
"zigistry-main.db", sync_url=url, auth_token=auth_token
|
| 30 |
+
)
|
| 31 |
db_conn.sync()
|
| 32 |
print("Database connection established successfully")
|
| 33 |
except Exception as e:
|
| 34 |
raise RuntimeError(f"Failed to connect to database: {e}")
|
| 35 |
+
|
| 36 |
yield
|
| 37 |
+
|
| 38 |
# Cleanup on shutdown
|
| 39 |
if db_conn:
|
| 40 |
try:
|
|
|
|
| 42 |
except Exception as e:
|
| 43 |
print(f"Error closing database connection: {e}")
|
| 44 |
|
| 45 |
+
|
| 46 |
app = FastAPI(
|
| 47 |
title="Zigistry API",
|
| 48 |
description="API for searching and browsing Zig packages and programs",
|
| 49 |
version="1.0.0",
|
| 50 |
+
lifespan=lifespan,
|
| 51 |
)
|
| 52 |
|
| 53 |
# CORS middleware
|
|
|
|
| 59 |
allow_headers=["*"],
|
| 60 |
)
|
| 61 |
|
| 62 |
+
|
| 63 |
def get_default_branch_info(conn, repo_id: str, default_branch: str) -> Dict[str, Any]:
|
| 64 |
"""Get information about the default branch (build.zig.zon file).
|
| 65 |
+
|
| 66 |
Args:
|
| 67 |
conn: Database connection
|
| 68 |
repo_id: Repository identifier
|
| 69 |
default_branch: Name of the default branch
|
| 70 |
+
|
| 71 |
Returns:
|
| 72 |
Dictionary with default branch information including dependencies and minimum zig version
|
| 73 |
"""
|
|
|
|
| 86 |
ORDER BY published_at DESC
|
| 87 |
LIMIT 1
|
| 88 |
"""
|
| 89 |
+
|
| 90 |
cursor = conn.execute(sql, (repo_id,))
|
| 91 |
release = cursor.fetchone()
|
| 92 |
+
|
| 93 |
if not release:
|
| 94 |
return {
|
| 95 |
"minimum_zig_version": "0.0.0",
|
| 96 |
"dependencies": [],
|
| 97 |
+
"branch_name": default_branch,
|
| 98 |
}
|
| 99 |
+
|
| 100 |
release_id, version, published_at, min_zig_ver, readme_url, is_prerelease = release
|
| 101 |
+
|
| 102 |
# Get dependencies for this release
|
| 103 |
deps_sql = """
|
| 104 |
SELECT name, url, hash, lazy, path
|
|
|
|
| 107 |
"""
|
| 108 |
cursor = conn.execute(deps_sql, (release_id,))
|
| 109 |
dep_rows = cursor.fetchall()
|
| 110 |
+
|
| 111 |
dependencies = [
|
| 112 |
{
|
| 113 |
"name": row[0],
|
| 114 |
"url": row[1],
|
| 115 |
"hash": row[2],
|
| 116 |
"lazy": bool(row[3]) if row[3] else False,
|
| 117 |
+
"path": row[4],
|
| 118 |
}
|
| 119 |
for row in dep_rows
|
| 120 |
]
|
| 121 |
+
|
| 122 |
return {
|
| 123 |
"minimum_zig_version": min_zig_ver or "0.0.0",
|
| 124 |
"dependencies": dependencies,
|
| 125 |
"branch_name": default_branch,
|
| 126 |
"latest_version": version,
|
| 127 |
+
"readme_url": readme_url,
|
| 128 |
}
|
| 129 |
|
| 130 |
+
|
| 131 |
def get_version_info(conn, repo_id: str, version: str) -> Dict[str, Any]:
|
| 132 |
"""Get information about a specific version/release.
|
| 133 |
+
|
| 134 |
Args:
|
| 135 |
conn: Database connection
|
| 136 |
repo_id: Repository identifier
|
| 137 |
version: Version string
|
| 138 |
+
|
| 139 |
Returns:
|
| 140 |
Dictionary with version-specific information
|
| 141 |
+
|
| 142 |
Raises:
|
| 143 |
HTTPException: If version not found
|
| 144 |
"""
|
|
|
|
| 153 |
FROM releases
|
| 154 |
WHERE repo_id = ? AND version = ?
|
| 155 |
"""
|
| 156 |
+
|
| 157 |
cursor = conn.execute(sql, (repo_id, version))
|
| 158 |
release = cursor.fetchone()
|
| 159 |
+
|
| 160 |
if not release:
|
| 161 |
raise HTTPException(
|
| 162 |
+
status_code=404,
|
| 163 |
+
detail=f"Version '{version}' not found for repository '{repo_id}'",
|
| 164 |
)
|
| 165 |
+
|
| 166 |
release_id, ver, published_at, min_zig_ver, readme_url, is_prerelease = release
|
| 167 |
+
|
| 168 |
# Get dependencies for this version
|
| 169 |
deps_sql = """
|
| 170 |
SELECT name, url, hash, lazy, path
|
|
|
|
| 173 |
"""
|
| 174 |
cursor = conn.execute(deps_sql, (release_id,))
|
| 175 |
dep_rows = cursor.fetchall()
|
| 176 |
+
|
| 177 |
dependencies = [
|
| 178 |
{
|
| 179 |
"name": row[0],
|
| 180 |
"url": row[1],
|
| 181 |
"hash": row[2],
|
| 182 |
"lazy": bool(row[3]) if row[3] else False,
|
| 183 |
+
"path": row[4],
|
| 184 |
}
|
| 185 |
for row in dep_rows
|
| 186 |
]
|
| 187 |
+
|
| 188 |
return {
|
| 189 |
"version": ver,
|
| 190 |
"published_at": str(published_at) if published_at else None,
|
| 191 |
"minimum_zig_version": min_zig_ver or "0.0.0",
|
| 192 |
"readme_url": readme_url,
|
| 193 |
"is_prerelease": bool(is_prerelease),
|
| 194 |
+
"dependencies": dependencies,
|
| 195 |
}
|
| 196 |
|
| 197 |
+
|
| 198 |
def row_to_repo_dict(row: tuple) -> Dict[str, Any]:
|
| 199 |
"""Convert database row to repository dictionary.
|
| 200 |
+
|
| 201 |
Handles the common transformation from SQL result to API response format.
|
| 202 |
"""
|
| 203 |
platform_raw = (row[3] or "").lower()
|
| 204 |
+
provider = (
|
| 205 |
+
"gh"
|
| 206 |
+
if "github" in platform_raw
|
| 207 |
+
else ("cb" if "codeberg" in platform_raw else "gh")
|
| 208 |
+
)
|
| 209 |
repo_id = row[0]
|
| 210 |
+
repo_name = repo_id.split("/")[-1] if "/" in repo_id else repo_id
|
| 211 |
|
| 212 |
return {
|
| 213 |
"id": row[0],
|
|
|
|
| 234 |
"dependents_count": row[18] if row[18] is not None else 0,
|
| 235 |
}
|
| 236 |
|
| 237 |
+
|
| 238 |
def get_type_filter(search_type: str) -> str:
|
| 239 |
"""Generate SQL WHERE clause filter based on search type."""
|
| 240 |
if search_type == "package":
|
|
|
|
| 244 |
else: # all
|
| 245 |
return "AND (pkg.repo_id IS NOT NULL OR prog.repo_id IS NOT NULL)"
|
| 246 |
|
| 247 |
+
|
| 248 |
def search_repos(
|
| 249 |
+
conn, query: str, search_type: str = "all", limit: int = 50
|
|
|
|
|
|
|
|
|
|
| 250 |
) -> List[Dict[str, Any]]:
|
| 251 |
"""Search repositories by query string.
|
| 252 |
+
|
| 253 |
Args:
|
| 254 |
conn: Database connection
|
| 255 |
query: Search query string
|
| 256 |
search_type: Filter by 'package', 'program', or 'all'
|
| 257 |
limit: Maximum number of results to return
|
| 258 |
+
|
| 259 |
Returns:
|
| 260 |
List of repository dictionaries with default branch minimum_zig_version
|
| 261 |
"""
|
| 262 |
search_term_like = f"%{query}%"
|
| 263 |
# FTS5 query with prefix matching for each word
|
| 264 |
+
fts_query = " ".join(
|
| 265 |
+
'"' + word.replace('"', '""') + '"*' for word in query.split() if word
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
type_filter = get_type_filter(search_type)
|
| 269 |
|
| 270 |
sql = f"""
|
|
|
|
| 314 |
rd.stargazer_count DESC
|
| 315 |
LIMIT ?5
|
| 316 |
"""
|
| 317 |
+
|
| 318 |
starts_with = f"{query}%"
|
| 319 |
# Parameters matches ?1 (FTS), ?2 (LIKE), ?3 (Exact), ?4 (Starts with), ?5 (Limit)
|
| 320 |
cursor = conn.execute(sql, (fts_query, search_term_like, query, starts_with, limit))
|
| 321 |
rows = cursor.fetchall()
|
| 322 |
+
|
| 323 |
return [row_to_repo_dict(row) for row in rows]
|
| 324 |
|
| 325 |
+
|
| 326 |
def get_latest_repos(
|
| 327 |
+
conn, search_type: str = "all", limit: int = 10
|
|
|
|
|
|
|
| 328 |
) -> List[Dict[str, Any]]:
|
| 329 |
"""Get latest repositories ordered by creation date.
|
| 330 |
+
|
| 331 |
Args:
|
| 332 |
conn: Database connection
|
| 333 |
search_type: Filter by 'package', 'program', or 'all'
|
| 334 |
limit: Maximum number of results to return
|
| 335 |
+
|
| 336 |
Returns:
|
| 337 |
List of repository dictionaries with default branch minimum_zig_version
|
| 338 |
"""
|
|
|
|
| 373 |
(SELECT COUNT(*) FROM repo_dependents WHERE repo_id = rd.id) as dependents_count
|
| 374 |
FROM repo_data rd
|
| 375 |
"""
|
| 376 |
+
|
| 377 |
cursor = conn.execute(sql, (limit,))
|
| 378 |
rows = cursor.fetchall()
|
| 379 |
+
|
| 380 |
return [row_to_repo_dict(row) for row in rows]
|
| 381 |
|
| 382 |
+
|
| 383 |
def get_scroll_repos(
|
| 384 |
+
conn, search_type: str = "all", per_page: int = 20, page: int = 1
|
|
|
|
|
|
|
|
|
|
| 385 |
) -> List[Dict[str, Any]]:
|
| 386 |
"""Get paginated repositories ordered by star count.
|
| 387 |
+
|
| 388 |
Args:
|
| 389 |
conn: Database connection
|
| 390 |
search_type: Filter by 'package', 'program', or 'all'
|
| 391 |
per_page: Number of items per page (max 20)
|
| 392 |
page: Page number (1-indexed)
|
| 393 |
+
|
| 394 |
Returns:
|
| 395 |
List of repository dictionaries with default branch minimum_zig_version
|
| 396 |
"""
|
|
|
|
| 433 |
(SELECT COUNT(*) FROM repo_dependents WHERE repo_id = rd.id) as dependents_count
|
| 434 |
FROM repo_data rd
|
| 435 |
"""
|
| 436 |
+
|
| 437 |
cursor = conn.execute(sql, (actual_per_page, offset))
|
| 438 |
rows = cursor.fetchall()
|
| 439 |
+
|
| 440 |
return [row_to_repo_dict(row) for row in rows]
|
| 441 |
|
| 442 |
+
|
| 443 |
def parse_repo_id(repo_id: str) -> tuple[str, str]:
|
| 444 |
"""Parse repo_id into owner and repo name.
|
| 445 |
+
|
| 446 |
Args:
|
| 447 |
repo_id: Repository ID in format 'owner/repo' or 'provider/owner/repo'
|
| 448 |
+
|
| 449 |
Returns:
|
| 450 |
Tuple of (owner_name, repo_name)
|
| 451 |
+
|
| 452 |
Raises:
|
| 453 |
HTTPException: If repo_id format is invalid
|
| 454 |
"""
|
| 455 |
+
parts = repo_id.split("/")
|
| 456 |
+
|
| 457 |
if len(parts) == 3:
|
| 458 |
return parts[1], parts[2]
|
| 459 |
elif len(parts) == 2:
|
| 460 |
return parts[0], parts[1]
|
| 461 |
else:
|
| 462 |
raise HTTPException(
|
| 463 |
+
status_code=400,
|
| 464 |
+
detail="Invalid repo_id format. Expected 'provider/owner/repo' or 'owner/repo'",
|
| 465 |
)
|
| 466 |
|
| 467 |
+
|
| 468 |
+
def get_repo_details(
|
| 469 |
+
conn, repo_id: str, version: Optional[str] = None
|
| 470 |
+
) -> Dict[str, Any]:
|
| 471 |
"""Get detailed information about a repository.
|
| 472 |
+
|
| 473 |
Args:
|
| 474 |
conn: Database connection
|
| 475 |
repo_id: Repository identifier
|
| 476 |
version: Optional specific version to fetch details for
|
| 477 |
+
|
| 478 |
Returns:
|
| 479 |
Dictionary with complete repository details including:
|
| 480 |
- If version is None: default branch information with dependencies
|
| 481 |
- If version is specified: that specific version's information with dependencies
|
| 482 |
+
|
| 483 |
Raises:
|
| 484 |
HTTPException: If repository or version not found
|
| 485 |
"""
|
| 486 |
owner_name, repo_name = parse_repo_id(repo_id)
|
| 487 |
+
|
| 488 |
# Get repository details
|
| 489 |
repo_sql = """
|
| 490 |
SELECT
|
|
|
|
| 494 |
license, primary_language, minimum_zig_version
|
| 495 |
FROM repos WHERE id = ?
|
| 496 |
"""
|
| 497 |
+
|
| 498 |
cursor = conn.execute(repo_sql, (repo_id,))
|
| 499 |
repo_row = cursor.fetchone()
|
| 500 |
+
|
| 501 |
if not repo_row:
|
| 502 |
raise HTTPException(status_code=404, detail="Repository not found")
|
| 503 |
+
|
| 504 |
(
|
| 505 |
+
r_id,
|
| 506 |
+
avatar_id,
|
| 507 |
+
owner,
|
| 508 |
+
platform,
|
| 509 |
+
desc,
|
| 510 |
+
issues,
|
| 511 |
+
default_branch,
|
| 512 |
+
forks,
|
| 513 |
+
stars,
|
| 514 |
+
watchers,
|
| 515 |
+
pushed_at,
|
| 516 |
+
created_at,
|
| 517 |
+
is_archived,
|
| 518 |
+
is_disabled,
|
| 519 |
+
is_fork,
|
| 520 |
+
license_spdx,
|
| 521 |
+
primary_language,
|
| 522 |
+
min_zig_ver,
|
| 523 |
) = repo_row
|
| 524 |
+
|
| 525 |
# Map platform to provider
|
| 526 |
platform_raw = (platform or "").lower()
|
| 527 |
+
provider_id = (
|
| 528 |
+
"gh"
|
| 529 |
+
if "github" in platform_raw
|
| 530 |
+
else ("cb" if "codeberg" in platform_raw else "gh")
|
| 531 |
+
)
|
| 532 |
+
|
| 533 |
# Get all releases for the releases list
|
| 534 |
releases_sql = """
|
| 535 |
SELECT version
|
|
|
|
| 539 |
"""
|
| 540 |
cursor = conn.execute(releases_sql, (repo_id,))
|
| 541 |
releases_list = [row[0] for row in cursor.fetchall() if row[0]]
|
| 542 |
+
|
| 543 |
# Get dependents (same for all versions)
|
| 544 |
dependents_sql = "SELECT dependent FROM repo_dependents WHERE repo_id = ?"
|
| 545 |
cursor = conn.execute(dependents_sql, (repo_id,))
|
| 546 |
dependents = [row[0] for row in cursor.fetchall()]
|
| 547 |
+
|
| 548 |
# Build response with either version-specific or default branch info
|
| 549 |
response = {
|
| 550 |
"id": r_id,
|
|
|
|
| 572 |
"releases": releases_list,
|
| 573 |
"dependents": dependents,
|
| 574 |
}
|
| 575 |
+
|
| 576 |
if version:
|
| 577 |
# Get specific version info
|
| 578 |
version_info = get_version_info(conn, repo_id, version)
|
| 579 |
+
response.update(
|
| 580 |
+
{
|
| 581 |
+
"version": version_info["version"],
|
| 582 |
+
"published_at": version_info["published_at"],
|
| 583 |
+
"minimum_zig_version": version_info["minimum_zig_version"],
|
| 584 |
+
"readme_url": version_info["readme_url"],
|
| 585 |
+
"is_prerelease": version_info["is_prerelease"],
|
| 586 |
+
"dependencies": version_info["dependencies"],
|
| 587 |
+
"requested_version": version,
|
| 588 |
+
}
|
| 589 |
+
)
|
| 590 |
else:
|
| 591 |
# Get default branch info
|
| 592 |
branch_info = get_default_branch_info(conn, repo_id, default_branch)
|
| 593 |
+
response.update(
|
| 594 |
+
{
|
| 595 |
+
"minimum_zig_version": min_zig_ver
|
| 596 |
+
or branch_info["minimum_zig_version"],
|
| 597 |
+
"dependencies": branch_info["dependencies"],
|
| 598 |
+
"branch_name": branch_info["branch_name"],
|
| 599 |
+
"latest_version": branch_info.get("latest_version"),
|
| 600 |
+
"readme_url": branch_info.get("readme_url"),
|
| 601 |
+
"version": None, # Indicates default branch, not a specific version
|
| 602 |
+
}
|
| 603 |
+
)
|
| 604 |
+
|
| 605 |
return response
|
| 606 |
|
| 607 |
+
|
| 608 |
def check_db_connection():
|
| 609 |
"""Verify database connection is available."""
|
| 610 |
if not db_conn:
|
| 611 |
raise HTTPException(status_code=503, detail="Database not initialized")
|
| 612 |
|
| 613 |
+
|
| 614 |
# API Endpoints
|
| 615 |
|
| 616 |
+
|
| 617 |
@app.get("/search/packages", tags=["Search"])
|
| 618 |
async def search_packages_endpoint(
|
| 619 |
q: str = Query(..., min_length=1, description="Search query"),
|
| 620 |
+
limit: int = Query(50, ge=1, le=100, description="Maximum results"),
|
| 621 |
):
|
| 622 |
"""Search for Zig packages. Returns default branch minimum_zig_version for each result."""
|
| 623 |
check_db_connection()
|
| 624 |
return search_repos(db_conn, q, search_type="package", limit=limit)
|
| 625 |
|
| 626 |
+
|
| 627 |
@app.get("/search/programs", tags=["Search"])
|
| 628 |
async def search_programs_endpoint(
|
| 629 |
q: str = Query(..., min_length=1, description="Search query"),
|
| 630 |
+
limit: int = Query(50, ge=1, le=100, description="Maximum results"),
|
| 631 |
):
|
| 632 |
"""Search for Zig programs. Returns default branch minimum_zig_version for each result."""
|
| 633 |
check_db_connection()
|
| 634 |
return search_repos(db_conn, q, search_type="program", limit=limit)
|
| 635 |
|
| 636 |
+
|
| 637 |
@app.get("/packages/latest", tags=["Packages"])
|
| 638 |
async def get_latest_packages_endpoint(
|
| 639 |
+
limit: int = Query(10, ge=1, le=50, description="Number of packages"),
|
| 640 |
):
|
| 641 |
"""Get latest Zig packages. Returns default branch minimum_zig_version for each result."""
|
| 642 |
check_db_connection()
|
| 643 |
return get_latest_repos(db_conn, search_type="package", limit=limit)
|
| 644 |
|
| 645 |
+
|
| 646 |
@app.get("/programs/latest", tags=["Programs"])
|
| 647 |
async def get_latest_programs_endpoint(
|
| 648 |
+
limit: int = Query(10, ge=1, le=50, description="Number of programs"),
|
| 649 |
):
|
| 650 |
"""Get latest Zig programs. Returns default branch minimum_zig_version for each result."""
|
| 651 |
check_db_connection()
|
| 652 |
return get_latest_repos(db_conn, search_type="program", limit=limit)
|
| 653 |
|
| 654 |
+
|
| 655 |
@app.get("/packages/scroll", tags=["Packages"])
|
| 656 |
async def scroll_packages_endpoint(
|
| 657 |
per_page: int = Query(20, ge=1, le=20, description="Items per page"),
|
| 658 |
+
page: int = Query(1, ge=1, description="Page number"),
|
| 659 |
):
|
| 660 |
"""Get paginated list of packages sorted by stars. Returns default branch minimum_zig_version for each result."""
|
| 661 |
check_db_connection()
|
| 662 |
+
return get_scroll_repos(
|
| 663 |
+
db_conn, search_type="package", per_page=per_page, page=page
|
| 664 |
+
)
|
| 665 |
+
|
| 666 |
|
| 667 |
@app.get("/programs/scroll", tags=["Programs"])
|
| 668 |
async def scroll_programs_endpoint(
|
| 669 |
per_page: int = Query(20, ge=1, le=20, description="Items per page"),
|
| 670 |
+
page: int = Query(1, ge=1, description="Page number"),
|
| 671 |
):
|
| 672 |
"""Get paginated list of programs sorted by stars. Returns default branch minimum_zig_version for each result."""
|
| 673 |
check_db_connection()
|
| 674 |
+
return get_scroll_repos(
|
| 675 |
+
db_conn, search_type="program", per_page=per_page, page=page
|
| 676 |
+
)
|
| 677 |
+
|
| 678 |
|
| 679 |
@app.get("/packages", tags=["Packages"])
|
| 680 |
async def get_package_details_endpoint(
|
| 681 |
+
q: str = Query(
|
| 682 |
+
..., alias="q", description="Repository ID (owner/repo or provider/owner/repo)"
|
| 683 |
+
),
|
| 684 |
+
version: Optional[str] = Query(
|
| 685 |
+
None, description="Specific version to fetch (omit for default branch)"
|
| 686 |
+
),
|
| 687 |
):
|
| 688 |
"""
|
| 689 |
Get detailed information about a package.
|
| 690 |
+
|
| 691 |
- Without version parameter: Returns default branch information with dependencies
|
| 692 |
- With version parameter: Returns that specific version's information with dependencies
|
| 693 |
+
|
| 694 |
Examples:
|
| 695 |
- /packages?q=gh/rohanvashisht1234/zorsig (default branch)
|
| 696 |
- /packages?q=gh/rohanvashisht1234/zorsig&version=0.0.1 (specific version)
|
|
|
|
| 698 |
check_db_connection()
|
| 699 |
return get_repo_details(db_conn, q, version)
|
| 700 |
|
| 701 |
+
|
| 702 |
@app.get("/programs", tags=["Programs"])
|
| 703 |
async def get_program_details_endpoint(
|
| 704 |
+
q: str = Query(
|
| 705 |
+
..., alias="q", description="Repository ID (owner/repo or provider/owner/repo)"
|
| 706 |
+
),
|
| 707 |
+
version: Optional[str] = Query(
|
| 708 |
+
None, description="Specific version to fetch (omit for default branch)"
|
| 709 |
+
),
|
| 710 |
):
|
| 711 |
"""
|
| 712 |
Get detailed information about a program.
|
| 713 |
+
|
| 714 |
- Without version parameter: Returns default branch information with dependencies
|
| 715 |
- With version parameter: Returns that specific version's information with dependencies
|
| 716 |
+
|
| 717 |
Examples:
|
| 718 |
- /programs?q=gh/owner/program (default branch)
|
| 719 |
- /programs?q=gh/owner/program&version=1.0.0 (specific version)
|
|
|
|
| 721 |
check_db_connection()
|
| 722 |
return get_repo_details(db_conn, q, version)
|
| 723 |
|
| 724 |
+
|
| 725 |
@app.get("/health", tags=["System"])
|
| 726 |
async def health_check():
|
| 727 |
"""Check API health status."""
|
| 728 |
+
return {"status": "healthy", "database": "connected" if db_conn else "disconnected"}
|
| 729 |
+
|
|
|
|
|
|
|
| 730 |
|
| 731 |
if __name__ == "__main__":
|
| 732 |
import uvicorn
|
| 733 |
+
|
| 734 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|