Skip to content

Commit c8bf975

Browse files
authored
Merge pull request #150 from ddxv/main
Add Watchlists to Account Page and New Keywords ASO dash
2 parents 84edf63 + 85a2b11 commit c8bf975

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+4154
-1590
lines changed

backend/api_app/controllers/apps.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ def create_app_country_plot_dict(app_hist: pd.DataFrame) -> pd.DataFrame:
104104
Processes each country independently using groupby to maintain separate time series.
105105
"""
106106
star_cols = ["one_star", "two_star", "three_star", "four_star", "five_star"]
107-
metrics = ["rating", "review_count", "rating_count", *star_cols]
107+
metrics = ["rating", "rating_count", *star_cols]
108108
weekly_metrics = [
109109
metric
110110
for metric in [
@@ -143,7 +143,7 @@ def process_country_group(group):
143143
group = group.resample("W").last()
144144

145145
# Replace zeros with NaN for cumulative metrics (zeros are data holes, not valid values)
146-
cumulative_metrics = ["rating_count", "review_count", *star_cols]
146+
cumulative_metrics = ["rating_count", *star_cols]
147147

148148
# Metrics to turn numeric / clean
149149
nmetrics = [
@@ -238,7 +238,6 @@ def create_app_plot_df(app_hist: pd.DataFrame) -> pd.DataFrame:
238238
"weekly_installs",
239239
"weekly_ratings",
240240
"weekly_active_users",
241-
"monthly_active_users",
242241
"weekly_ad_revenue",
243242
"weekly_iap_revenue",
244243
]
@@ -863,7 +862,14 @@ async def request_sdk_scan(
863862
return {"status": "success"}
864863

865864
@get(path="/{store_id:str}/keywords", cache=3600)
866-
async def get_app_keywords(self: Self, state: State, store_id: str) -> dict:
865+
async def get_app_keywords(
866+
self: Self,
867+
state: State,
868+
store_id: str,
869+
keyword_text: list[str] | None = Parameter(
870+
query="keyword_text", required=False
871+
),
872+
) -> dict:
867873
"""Handle GET request for a list of apps.
868874
869875
Returns
@@ -872,7 +878,12 @@ async def get_app_keywords(self: Self, state: State, store_id: str) -> dict:
872878
873879
"""
874880
start = time.perf_counter() * 1000
875-
keywords_df = get_single_app_keywords(state, store_id)
881+
if keyword_text is None:
882+
keyword_text = []
883+
884+
keywords_df = get_single_app_keywords(
885+
state, store_id, keyword_texts=keyword_text
886+
)
876887
keyword_scores = keywords_df.to_dict(orient="records")
877888
keywords_list = keywords_df["keyword_text"].tolist()
878889
keywords_dict = {"keywords": keywords_list, "keyword_scores": keyword_scores}

backend/api_app/controllers/companies.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from litestar import Controller, get
2121
from litestar.config.response_cache import CACHE_FOREVER
2222
from litestar.datastructures import State
23+
from litestar.exceptions import NotFoundException
2324
from litestar.response import Stream
2425

2526
from api_app.models import (
@@ -28,6 +29,7 @@
2829
CompaniesOverview,
2930
CompanyCategoryOverview,
3031
CompanyDetail,
32+
CompanyFollowLookup,
3133
CompanyPatternsDict,
3234
CompanyPlatformOverview,
3335
CompanyPubIDOverview,
@@ -49,6 +51,7 @@
4951
get_company_adstxt_publisher_id_apps_raw,
5052
get_company_adstxt_publishers_overview,
5153
get_company_categories_topn,
54+
get_company_follow_lookup,
5255
get_company_sdks,
5356
get_company_stats,
5457
get_company_tree,
@@ -823,6 +826,28 @@ async def company_overview(
823826
logger.info(f"GET /api/companies/{company_domain} took {duration}ms")
824827
return overview
825828

829+
@get(path="/companies/{company_domain:str}/lookup", cache=3600)
830+
async def company_lookup(
831+
self: Self,
832+
state: State,
833+
company_domain: str,
834+
) -> CompanyFollowLookup:
835+
"""Resolve a company domain to company_id metadata for follow actions."""
836+
start = time.perf_counter() * 1000
837+
df = get_company_follow_lookup(state=state, company_domain=company_domain)
838+
if df.empty:
839+
msg = f"Company domain not found: {company_domain!r}"
840+
raise NotFoundException(msg, status_code=404)
841+
842+
row = df.to_dict(orient="records")[0]
843+
duration = round((time.perf_counter() * 1000 - start), 2)
844+
logger.info(f"GET /api/companies/{company_domain}/lookup took {duration}ms")
845+
return CompanyFollowLookup(
846+
company_id=int(row["company_id"]),
847+
company_name=str(row["company_name"]),
848+
company_domain=str(row["company_domain"]),
849+
)
850+
826851
@get(
827852
path="/companies/{company_domain:str}/topapps",
828853
cache=86400,

backend/api_app/controllers/keywords.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,17 @@
55

66
from typing import Self
77

8+
import pandas as pd
89
from litestar import Controller, get
910
from litestar.datastructures import State
1011

1112
from api_app.utils import extend_app_icon_url
1213
from config import get_logger
13-
from dbcon.queries import get_keyword_apps, get_keyword_details
14+
from dbcon.queries import (
15+
get_app_keywords_history,
16+
get_keyword_apps,
17+
get_keyword_details,
18+
)
1419

1520
logger = get_logger(__name__)
1621

@@ -49,3 +54,46 @@ async def get_keyword_apps(self: Self, state: State, keyword: str) -> dict:
4954
"apple": {"ranks": df_ios.to_dict(orient="records")},
5055
"google": {"ranks": df_android.to_dict(orient="records")},
5156
}
57+
58+
@get(path="/app/{store_app_id:int}", cache=3600)
59+
async def get_app_keywords(
60+
self: Self, state: State, store_app_id: int, keyword_ids: list[int]
61+
) -> list[dict]:
62+
"""Handle GET request for app keywords history.
63+
64+
Returns
65+
-------
66+
A list of dictionary representations of the history
67+
68+
"""
69+
if not keyword_ids:
70+
return []
71+
logger.info(
72+
f"Getting keyword history for app {store_app_id} and keywords {keyword_ids}"
73+
)
74+
df = get_app_keywords_history(
75+
state, store_app_id=store_app_id, keyword_ids=tuple(keyword_ids)
76+
)
77+
if df.empty:
78+
return []
79+
80+
# Build one row per date with keyword_id columns, then carry rank forward.
81+
df["crawled_date"] = pd.to_datetime(df["crawled_date"]).dt.strftime("%Y-%m-%d")
82+
pivoted = (
83+
df.pivot_table(
84+
index="crawled_date",
85+
columns="keyword_id",
86+
values="app_rank",
87+
aggfunc="first",
88+
)
89+
.sort_index()
90+
.ffill()
91+
)
92+
93+
# Use string keys for stable JSON object fields.
94+
pivoted.columns = pivoted.columns.astype(str)
95+
pivoted = pivoted.reset_index()
96+
pivoted = pivoted.astype(object).where(pd.notna(pivoted), None)
97+
pdicts = pivoted.to_dict(orient="records")
98+
99+
return pdicts

backend/api_app/models.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,15 @@ class CompanyDetail:
211211
count: int
212212

213213

214+
@dataclass
215+
class CompanyFollowLookup:
216+
"""Canonical company metadata used by follow actions."""
217+
218+
company_id: int
219+
company_name: str
220+
company_domain: str
221+
222+
214223
@dataclass
215224
class CompanyPubIDTotals:
216225
"""Totals for a publisher ID."""

backend/dbcon/queries.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -845,6 +845,16 @@ def search_companies(state: State, search_input: str, limit: int = 10) -> pd.Dat
845845
return df
846846

847847

848+
def get_company_follow_lookup(state: State, company_domain: str) -> pd.DataFrame:
849+
"""Resolve company domain to canonical company metadata."""
850+
df = pd.read_sql(
851+
sql.company_follow_lookup,
852+
state.dbcon.engine,
853+
params={"company_domain": company_domain},
854+
)
855+
return df
856+
857+
848858
def search_apps(state: State, search_input: str, limit: int = 100) -> pd.DataFrame:
849859
"""Search apps by term in database."""
850860
logger.info(f"App search: {search_input=}")
@@ -967,10 +977,21 @@ def query_apps_crossfilter(
967977
return df
968978

969979

970-
def get_single_app_keywords(state: State, store_id: str) -> pd.DataFrame:
980+
def get_single_app_keywords(
981+
state: State, store_id: str, keyword_texts: list[str] | None = None
982+
) -> pd.DataFrame:
971983
"""Get single app keywords."""
984+
if keyword_texts is None:
985+
keyword_texts = []
986+
987+
keyword_texts = [
988+
text.strip().lower() for text in keyword_texts if text and text.strip()
989+
]
990+
972991
df = pd.read_sql(
973-
sql.single_app_keywords, state.dbcon.engine, params={"store_id": store_id}
992+
sql.single_app_keywords,
993+
state.dbcon.engine,
994+
params={"store_id": store_id, "keyword_texts": keyword_texts},
974995
)
975996
df = df.sort_values(by="d30_best_rank", ascending=True)
976997
return df
@@ -1056,3 +1077,25 @@ def insert_search_query(state: State, search_term: str, user_id: int | None) ->
10561077

10571078
with state.dbconwrite.engine.connect() as connection, connection.begin():
10581079
connection.execute(sql.insert_search_query, data)
1080+
1081+
1082+
def get_app_keywords_history(
1083+
state: State, store_app_id: int, keyword_ids: tuple[int, ...], days: int = 120
1084+
) -> pd.DataFrame:
1085+
"""Get app keyword rank history."""
1086+
start_date = (
1087+
datetime.datetime.now(datetime.UTC) - datetime.timedelta(days=days)
1088+
).strftime("%Y-%m-%d")
1089+
query = sql.app_keywords_history.bindparams(
1090+
bindparam("keyword_ids", expanding=True)
1091+
)
1092+
df = pd.read_sql_query(
1093+
query,
1094+
state.dbcon.engine,
1095+
params={
1096+
"store_app_id": store_app_id,
1097+
"keyword_ids": keyword_ids,
1098+
"start_date": start_date,
1099+
},
1100+
)
1101+
return df

backend/dbcon/sql/query_app_country_metrics_history.sql

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ SELECT
33
c.alpha2 AS country,
44
acmh.rating,
55
acmh.rating_count,
6-
acmh.review_count,
76
acmh.one_star,
87
acmh.two_star,
98
acmh.three_star,

backend/dbcon/sql/query_app_global_metrics_history.sql

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ SELECT
33
agmh.weekly_installs,
44
agmh.weekly_ratings,
55
agmh.weekly_active_users,
6-
agmh.monthly_active_users,
76
agmh.weekly_ad_revenue,
87
agmh.weekly_iap_revenue,
98
agmh.total_installs AS cumulative_installs,
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
SELECT
2+
crawled_date,
3+
store,
4+
country,
5+
keyword_id,
6+
store_app,
7+
app_rank
8+
FROM
9+
frontend.app_keyword_ranks_daily
10+
WHERE
11+
store_app = :store_app_id
12+
-- note this may need to change to = ANY() to support psycopg3
13+
AND keyword_id IN :keyword_ids
14+
AND crawled_date >= :start_date
15+
ORDER BY
16+
crawled_date DESC;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
SELECT
2+
c.id AS company_id,
3+
c.name AS company_name,
4+
ad.domain_name AS company_domain
5+
FROM domains AS qd
6+
LEFT JOIN adtech.company_domain_mapping AS cdm
7+
ON qd.id = cdm.domain_id
8+
LEFT JOIN adtech.companies AS c
9+
ON cdm.company_id = c.id
10+
LEFT JOIN domains AS ad
11+
ON c.domain_id = ad.id
12+
WHERE qd.domain_name = :company_domain
13+
ORDER BY
14+
CASE WHEN ad.domain_name = :company_domain THEN 0 ELSE 1 END,
15+
c.id
16+
LIMIT 1;

0 commit comments

Comments
 (0)