Skip to content

Commit eb024e4

Browse files
authored
Merge pull request #41 from Code4GovTech/dev
Migrations to Prod
2 parents 6cc3f54 + 66f5833 commit eb024e4

13 files changed

Lines changed: 597 additions & 128 deletions

File tree

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
.env
2+
!.git
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: Build and Push Docker Image
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- dev
8+
release:
9+
types: [published]
10+
env:
11+
REGISTRY: ghcr.io
12+
IMAGE_NAME: ${{ github.repository }}
13+
14+
jobs:
15+
build-and-push:
16+
runs-on: ubuntu-latest
17+
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
18+
permissions:
19+
contents: read
20+
packages: write
21+
steps:
22+
23+
- name: Checkout code
24+
uses: actions/checkout@v2
25+
26+
- name: Set up Docker Buildx
27+
uses: docker/setup-buildx-action@v2
28+
29+
- name: Log in to the Container registry
30+
uses: docker/login-action@v3
31+
with:
32+
registry: ${{ env.REGISTRY }}
33+
username: ${{ github.actor }}
34+
password: ${{ secrets.GITHUB_TOKEN }}
35+
36+
- name: Extract metadata (tags, labels) for Docker
37+
id: meta
38+
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
39+
with:
40+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
41+
tags: |
42+
# minimal
43+
type=pep440,pattern={{version}},value=${{ github.ref_name }},enable=${{ github.event_name == 'release' }}
44+
# branch event
45+
type=ref,event=branch
46+
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
47+
48+
- name: Build and Push Docker image
49+
uses: docker/build-push-action@v4
50+
with:
51+
# build-args:
52+
context: .
53+
push: true
54+
cache-from: type=gha
55+
cache-to: type=gha,mode=max
56+
tags: ${{ steps.meta.outputs.tags }}
57+
labels: ${{ steps.meta.outputs.labels }}

.github/workflows/ci.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,16 @@ jobs:
6666
SUPABASE_KEY: ${{ secrets[format('APP_{0}_SUPABASE_KEY', needs.set_vars.outputs.APP_ENV)] }}
6767
SUPABASE_URL: ${{ vars[format('APP_{0}_SUPABASE_URL', needs.set_vars.outputs.APP_ENV)] }}
6868
SCHEDULER_DELAY_IN_MINS: ${{ vars[format('APP_{0}_SCHEDULER_DELAY_IN_MINS', needs.set_vars.outputs.APP_ENV)] }}
69+
POSTGRES_DB_HOST: ${{ secrets[format('APP_{0}_POSTGRES_DB_HOST', needs.set_vars.outputs.APP_ENV)] }}
70+
POSTGRES_DB_NAME: ${{ secrets[format('APP_{0}_POSTGRES_DB_NAME', needs.set_vars.outputs.APP_ENV)] }}
71+
POSTGRES_DB_USER: ${{ secrets[format('APP_{0}_POSTGRES_DB_USER', needs.set_vars.outputs.APP_ENV)] }}
72+
POSTGRES_DB_PASS: ${{ secrets[format('APP_{0}_POSTGRES_DB_PASS', needs.set_vars.outputs.APP_ENV)] }}
73+
6974
steps:
7075
- name: Checkout code
7176
uses: actions/checkout@v2
7277

78+
7379
- name: Log in to the Container registry
7480
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
7581
with:
@@ -87,6 +93,10 @@ jobs:
8793
echo "SUPABASE_URL=${SUPABASE_URL}" >> .env
8894
echo "SUPABASE_KEY=${SUPABASE_KEY}" >> .env
8995
echo "SCHEDULER_DELAY_IN_MINS=${SCHEDULER_DELAY_IN_MINS}" >> .env
96+
echo "POSTGRES_DB_HOST=${POSTGRES_DB_HOST}" >> .env
97+
echo "POSTGRES_DB_NAME=${POSTGRES_DB_NAME}" >> .env
98+
echo "POSTGRES_DB_USER=${POSTGRES_DB_USER}" >> .env
99+
echo "POSTGRES_DB_PASS=${POSTGRES_DB_PASS}" >> .env
90100
91101
mv .env ${{ env.DOT_ENV_FILE_NAME }}
92102

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "shared_migrations"]
2+
path = shared_migrations
3+
url = https://github.com/Code4GovTech/shared-models-migrations.git

Dockerfile

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,21 @@ FROM python:3.12-slim
44
# Set the working directory in the container
55
WORKDIR /app
66

7-
# Copy the current directory contents into the container at /app
7+
# Install necessary tools
8+
RUN apt-get update && \
9+
apt-get install -y --no-install-recommends git openssh-client && \
10+
rm -rf /var/lib/apt/lists/*
11+
12+
# Copy the current directory contents, including the .git directory
813
COPY . /app
914

15+
# Set up the SSH agent forwarding
16+
RUN --mount=type=ssh git submodule update --init --recursive
17+
1018
# Install any needed packages specified in requirements.txt
1119
RUN pip install --no-cache-dir -r requirements.txt
1220

21+
# Expose the application port
1322
EXPOSE 5000
1423

1524
# Run app.py when the container launches

app.py

Lines changed: 74 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,32 @@
11
# app.py
22
from quart import Quart
3-
import httpx
4-
import os,markdown2
5-
from db import SupabaseInterface
3+
import os, markdown2, httpx
64
from apscheduler.schedulers.asyncio import AsyncIOScheduler
75
from dotenv import load_dotenv
8-
from datetime import datetime
9-
6+
from datetime import datetime, timezone
7+
# from query import PostgresORM
8+
from shared_migrations.db import PostgresORM, get_postgres_uri
9+
from shared_migrations.db.dmp_cron import DmpCronQueries
1010
from utils import handle_week_data, parse_issue_description
11+
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
12+
from sqlalchemy.orm import sessionmaker
13+
from datetime import datetime
14+
from shared_migrations.db.models import *
15+
from sqlalchemy.pool import NullPool
1116

1217
# Load environment variables from .env file
1318
load_dotenv()
1419
delay_mins: str = os.getenv("SCHEDULER_DELAY_IN_MINS")
1520

1621
app = Quart(__name__)
1722

23+
# Initialize Quart app
24+
app.config['SQLALCHEMY_DATABASE_URI'] = get_postgres_uri()
25+
26+
# Initialize Async SQLAlchemy
27+
engine = create_async_engine(app.config['SQLALCHEMY_DATABASE_URI'], echo=False, poolclass=NullPool)
28+
async_session = sessionmaker(autocommit=False, autoflush=False, bind=engine, class_=AsyncSession)
29+
1830
scheduler = AsyncIOScheduler()
1931

2032

@@ -88,10 +100,9 @@ async def dmp_updates():
88100
GITHUB_TOKEN = os.getenv('GITHUB_TOKEN')
89101
try:
90102
TARGET_DATE = os.getenv('TARGET_DATE')
91-
db = SupabaseInterface().get_instance()
92103

93104
# Loop through all dmp issues
94-
dmp_tickets = db.get_dmp_issues()
105+
dmp_tickets = await DmpCronQueries.get_all_dmp_issues(async_session)
95106

96107
for dmp in dmp_tickets:
97108
dmp_id = dmp['id']
@@ -100,7 +111,7 @@ async def dmp_updates():
100111
repo = dmp['repo']
101112
owner = dmp['repo_owner']
102113

103-
app.logger.info("DMP_ID: "+str(dmp_id))
114+
app.logger.info("DMP_ID: " + str(dmp_id))
104115

105116
# # Make the HTTP request to GitHub API
106117
headers = {
@@ -120,18 +131,22 @@ async def dmp_updates():
120131
# Parse issue discription
121132
print('processing description ')
122133
issue_update = define_issue_description_update(issue_response.json())
123-
124-
issue_update['mentor_username'] = dmp['mentor_username'] #get from db
125-
issue_update['contributor_username'] = dmp['contributor_username'] #get from db
126-
134+
135+
issue_update['mentor_username'] = dmp['mentor_username'] # get from db
136+
issue_update['contributor_username'] = dmp['contributor_username'] # get from db
137+
127138
app.logger.info('Decription from remote: ', issue_update)
128-
update_data = db.update_data(
129-
issue_update, 'dmp_issues', 'id', dmp_id)
139+
140+
update_data = await DmpCronQueries.update_dmp_issue(async_session, issue_id=dmp_id,
141+
update_data=issue_update)
142+
143+
print(f"dmp_issue update works - dmp_id {dmp_id}") if update_data else print(
144+
f"dmp_issue update failed - dmp_id {dmp_id}")
130145
app.logger.info(update_data)
131146
else:
132147
print('issue response ', issue_response)
133148
app.logger.error("Description API failed: " +
134-
str(issue_response.status_code) + " for dmp_id: "+str(dmp_id))
149+
str(issue_response.status_code) + " for dmp_id: " + str(dmp_id))
135150

136151
# 2. Read & Update comments of the ticket
137152
page = 1
@@ -148,29 +163,43 @@ async def dmp_updates():
148163
week_learning_status = False
149164
# Loop through comments
150165
comments_array = comments_response.json()
151-
if comments_array == [] or len(comments_array)==0:
166+
if comments_array == [] or len(comments_array) == 0:
152167
break
153168
for val in comments_response.json():
154-
# Handle if any of the comments are week data
169+
# Handle if any of the comments are week data
155170
plain_text_body = markdown2.markdown(val['body'])
156171
if "Weekly Goals" in plain_text_body and not week_update_status:
157-
week_update_status = handle_week_data(val, dmp['issue_url'], dmp_id, issue_update['mentor_username'])
158-
172+
week_update_status = await handle_week_data(val, dmp['issue_url'], dmp_id,
173+
issue_update['mentor_username'],
174+
async_session)
175+
159176
if "Weekly Learnings" in plain_text_body and not week_learning_status:
160-
week_learning_status = handle_week_data(val, dmp['issue_url'], dmp_id, issue_update['mentor_username'])
161-
177+
week_learning_status = await handle_week_data(val, dmp['issue_url'], dmp_id,
178+
issue_update['mentor_username'],
179+
async_session)
180+
162181
# Parse comments
163-
comment_update = define_issue_update(
164-
val, dmp_id=dmp_id)
165-
app.logger.info(
166-
'Comment from remote: ', comment_update)
167-
upsert_comments = db.upsert_data(
168-
comment_update, 'dmp_issue_updates')
182+
comment_update = define_issue_update(val, dmp_id=dmp_id)
183+
app.logger.info('Comment from remote: ', comment_update)
184+
185+
# get created_at
186+
created_timestamp = await DmpCronQueries.get_timestamp(async_session, DmpIssueUpdates,
187+
'created_at', 'comment_id',
188+
comment_update['comment_id'])
189+
comment_update[
190+
'created_at'] = datetime.utcnow() if not created_timestamp else created_timestamp
191+
comment_update['comment_updated_at'] = datetime.utcnow().replace(tzinfo=None)
192+
comment_update['created_at'] = comment_update['created_at'].replace(tzinfo=None)
193+
194+
upsert_comments = await DmpCronQueries.upsert_data_orm(async_session, comment_update)
195+
196+
print(f"dmp_issue_updates works dmp_id - {dmp_id}") if upsert_comments else print(
197+
f"comment failed dmp_id - {dmp_id}")
169198
app.logger.info(upsert_comments)
170199
else:
171200
print('issue response ', issue_response)
172201
app.logger.error("Comments API failed: " +
173-
str(issue_response.status_code) + " for dmp_id: "+str(dmp_id))
202+
str(issue_response.status_code) + " for dmp_id: " + str(dmp_id))
174203
break
175204
page = page + 1
176205

@@ -188,25 +217,37 @@ async def dmp_updates():
188217
pr_created_at = pr_val['created_at']
189218
if (pr_created_at >= TARGET_DATE):
190219
pr_data = define_pr_update(pr_val, dmp_id)
191-
upsert_pr = db.upsert_data(
192-
pr_data, 'dmp_pr_updates')
220+
221+
created_timestamp = await DmpCronQueries.get_timestamp(async_session, DmpPrUpdates,
222+
'created_at', 'pr_id',
223+
pr_data['pr_id'])
224+
pr_data['created_at'] = datetime.utcnow() if not created_timestamp else created_timestamp
225+
pr_data['created_at'] = pr_data['created_at'].replace(tzinfo=None)
226+
227+
upsert_pr = await DmpCronQueries.upsert_pr_update(async_session, pr_data)
228+
229+
print(f"dmp_pr_updates works - dmp_id is {dmp_id}") if upsert_pr else print(
230+
f"dmp_pr_updates failed - dmp_id is {dmp_id}")
193231
app.logger.info(upsert_pr)
194232
else:
195233
print('issue response ', issue_response)
196234
app.logger.error("PR API failed: " +
197-
str(issue_response.status_code) + " for dmp_id: "+str(dmp_id))
235+
str(issue_response.status_code) + " for dmp_id: " + str(dmp_id))
236+
print(f"last run at - {datetime.utcnow()}")
198237
return "success"
199238
except Exception as e:
200239
print(e)
240+
print(f"last run with error - {datetime.utcnow()}")
201241
return "Server Error"
202242

203243

204244
@app.before_serving
205245
async def start_scheduler():
206246
app.logger.info(
207-
"Scheduling dmp_updates_job to run every "+delay_mins+" mins")
247+
"Scheduling dmp_updates_job to run every " + delay_mins + " mins")
208248
scheduler.add_job(dmp_updates, 'interval', minutes=int(delay_mins))
209249
scheduler.start()
210250

251+
211252
if __name__ == '__main__':
212-
app.run(host='0.0.0.0')
253+
app.run(host='0.0.0.0')

db.py

Lines changed: 0 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +0,0 @@
1-
import os, sys
2-
from typing import Any
3-
from supabase import create_client, Client
4-
from supabase.lib.client_options import ClientOptions
5-
from abc import ABC, abstractmethod
6-
from dotenv import load_dotenv
7-
8-
load_dotenv()
9-
10-
client_options = ClientOptions(postgrest_client_timeout=None)
11-
12-
url: str = os.getenv("SUPABASE_URL")
13-
key: str = os.getenv("SUPABASE_KEY")
14-
15-
class SupabaseInterface():
16-
17-
_instance = None
18-
19-
def __init__(self):
20-
# Initialize Supabase client upon first instantiation
21-
if not SupabaseInterface._instance:
22-
self.supabase_url =url
23-
self.supabase_key =key
24-
self.client: Client = create_client(self.supabase_url, self.supabase_key, options=client_options)
25-
SupabaseInterface._instance = self
26-
else:
27-
SupabaseInterface._instance = self._instance
28-
29-
30-
31-
@staticmethod
32-
def get_instance():
33-
# Static method to retrieve the singleton instance
34-
if not SupabaseInterface._instance:
35-
# If no instance exists, create a new one
36-
SupabaseInterface._instance = SupabaseInterface()
37-
return SupabaseInterface._instance
38-
39-
40-
def readAll(self, table):
41-
data = self.client.table(f"{table}").select("*").execute()
42-
return data.data
43-
44-
def add_data(self, data,table_name):
45-
data = self.client.table(table_name).insert(data).execute()
46-
return data.data
47-
48-
def update_data(self,data,table_name, match_column, match_value):
49-
response = self.client.table(table_name).update(data).eq(match_column, match_value).execute()
50-
return response.data
51-
52-
def multiple_update_data(self,data,table_name, match_column, match_value):
53-
response = self.client.table(table_name).update(data).eq(match_column[0], match_value[0]).eq(match_column[1], match_value[1]).execute()
54-
return response.data
55-
56-
def upsert_data(self,data,table_name):
57-
response = self.client.table(table_name).upsert(data).execute()
58-
return response.data
59-
60-
def add_data_filter(self, data, table_name):
61-
# Construct the filter based on the provided column names and values
62-
filter_data = {column: data[column] for column in ['dmp_id','issue_number','owner']}
63-
64-
# Check if the data already exists in the table based on the filter
65-
existing_data = self.client.table(table_name).select("*").eq('dmp_id',data['dmp_id']).execute()
66-
67-
# If the data already exists, return without creating a new record
68-
if existing_data.data:
69-
return "Data already exists"
70-
71-
# If the data doesn't exist, insert it into the table
72-
new_data = self.client.table(table_name).insert(data).execute()
73-
return new_data.data
74-
75-
def get_dmp_issues(self):
76-
response = self.client.table('dmp_issues').select('*, dmp_orgs(*)').execute()
77-
return response.data

models.py

Whitespace-only changes.

0 commit comments

Comments
 (0)