Skip to content

Commit 601b406

Browse files
authored
Merge pull request #3 from dvrpc/colin-branch
Changed content/viz repository to postgres db
2 parents c23606d + c2f4023 commit 601b406

File tree

7 files changed

+337
-25
lines changed

7 files changed

+337
-25
lines changed
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
2+
import psycopg
3+
import requests
4+
import os
5+
import logging
6+
import asyncio
7+
import aiohttp
8+
from dotenv import load_dotenv
9+
from fastapi_cache.decorator import cache
10+
import pandas as pd
11+
from sqlalchemy import create_engine
12+
from datetime import datetime
13+
14+
# from utils.consts import subcategory_map
15+
16+
log = logging.getLogger(__name__)
17+
18+
load_dotenv()
19+
token = os.getenv("GITHUB_TOKEN")
20+
headers = {
21+
"Authorization": f"Bearer {token}"
22+
}
23+
base_contents_url = "https://api.github.com/repos/dvrpc/community-profiles-content/contents"
24+
25+
subcategory_map = {
26+
'demographics-housing': {'demographics': [], 'housing': []},
27+
'economy': {'employment': [], 'income-poverty': [], 'transportation': []},
28+
'active-transportation': {'cycling': [], 'pedestrian': [], 'commute': []},
29+
'safety-health': {'crash': [], 'health': []},
30+
'freight': {'freight': []},
31+
'environment': {'open-space': [], 'planning': []},
32+
'transit': {'transit': [], 'tip': []},
33+
'roadways': {'conditions': [], 'tip': []}
34+
}
35+
36+
host = os.getenv("DB_HOST")
37+
dbname = os.getenv("DB_NAME")
38+
user = os.getenv("DB_USER")
39+
password = os.getenv("DB_PASS")
40+
port = os.getenv("DB_PORT")
41+
42+
uri = f"postgresql://{user}:{password}@{host}:{port}/{dbname}"
43+
print(uri)
44+
# establish connection with the database
45+
engine = create_engine(uri)
46+
47+
async def get_viz_download_url(geo_level, category, subcategory, topic):
48+
url = f"{base_contents_url}/{geo_level}/viz/{category}/{subcategory}/{topic}.json"
49+
try:
50+
r = requests.get(url, headers=headers)
51+
r.raise_for_status()
52+
53+
except requests.exceptions.HTTPError as e:
54+
log.error(
55+
f"Error fetching {geo_level} contents: \n{e}")
56+
57+
data = r.json()
58+
59+
return data['download_url']
60+
61+
62+
async def get_download_urls(geo_level, type):
63+
urls = []
64+
65+
for key, value in subcategory_map.items():
66+
for subcategory in value.keys():
67+
urls.append({
68+
'category': key,
69+
'subcategory': subcategory,
70+
'geo_level': geo_level,
71+
'url': f"{base_contents_url}/{geo_level}/{type}/{key}/{subcategory}"
72+
})
73+
74+
75+
async with aiohttp.ClientSession() as session:
76+
tasks = []
77+
for url in urls:
78+
tasks.append(get_download_url(session, url, type))
79+
80+
results = await asyncio.gather(*tasks)
81+
flattened = [item for sublist in results for item in sublist]
82+
return flattened
83+
84+
async def get_download_url(session, url, type):
85+
try:
86+
async with session.get(url['url'], headers=headers) as response:
87+
if response.status == 200:
88+
data = await response.json()
89+
download_urls = []
90+
for file in data:
91+
index = -5 if type == 'viz' else -3
92+
download_urls.append({
93+
'category': url['category'],
94+
'subcategory': url['subcategory'],
95+
'geo_level': url['geo_level'],
96+
'name': file['name'][:index],
97+
'url': file['download_url'],
98+
})
99+
return download_urls
100+
else:
101+
log.info(response.status)
102+
except aiohttp.ClientConnectionError as e:
103+
log.error(f'Connection error: {e}')
104+
105+
async def get_files(urls):
106+
async with aiohttp.ClientSession() as session:
107+
tasks = []
108+
for url in urls:
109+
tasks.append(get_file(session, url))
110+
111+
results = await asyncio.gather(*tasks)
112+
return results
113+
114+
115+
async def get_file(session, url):
116+
try:
117+
async with session.get(url['url']) as response:
118+
if response.status == 200:
119+
text = await response.text()
120+
return {
121+
'category': url['category'],
122+
'subcategory': url['subcategory'],
123+
'geo_level': url['geo_level'],
124+
'name': url['name'],
125+
'file': text,
126+
}
127+
else:
128+
log.info(response.status)
129+
except aiohttp.ClientConnectionError as e:
130+
log.error(f'Connection error: {e}')
131+
132+
133+
# def get_file(url):
134+
# try:
135+
# r = requests.get(url)
136+
# r.raise_for_status()
137+
138+
# except requests.exceptions.HTTPError as e:
139+
# log.error(
140+
# f"Error fetching {url}: \n{e}")
141+
142+
# return r.json()
143+
144+
async def save_content(geo_level):
145+
all_urls = []
146+
for geo_level in ['region', 'county', 'municipality']:
147+
md_download_urls = await get_download_urls(geo_level, 'md')
148+
all_urls += md_download_urls
149+
150+
files = await get_files(all_urls)
151+
df = pd.DataFrame(files)
152+
153+
current_local_time = datetime.now()
154+
df['create_date'] = current_local_time
155+
df.to_sql('content', con=engine, if_exists='replace',index=False)
156+
157+
async def save_visualizations(geo_level):
158+
all_urls = []
159+
160+
for geo_level in ['region', 'county', 'municipality']:
161+
viz_download_urls = await get_download_urls(geo_level, 'viz')
162+
all_urls += viz_download_urls
163+
164+
165+
files = await get_files(all_urls)
166+
df = pd.DataFrame(files)
167+
168+
current_local_time = datetime.now()
169+
df['create_date'] = current_local_time
170+
df.to_sql('visualizations', con=engine, if_exists='replace',index=False)
171+
172+
asyncio.run(save_content('county'))
173+
asyncio.run(save_visualizations('county'))
174+
175+
176+
177+
178+
# content_map = copy.deepcopy(subcategory_map)
179+
# for md in files:
180+
# content_map[md['category']][md['subcategory']].append({
181+
# 'name': md['name'],
182+
# 'content': content
183+
# })
184+
185+
# return content_map

src/repository/__init__.py

Whitespace-only changes.

src/repository/profile_repository.py

Lines changed: 84 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
import logging
44
import psycopg
55
import asyncio
6+
import json
67
log = logging.getLogger(__name__)
78

89

9-
def fetch_profile(query):
10+
def fetch_one(query):
1011
cur = db.conn.cursor()
1112
response = None
1213

@@ -24,12 +25,33 @@ def fetch_profile(query):
2425

2526
return response
2627

28+
def fetch_many(query):
29+
cur = db.conn.cursor()
30+
response = None
31+
32+
try:
33+
cur.execute(query)
34+
data = cur.fetchall()
35+
column_names = [desc[0] for desc in cur.description]
36+
37+
response = []
38+
for row in data:
39+
response.append(dict(zip(column_names, row)))
40+
except psycopg.OperationalError as err:
41+
log.error(f"Connection exception executing: \n{query} \n{err}")
42+
except psycopg.Error as err:
43+
log.error(f"Other psycopg error executing: \n{query} \n{err}")
44+
except Exception as err:
45+
log.error(f"Error executing query: \n{query} \n{err}")
46+
47+
return response
48+
2749

2850
@cache(expire=60)
2951
async def fetch_county(geoid):
3052
log.info(f'Fetching county profile: {geoid}')
3153
query = f"SELECT * FROM county WHERE geoid = '{geoid}'"
32-
profile = fetch_profile(query)
54+
profile = fetch_one(query)
3355
log.info(f'Succesfully retrieved county profile: {geoid}')
3456
return profile
3557

@@ -38,14 +60,72 @@ async def fetch_county(geoid):
3860
async def fetch_municipality(geoid):
3961
log.info(f'Fetching municipality profile: {geoid}')
4062
query = f"SELECT * FROM municipality WHERE geoid = '{geoid}'"
41-
profile = fetch_profile(query)
63+
profile = fetch_one(query)
4264
log.info(f'Succesfully retrieved municipality profile: {geoid}')
4365
return profile
4466

4567
@cache(expire=60)
4668
async def fetch_region():
4769
log.info(f'Fetching regional profile')
4870
query = "SELECT * FROM region"
49-
profile = fetch_profile(query)
71+
profile = fetch_one(query)
5072
log.info(f'Succesfully retrieved regional profile')
5173
return profile
74+
75+
@cache(expire=60)
76+
async def fetch_content(geo_level):
77+
log.info(f'Fetching {geo_level} content...')
78+
query = f"SELECT category, subcategory, name, file FROM content WHERE geo_level = '{geo_level}'"
79+
response = fetch_many(query)
80+
log.info(f'Succesfully retrieved {geo_level} content')
81+
return response
82+
83+
84+
@cache(expire=60)
85+
async def fetch_visualizations(geo_level, category, subcategory, topic):
86+
log.info(f'Fetching {geo_level}/{category}/{subcategory}/{topic} visualizations...')
87+
query = f"""SELECT file FROM visualizations WHERE
88+
geo_level = '{geo_level}'
89+
AND category = '{category}'
90+
AND subcategory = '{subcategory}'
91+
AND name = '{topic}' """
92+
93+
response = fetch_one(query)
94+
log.info(f'Succesfully retrieved {geo_level}/{category}/{subcategory}/{topic} visualizations')
95+
return json.loads(response['file'])
96+
97+
async def fetch_content_template(geo_level: str, category: str, subcategory: str, topic: str):
98+
log.info(f'Fetching {geo_level}/{category}/{subcategory}/{topic} content template...')
99+
query = f"""SELECT file FROM content WHERE
100+
geo_level = '{geo_level}'
101+
AND category = '{category}'
102+
AND subcategory = '{subcategory}'
103+
AND name = '{topic}' """
104+
105+
response = fetch_one(query)
106+
log.info(f'Succesfully retrieved {geo_level}/{category}/{subcategory}/{topic} content template...')
107+
return response['file']
108+
109+
async def fetch_viz_template(geo_level: str, category: str, subcategory: str, topic: str):
110+
log.info(f'Fetching {geo_level}/{category}/{subcategory}/{topic} viz template...')
111+
query = f"""SELECT file FROM visualizations WHERE
112+
geo_level = '{geo_level}'
113+
AND category = '{category}'
114+
AND subcategory = '{subcategory}'
115+
AND name = '{topic}' """
116+
117+
response = fetch_one(query)
118+
log.info(f'Succesfully retrieved {geo_level}/{category}/{subcategory}/{topic} viz template...')
119+
return json.loads(response['file'])
120+
121+
async def fetch_template_tree(geo_level: str):
122+
query = f"select category, subcategory, name from content where geo_level = '{geo_level}'"
123+
response = fetch_many(query)
124+
return response
125+
126+
async def fetch_single_content(category: str, subcategory: str, topic: str):
127+
log.info(f'Fetching {category}/{subcategory}/{topic} content ...')
128+
query = f"select file from content where category = '{category}' and subcategory = '{subcategory}' and name = '{topic}'"
129+
response = fetch_one(query)
130+
log.info(f'Succesfully fetched /{category}/{subcategory}/{topic} content')
131+
return response['file']

src/routers/content.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
from fastapi import APIRouter
2-
from repository.profile_repository import fetch_county, fetch_municipality, fetch_region
3-
from services.content import build_content
1+
from fastapi import APIRouter, Body
2+
from repository.profile_repository import fetch_content_template, fetch_county, fetch_municipality, fetch_region
3+
from services.content import build_content, build_single_content, build_template_tree
44
from fastapi.responses import HTMLResponse
55
from fastapi.staticfiles import StaticFiles
6-
from fastapi.templating import Jinja2Templates
76

87
router = APIRouter(
98
prefix="/content",
@@ -14,7 +13,6 @@
1413
# profile = build_tract_profile(geoid)
1514
# return profile
1615

17-
templates = Jinja2Templates(directory="content")
1816

1917

2018
@router.get("/municipality/{geoid}")
@@ -35,4 +33,21 @@ async def get_county(geoid: str):
3533
async def get_region():
3634
profile = await fetch_region()
3735
content = await build_content('region', profile)
38-
return content
36+
return content
37+
38+
@router.post('/preview')
39+
async def get_content_template(category: str, subcategory: str, topic: str, body: str = Body(..., media_type="text/plain")):
40+
profile = await fetch_region()
41+
42+
template = await build_single_content(body, profile,category, subcategory, topic)
43+
return template
44+
45+
@router.get('/template/{geo_level}')
46+
async def get_content_template(geo_level: str, category: str, subcategory: str, topic: str):
47+
template = await fetch_content_template(geo_level, category, subcategory, topic)
48+
return template
49+
50+
@router.get('/template/tree/{geo_level}')
51+
async def get_template_tree(geo_level: str):
52+
tree = await build_template_tree(geo_level)
53+
return tree

src/routers/viz.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from fastapi import APIRouter
2-
from repository.profile_repository import fetch_county, fetch_municipality, fetch_region
2+
from repository.profile_repository import fetch_county, fetch_municipality, fetch_region, fetch_viz_template
33
from services.viz import build_visualizations
44
from fastapi_cache.decorator import cache
55

@@ -25,4 +25,9 @@ async def get_municipality_visualizations(geoid: str, category: str, subcategory
2525
async def get_region_visualization(category: str, subcategory: str, topic: str):
2626
profile = await fetch_region()
2727
viz = await build_visualizations('region', profile, category, subcategory, topic)
28-
return viz
28+
return viz
29+
30+
@router.get('/template/{geo_level}')
31+
async def get_viz_template(geo_level: str, category: str, subcategory: str, topic: str):
32+
template = await fetch_viz_template(geo_level, category, subcategory, topic)
33+
return template

0 commit comments

Comments
 (0)