diff --git a/backend/api_app/controllers/networks.py b/backend/api_app/controllers/networks.py
new file mode 100644
index 0000000..e822f6a
--- /dev/null
+++ b/backend/api_app/controllers/networks.py
@@ -0,0 +1,76 @@
+"""API endoipoints for app networks.
+
+/networks/ returns list of top networks.
+"""
+
+from typing import Self
+
+from litestar import Controller, get
+from litestar.exceptions import NotFoundException
+
+from api_app.models import NetworkApps, TopNetworks
+from config import get_logger
+from dbcon.queries import get_apps_for_network, get_top_networks
+
+logger = get_logger(__name__)
+
+
+def networks_overview() -> TopNetworks:
+ """Process networks and return TopNetworks class."""
+ df = get_top_networks()
+ df = df[~df["network_name"].isna()]
+ df = df.sort_values("app_count", ascending=False)
+ networks = TopNetworks(networks=df.to_dict(orient="records"))
+ return networks
+
+
+class NetworksController(Controller):
+
+ """API EndPoint return for app networks."""
+
+ path = "/api/networks/"
+
+ @get(path="/", cache=True)
+ async def top_networks(self: Self) -> TopNetworks:
+ """Handle GET request for a list of top networks.
+
+ Returns
+ -------
+ A dictionary representation of the list of networks
+ each with an id, name, type and total of apps.
+
+ """
+ logger.info(f"{self.path} start")
+ overview = networks_overview()
+
+ return overview
+
+ @get(path="/{network_name:str}", cache=3600)
+ async def get_network_apps(self: Self, network_name: str) -> NetworkApps:
+ """Handle GET request for a specific network.
+
+ Args:
+ ----
+ network_name (str): The name of the network to retrieve apps for.
+
+ Returns:
+ -------
+ json
+
+ """
+ logger.info(f"{self.path} start")
+ apps_df = get_apps_for_network(network_name)
+
+ if apps_df.empty:
+ msg = f"Network Name not found: {network_name!r}"
+ raise NotFoundException(
+ msg,
+ status_code=404,
+ )
+ apps_dict = apps_df.to_dict(orient="records")
+
+ apps = NetworkApps(
+ title=network_name,
+ apps=apps_dict,
+ )
+ return apps
diff --git a/backend/api_app/models.py b/backend/api_app/models.py
index 3abe674..aeb4767 100644
--- a/backend/api_app/models.py
+++ b/backend/api_app/models.py
@@ -68,6 +68,15 @@ class TrackerApps:
apps: list[AppDetail]
+@dataclass
+class NetworkApps:
+
+ """A network's list of apps."""
+
+ title: str
+ apps: list[AppDetail]
+
+
@dataclass
class Collection:
@@ -124,6 +133,19 @@ class TrackerDetail:
count: int
+@dataclass
+class NetworkDetail:
+
+ """Describes details of a network.
+
+ Includes its db identifier, name, and the count of its occurrences.
+ """
+
+ network: int
+ network_name: str
+ count: int
+
+
@dataclass
class TopTrackers:
@@ -132,6 +154,14 @@ class TopTrackers:
trackers: list[TrackerDetail]
+@dataclass
+class TopNetworks:
+
+ """Contains a list of NetworkDetail objects representing the top networks identified."""
+
+ networks: list[NetworkDetail]
+
+
@dataclass
class StoreCategoryDetail:
diff --git a/backend/app.py b/backend/app.py
index 0305cb9..b03c5a7 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -8,6 +8,7 @@
from api_app.controllers.apps import AppController
from api_app.controllers.categories import CategoryController
+from api_app.controllers.networks import NetworksController
from api_app.controllers.rankings import RankingsController
from api_app.controllers.trackers import TrackersController
@@ -39,6 +40,7 @@ class MyOpenAPIController(OpenAPIController):
CategoryController,
RankingsController,
TrackersController,
+ NetworksController,
],
cors_config=cors_config,
openapi_config=OpenAPIConfig(
diff --git a/backend/dbcon/queries.py b/backend/dbcon/queries.py
index b1bf215..6527861 100644
--- a/backend/dbcon/queries.py
+++ b/backend/dbcon/queries.py
@@ -40,6 +40,8 @@ def load_sql_file(file_name: str) -> str:
)
QUERY_TOP_TRACKERS = load_sql_file("query_top_trackers.sql")
QUERY_TRACKER_APPS = load_sql_file("query_tracker_apps.sql")
+QUERY_TOP_NETWORKS = load_sql_file("query_top_networks.sql")
+QUERY_NETWORK_APPS = load_sql_file("query_network_apps.sql")
def get_recent_apps(collection: str, limit: int = 20) -> pd.DataFrame:
@@ -275,6 +277,19 @@ def get_apps_for_tracker(tracker_name: str) -> pd.DataFrame:
return df
+def get_apps_for_network(network_name: str) -> pd.DataFrame:
+ """Get apps for for a network."""
+ logger.info(f"Tracker: {network_name=}")
+ df = pd.read_sql(
+ QUERY_NETWORK_APPS,
+ con=DBCON.engine,
+ params={"network_name": network_name, "mylimit": 20},
+ )
+ if not df.empty:
+ df = clean_app_df(df)
+ return df
+
+
def search_apps(search_input: str, limit: int = 100) -> pd.DataFrame:
"""Search apps by term in database."""
logger.info(f"App search: {search_input=}")
@@ -307,6 +322,16 @@ def get_top_trackers() -> pd.DataFrame:
return df
+def get_top_networks() -> pd.DataFrame:
+ """Get top networks.
+
+ Data is pre-processed by materialized views.
+
+ """
+ df = pd.read_sql(QUERY_TOP_NETWORKS, DBCON.engine)
+ return df
+
+
logger.info("set db engine")
DBCON = get_db_connection("madrone")
DBCON.set_engine()
diff --git a/backend/dbcon/sql/query_network_apps.sql b/backend/dbcon/sql/query_network_apps.sql
new file mode 100644
index 0000000..c31116f
--- /dev/null
+++ b/backend/dbcon/sql/query_network_apps.sql
@@ -0,0 +1,17 @@
+SELECT *
+FROM
+ store_apps
+WHERE
+ id IN
+ (
+ SELECT sat.store_app
+ FROM
+ store_apps_networks AS sat
+ LEFT JOIN networks AS t
+ ON
+ sat.network = t.id
+ WHERE
+ t.name = :network_name
+ )
+ORDER BY installs DESC
+LIMIT :mylimit;
diff --git a/backend/dbcon/sql/query_top_networks.sql b/backend/dbcon/sql/query_top_networks.sql
new file mode 100644
index 0000000..5b4fbca
--- /dev/null
+++ b/backend/dbcon/sql/query_top_networks.sql
@@ -0,0 +1,31 @@
+WITH
+network_counts AS (
+ SELECT
+ network,
+ count(DISTINCT store_app) AS app_count
+ FROM
+ store_apps_networks
+ GROUP BY
+ network
+),
+
+total_app_count AS (
+ SELECT count(DISTINCT store_app)
+ FROM
+ store_apps_networks
+)
+
+SELECT
+ t.name AS network_name,
+ tc.app_count,
+ total_app_count.count AS total_app_count,
+ (
+ tc.app_count / total_app_count.count::decimal
+ ) AS percent
+FROM
+ network_counts AS tc
+LEFT JOIN networks AS t
+ ON
+ tc.network = t.id
+INNER JOIN total_app_count ON
+ TRUE;
diff --git a/frontend/src/lib/NavTabs.svelte b/frontend/src/lib/NavTabs.svelte
index 24e5f2b..f1ff05d 100644
--- a/frontend/src/lib/NavTabs.svelte
+++ b/frontend/src/lib/NavTabs.svelte
@@ -30,6 +30,9 @@
TRACKERS
+ NETWORKS
ABOUT
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index be6c2ae..4d63444 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -149,6 +149,19 @@ export interface TopTrackersInfo {
};
}
+export interface NetworkDetail {
+ network_name: string;
+ app_count: number;
+ percent: number;
+}
+
+export interface TopNetworksInfo {
+ status?: number;
+ error?: string;
+ networks: {
+ streamed: Promise<{ networks: NetworkDetail[] }>;
+ };
+}
export interface AppHistoryInfo {
crawled_date: string;
review_count: number;