Skip to content

Commit

Permalink
All Time Cards (#238)
Browse files Browse the repository at this point in the history
  • Loading branch information
avgupta456 authored Nov 25, 2023
1 parent bf9a3c3 commit 6083f7c
Show file tree
Hide file tree
Showing 17 changed files with 97 additions and 38 deletions.
6 changes: 3 additions & 3 deletions backend/src/aggregation/layer1/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,17 @@ async def query_user_month(
return user_month


# NOTE: can only be called once every 1-2 minutes from publisher due to separate alru_cache
@alru_cache(ttl=timedelta(hours=6))
async def query_user(
user_id: str,
access_token: Optional[str],
private_access: bool = False,
start_date: date = date.today() - timedelta(365),
end_date: date = date.today(),
max_time: int = 3600, # seconds
no_cache: bool = False,
) -> Tuple[bool, UserPackage]:
# Return (possibly incomplete) within 45 seconds
# Return (possibly incomplete) within max_time seconds
start_time = datetime.now()
incomplete = False

Expand All @@ -97,7 +97,7 @@ async def query_user(
# Start with complete months and add any incomplete months
all_user_packages: List[UserPackage] = [x.data for x in curr_data if x.complete]
for month in new_months:
if datetime.now() - start_time < timedelta(seconds=40):
if datetime.now() - start_time < timedelta(seconds=max_time):
temp = await query_user_month(user_id, access_token, private_access, month)
if temp is not None:
all_user_packages.append(temp.data)
Expand Down
27 changes: 19 additions & 8 deletions backend/src/aggregation/layer2/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,22 @@

async def _get_user(
user_id: str, private_access: bool, start_date: date, end_date: date
) -> Optional[UserPackage]:
) -> Tuple[Optional[UserPackage], bool]:
user_months = await get_user_months(user_id, private_access, start_date, end_date)
if len(user_months) == 0:
return None
return None, False

expected_num_months = (
(end_date.year - start_date.year) * 12 + (end_date.month - start_date.month) + 1
)
complete = len(user_months) == expected_num_months

user_data = user_months[0].data
for user_month in user_months[1:]:
user_data += user_month.data

# TODO: handle timezone_str here
return user_data.trim(start_date, end_date)
return user_data.trim(start_date, end_date), complete


@alru_cache()
Expand All @@ -33,17 +38,23 @@ async def get_user(
start_date: date,
end_date: date,
no_cache: bool = False,
) -> Tuple[bool, Tuple[Optional[UserPackage], Optional[UpdateUserBackgroundTask]]]:
) -> Tuple[
bool, Tuple[Optional[UserPackage], bool, Optional[UpdateUserBackgroundTask]]
]:
user: Optional[PublicUserModel] = await db_get_public_user(user_id)
if user is None:
return (False, (None, None))
return (False, (None, False, None))

private_access = user.private_access or False
user_data = await _get_user(user_id, private_access, start_date, end_date)
user_data, complete = await _get_user(user_id, private_access, start_date, end_date)
background_task = UpdateUserBackgroundTask(
user_id=user_id, access_token=user.access_token, private_access=private_access
user_id=user_id,
access_token=user.access_token,
private_access=private_access,
start_date=start_date,
end_date=end_date,
)
return (user_data is not None, (user_data, background_task))
return (complete, (user_data, complete, background_task))


@alru_cache(ttl=timedelta(minutes=15))
Expand Down
2 changes: 0 additions & 2 deletions backend/src/data/github/graphql/user/contribs/contribs.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ def get_user_contribution_events(
access_token: Optional[str] = None,
) -> RawEvents:
"""Fetches user contributions (commits, issues, prs, reviews)"""
if (end_date - start_date).days > 365:
raise ValueError("date range can be at most 1 year")
query = {
"variables": {
"login": user_id,
Expand Down
3 changes: 3 additions & 0 deletions backend/src/models/background.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from datetime import date
from typing import Optional

from pydantic import BaseModel
Expand All @@ -7,3 +8,5 @@ class UpdateUserBackgroundTask(BaseModel):
user_id: str
access_token: Optional[str]
private_access: bool
start_date: Optional[date]
end_date: Optional[date]
4 changes: 4 additions & 0 deletions backend/src/processing/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,13 +46,17 @@ async def authenticate(
user_id=user_id,
access_token=access_token,
private_access=new_private_access,
start_date=None,
end_date=None,
)
else:
# first time sign up
background_task = UpdateUserBackgroundTask(
user_id=user_id,
access_token=access_token,
private_access=private_access,
start_date=None,
end_date=None,
)

await db_update_user(user_id, raw_user)
Expand Down
9 changes: 5 additions & 4 deletions backend/src/processing/user/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,19 @@ async def svg_base(
time_range: str,
demo: bool,
no_cache: bool = False,
) -> Tuple[Optional[UserPackage], Optional[UpdateUserBackgroundTask], str]:
) -> Tuple[Optional[UserPackage], bool, Optional[UpdateUserBackgroundTask], str]:
# process time_range, start_date, end_date
time_range = "one_month" if demo else time_range
start_date, end_date, time_str = use_time_range(time_range, start_date, end_date)
complete = True # overwritten later if not complete
background_task = None

# fetch data, either using demo or user method
background_task = None
if demo:
output = await get_user_demo(user_id, start_date, end_date, no_cache=no_cache)
else:
output, background_task = await get_user(
output, complete, background_task = await get_user(
user_id, start_date, end_date, no_cache=no_cache
)

return output, background_task, time_str
return output, complete, background_task, time_str
8 changes: 7 additions & 1 deletion backend/src/processing/wrapped/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ async def query_wrapped_user(
access_token = None if user is None else user.access_token
private_access = False if user is None else user.private_access or False
user_package: UserPackage = await query_user(
user_id, access_token, private_access, start_date, end_date, no_cache=True
user_id,
access_token,
private_access,
start_date,
end_date,
max_time=40,
no_cache=True,
)
wrapped_package = get_wrapped_data(user_package, year)

Expand Down
6 changes: 5 additions & 1 deletion backend/src/render/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,13 @@ def get_bar_section(
)
)
total_percent, total_items = 0, len(data_row)
diff = max(0, 300 / bar_width - data_row[-1][0])
for j, (percent, color) in enumerate(data_row):
color = color or DEFAULT_COLOR
percent = max(300 / bar_width, percent)
if j == 0:
percent -= diff
elif j == total_items - 1:
percent += diff
bar_percent = bar_width * percent / 100
bar_total = bar_width * total_percent / 100
box_size, insert = (bar_percent, 8), (bar_total, 0)
Expand Down
5 changes: 4 additions & 1 deletion backend/src/render/top_langs.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def get_top_langs_svg(
time_str: str,
use_percent: bool,
loc_metric: str,
complete: bool,
commits_excluded: int,
compact: bool,
use_animation: bool,
Expand All @@ -24,7 +25,9 @@ def get_top_langs_svg(
subheader = time_str
if not use_percent:
subheader += " | " + ("LOC Changed" if loc_metric == "changed" else "LOC Added")
if commits_excluded > 50:
if not complete:
subheader += " | Incomplete (refresh to update)"
elif commits_excluded > 50:
subheader += f" | {commits_excluded} commits excluded"

if len(data) <= 1:
Expand Down
13 changes: 9 additions & 4 deletions backend/src/render/top_repos.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# type: ignore

from collections import defaultdict
from typing import List, Tuple

from svgwrite import Drawing
Expand All @@ -14,14 +15,17 @@ def get_top_repos_svg(
data: List[RepoStats],
time_str: str,
loc_metric: str,
complete: bool,
commits_excluded: int,
use_animation: bool,
theme: str,
) -> Drawing:
header = "Most Contributed Repositories"
subheader = time_str
subheader += " | " + ("LOC Changed" if loc_metric == "changed" else "LOC Added")
if commits_excluded > 50:
if not complete:
subheader += " | Incomplete (refresh to update)"
elif commits_excluded > 50:
subheader += f" | {commits_excluded} commits excluded"

if len(data) == 0:
Expand Down Expand Up @@ -53,11 +57,12 @@ def get_top_repos_svg(
get_bar_section(d=d, dataset=dataset, theme=theme, padding=45, bar_width=195)
)

langs = {}
langs = defaultdict(int)
for x in data[:4]:
for lang in x.langs:
langs[lang.lang] = lang.color
langs = list(langs.items())[:6]
langs[(lang.lang, lang.color)] += lang.loc
langs = sorted(langs.items(), key=lambda x: x[1], reverse=True)
langs = [lang[0] for lang in langs[:6]]

columns = {1: 1, 2: 2, 3: 3, 4: 2, 5: 3, 6: 3}[len(langs)]
padding = 215 + (10 if columns == len(langs) else 0)
Expand Down
4 changes: 2 additions & 2 deletions backend/src/routers/assets/assets.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

@router.get("/error", status_code=status.HTTP_200_OK, include_in_schema=False)
async def get_error_img():
return FileResponse("./src/publisher/routers/assets/assets/error.png")
return FileResponse("./src/routers/assets/assets/error.png")


@router.get("/stopwatch", status_code=status.HTTP_200_OK, include_in_schema=False)
async def get_stopwatch_img():
return FileResponse("./src/publisher/routers/assets/assets/stopwatch.png")
return FileResponse("./src/routers/assets/assets/stopwatch.png")
27 changes: 26 additions & 1 deletion backend/src/routers/background.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,32 @@
from typing import Dict

from src.aggregation.layer1 import query_user
from src.models.background import UpdateUserBackgroundTask

# create a cache for the function
cache: Dict[str, Dict[str, bool]] = {"update_user": {}}


async def run_in_background(task: UpdateUserBackgroundTask):
if isinstance(task, UpdateUserBackgroundTask): # type: ignore
await query_user(task.user_id, task.access_token, task.private_access)
inputs = {
"user_id": task.user_id,
"access_token": task.access_token,
"private_access": task.private_access,
"start_date": task.start_date,
"end_date": task.end_date,
}

inputs = {k: v for k, v in inputs.items() if v is not None}

# check if the task is already running
if task.user_id in cache["update_user"]:
return

# add the task to the cache
cache["update_user"][task.user_id] = True

await query_user(**inputs) # type: ignore

# remove the task from the cache
del cache["update_user"][task.user_id]
2 changes: 1 addition & 1 deletion backend/src/routers/users/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ async def get_user_endpoint(
timezone_str: str = "US/Eastern",
no_cache: bool = False,
) -> Optional[UserPackage]:
output, background_task = await get_user(
output, _, background_task = await get_user(
user_id, start_date, end_date, no_cache=no_cache
)
if background_task is not None:
Expand Down
6 changes: 4 additions & 2 deletions backend/src/routers/users/svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ async def get_user_lang_svg(
use_animation: bool = True,
theme: str = "classic",
) -> Any:
output, background_task, time_str = await svg_base(
output, complete, background_task, time_str = await svg_base(
user_id, start_date, end_date, time_range, demo, no_cache
)
if background_task is not None:
Expand All @@ -57,6 +57,7 @@ async def get_user_lang_svg(
time_str=time_str,
use_percent=use_percent,
loc_metric=loc_metric,
complete=complete,
commits_excluded=commits_excluded,
compact=compact,
use_animation=use_animation,
Expand Down Expand Up @@ -84,7 +85,7 @@ async def get_user_repo_svg(
use_animation: bool = True,
theme: str = "classic",
) -> Any:
output, background_task, time_str = await svg_base(
output, complete, background_task, time_str = await svg_base(
user_id, start_date, end_date, time_range, demo, no_cache
)
if background_task is not None:
Expand All @@ -103,6 +104,7 @@ async def get_user_repo_svg(
data=processed,
time_str=time_str,
loc_metric=loc_metric,
complete=complete,
commits_excluded=commits_excluded,
use_animation=use_animation,
theme=theme,
Expand Down
2 changes: 1 addition & 1 deletion backend/src/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
def date_to_datetime(
dt: date, hour: int = 0, minute: int = 0, second: int = 0
) -> datetime:

return datetime(dt.year, dt.month, dt.day, hour, minute, second)


Expand All @@ -18,6 +17,7 @@ def use_time_range(
"three_months": (90, "Past 3 Months"),
"six_months": (180, "Past 6 Months"),
"one_year": (365, "Past 1 Year"),
"all_time": (365 * 10, "All Time"),
}

start_str = start_date.strftime("X%m/X%d/%Y").replace("X0", "X").replace("X", "")
Expand Down
10 changes: 3 additions & 7 deletions frontend/src/components/Home/DateRangeSection.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Input } from '../Generic';
const DateRangeSection = ({
selectedTimeRange,
setSelectedTimeRange,
disabled,
privateAccess,
}) => {
const timeRangeOptions = [
{ id: 1, label: 'Past 1 Month', disabled: false, value: 'one_month' },
Expand All @@ -19,6 +19,7 @@ const DateRangeSection = ({
},
{ id: 2, label: 'Past 6 Months', disabled: false, value: 'six_months' },
{ id: 3, label: 'Past 1 Year', disabled: false, value: 'one_year' },
{ id: 4, label: 'All Time', disabled: !privateAccess, value: 'all_time' },
];

const selectedOption = selectedTimeRange || timeRangeOptions[2];
Expand All @@ -30,7 +31,6 @@ const DateRangeSection = ({
options={timeRangeOptions}
selectedOption={selectedOption}
setSelectedOption={setSelectedTimeRange}
disabled={disabled}
/>
</Section>
);
Expand All @@ -39,11 +39,7 @@ const DateRangeSection = ({
DateRangeSection.propTypes = {
selectedTimeRange: PropTypes.object.isRequired,
setSelectedTimeRange: PropTypes.func.isRequired,
disabled: PropTypes.bool,
};

DateRangeSection.defaultProps = {
disabled: false,
privateAccess: PropTypes.bool.isRequired,
};

export default DateRangeSection;
1 change: 1 addition & 0 deletions frontend/src/pages/Home/stages/Customize.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const CustomizeStage = ({
<DateRangeSection
selectedTimeRange={selectedTimeRange}
setSelectedTimeRange={setSelectedTimeRange}
privateAccess={privateAccess}
/>
{selectedCard === 'langs' && (
<CheckboxSection
Expand Down

0 comments on commit 6083f7c

Please sign in to comment.