Skip to content

Commit 70c9c07

Browse files
Merge pull request #100 from goldlabelapps/staging
Update GitHub API integration and versioning; standardize meta keys
2 parents 5c49b66 + 561330a commit 70c9c07

10 files changed

Lines changed: 592 additions & 9 deletions

File tree

app/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
"""Python° - FastAPI, Postgres, tsvector"""
22

33
# Current Version
4-
__version__ = "3.0.1"
4+
__version__ = "3.0.3"

app/api/github/README.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
## GitHub
2+
3+
We are going to use this route to get data from our GitHub account using their API. We'll be storing that info in the Postgres DB, but have not created any tables yet.
4+
5+
### Create Table Route
6+
7+
**POST /api/github/createtable**
8+
9+
Hits this URL to create the GitHub tables in Postgres. Each table is created with `IF NOT EXISTS`, so it is safe to call multiple times — existing tables and data will not be affected.
10+
11+
### Proposed Table Design
12+
13+
1. github_accounts
14+
- One row per GitHub account/user profile.
15+
- Stores account identity fields and full raw payload.
16+
17+
2. github_repos
18+
- One row per repository.
19+
- Stores common repo analytics fields plus raw JSON payload.
20+
21+
3. github_gists
22+
- One row per gist.
23+
- Stores gist metadata plus raw JSON payload.
24+
25+
4. github_projects
26+
- One row per project.
27+
- Stores project metadata plus raw JSON payload.
28+
29+
5. github_resources
30+
- Generic catch-all for any future GitHub resource type.
31+
- Supports "everything else" from the API without migrations for every new object type.
32+
33+
### Why this shape works
34+
35+
- Normalized for common entities you asked for: repos, gists, projects.
36+
- Flexible for all additional GitHub objects through jsonb payload storage.
37+
- Indexes on resource type, account, and payload for filtering and search.
38+

app/api/github/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""GitHub Routes"""
2+
3+
from fastapi import APIRouter
4+
from .github import router as _github_router
5+
from .sql.create_tables import router as _create_tables_router
6+
7+
github_router = APIRouter()
8+
github_router.include_router(_github_router)
9+
github_router.include_router(_create_tables_router)

app/api/github/github.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
from fastapi import APIRouter, Depends
2+
from app.utils.make_meta import make_meta
3+
from app.utils.db import get_db_connection_direct
4+
from app.utils.api_key_auth import get_api_key
5+
6+
router = APIRouter()
7+
8+
_TABLES = [
9+
"github_accounts",
10+
"github_repos",
11+
"github_gists",
12+
"github_projects",
13+
"github_resources",
14+
]
15+
16+
17+
def _fetch_table(cur, table: str) -> dict:
18+
cur.execute(f"SELECT COUNT(*) FROM {table};")
19+
row = cur.fetchone()
20+
count = row[0] if row and row[0] is not None else 0
21+
cur.execute(f"SELECT * FROM {table} ORDER BY id DESC LIMIT 100;")
22+
if cur.description:
23+
columns = [desc[0] for desc in cur.description]
24+
rows = [dict(zip(columns, r)) for r in cur.fetchall()]
25+
else:
26+
rows = []
27+
return {"count": count, "rows": rows}
28+
29+
30+
@router.get("/github")
31+
def get_github(api_key: str = Depends(get_api_key)) -> dict:
32+
"""GET /github: Return counts and records from all GitHub tables."""
33+
conn = None
34+
cur = None
35+
try:
36+
conn = get_db_connection_direct()
37+
cur = conn.cursor()
38+
data = {table: _fetch_table(cur, table) for table in _TABLES}
39+
return {"meta": make_meta("success", "GitHub data"), "data": data}
40+
except Exception as e:
41+
return {"meta": make_meta("error", f"DB error: {str(e)}"), "data": {}}
42+
finally:
43+
if cur is not None:
44+
cur.close()
45+
if conn is not None:
46+
conn.close()
47+
48+
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
from fastapi import APIRouter, HTTPException, Depends
2+
from app.utils.make_meta import make_meta
3+
from app.utils.db import get_db_connection_direct
4+
from app.utils.api_key_auth import get_api_key
5+
6+
router = APIRouter()
7+
8+
_SQL = [
9+
"""
10+
CREATE TABLE IF NOT EXISTS github_accounts (
11+
id SERIAL PRIMARY KEY,
12+
github_user_id BIGINT UNIQUE,
13+
login TEXT UNIQUE,
14+
name TEXT,
15+
email TEXT,
16+
company TEXT,
17+
blog TEXT,
18+
location TEXT,
19+
bio TEXT,
20+
avatar_url TEXT,
21+
html_url TEXT,
22+
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
23+
last_synced_at TIMESTAMPTZ,
24+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
25+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
26+
);
27+
""",
28+
"""
29+
CREATE TABLE IF NOT EXISTS github_repos (
30+
id BIGSERIAL PRIMARY KEY,
31+
github_repo_id BIGINT UNIQUE NOT NULL,
32+
account_login TEXT,
33+
name TEXT NOT NULL,
34+
full_name TEXT,
35+
private BOOLEAN NOT NULL DEFAULT FALSE,
36+
fork BOOLEAN NOT NULL DEFAULT FALSE,
37+
archived BOOLEAN NOT NULL DEFAULT FALSE,
38+
disabled BOOLEAN NOT NULL DEFAULT FALSE,
39+
default_branch TEXT,
40+
language TEXT,
41+
stargazers_count INTEGER,
42+
watchers_count INTEGER,
43+
forks_count INTEGER,
44+
open_issues_count INTEGER,
45+
size_kb INTEGER,
46+
pushed_at TIMESTAMPTZ,
47+
created_at_github TIMESTAMPTZ,
48+
updated_at_github TIMESTAMPTZ,
49+
html_url TEXT,
50+
api_url TEXT,
51+
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
52+
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
53+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
54+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
55+
);
56+
""",
57+
"""
58+
CREATE TABLE IF NOT EXISTS github_gists (
59+
id BIGSERIAL PRIMARY KEY,
60+
gist_id TEXT UNIQUE NOT NULL,
61+
owner_login TEXT,
62+
description TEXT,
63+
public BOOLEAN,
64+
files_count INTEGER,
65+
comments_count INTEGER,
66+
html_url TEXT,
67+
api_url TEXT,
68+
created_at_github TIMESTAMPTZ,
69+
updated_at_github TIMESTAMPTZ,
70+
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
71+
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
72+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
73+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
74+
);
75+
""",
76+
"""
77+
CREATE TABLE IF NOT EXISTS github_projects (
78+
id BIGSERIAL PRIMARY KEY,
79+
github_project_id BIGINT UNIQUE NOT NULL,
80+
owner_login TEXT,
81+
owner_type TEXT,
82+
name TEXT NOT NULL,
83+
body TEXT,
84+
state TEXT,
85+
number INTEGER,
86+
html_url TEXT,
87+
api_url TEXT,
88+
created_at_github TIMESTAMPTZ,
89+
updated_at_github TIMESTAMPTZ,
90+
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
91+
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
92+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
93+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
94+
);
95+
""",
96+
"""
97+
CREATE TABLE IF NOT EXISTS github_resources (
98+
id BIGSERIAL PRIMARY KEY,
99+
account_login TEXT,
100+
resource_type TEXT NOT NULL,
101+
resource_id TEXT NOT NULL,
102+
resource_name TEXT,
103+
resource_url TEXT,
104+
is_private BOOLEAN,
105+
state TEXT,
106+
created_at_github TIMESTAMPTZ,
107+
updated_at_github TIMESTAMPTZ,
108+
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
109+
synced_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
110+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
111+
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
112+
UNIQUE (resource_type, resource_id)
113+
);
114+
""",
115+
"CREATE INDEX IF NOT EXISTS idx_github_resources_type ON github_resources (resource_type);",
116+
"CREATE INDEX IF NOT EXISTS idx_github_resources_account ON github_resources (account_login);",
117+
"CREATE INDEX IF NOT EXISTS idx_github_resources_payload ON github_resources USING GIN (payload);",
118+
"CREATE INDEX IF NOT EXISTS idx_github_repos_payload ON github_repos USING GIN (payload);",
119+
"CREATE INDEX IF NOT EXISTS idx_github_gists_payload ON github_gists USING GIN (payload);",
120+
"CREATE INDEX IF NOT EXISTS idx_github_projects_payload ON github_projects USING GIN (payload);",
121+
]
122+
123+
124+
@router.post("/api/github/createtable")
125+
def create_github_tables(api_key: str = Depends(get_api_key)) -> dict:
126+
"""POST /api/github/createtable: Create GitHub ingestion tables."""
127+
conn = None
128+
cur = None
129+
try:
130+
conn = get_db_connection_direct()
131+
cur = conn.cursor()
132+
for statement in _SQL:
133+
cur.execute(statement)
134+
conn.commit()
135+
meta = make_meta("success", "GitHub tables created")
136+
return {
137+
"meta": meta,
138+
"data": {
139+
"tables": [
140+
"github_accounts",
141+
"github_repos",
142+
"github_gists",
143+
"github_projects",
144+
"github_resources",
145+
]
146+
},
147+
}
148+
except Exception as e:
149+
if conn is not None:
150+
conn.rollback()
151+
raise HTTPException(status_code=500, detail=str(e))
152+
finally:
153+
if cur is not None:
154+
cur.close()
155+
if conn is not None:
156+
conn.close()

0 commit comments

Comments
 (0)