Skip to content

Commit 2df5ba5

Browse files
committed
Refactor Statistics class in statistics.py to utilize internal API for data fetching, streamline chart creation, and enhance heatmap generation methods. Remove unused imports and redundant code for improved clarity and maintainability.
1 parent 3eca0a4 commit 2df5ba5

File tree

6 files changed

+339
-376
lines changed

6 files changed

+339
-376
lines changed

py_spoo_url/_internal/__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"""
2+
Internal modules for py_spoo_url
3+
"""
4+
5+
from .api import fetch_statistics
6+
from .plotting import make_chart, make_countries_heatmap, make_unique_countries_heatmap
7+
from .exporters import export_data
8+
9+
__all__ = ["fetch_statistics", "make_chart", "make_countries_heatmap", "make_unique_countries_heatmap", "export_data"]

py_spoo_url/_internal/api.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import requests
2+
import json
3+
from typing import Optional, Any
4+
5+
6+
def fetch_statistics(short_code: str, password: Optional[str] = None) -> Any:
7+
url = f"https://spoo.me/stats/{short_code}"
8+
params = {"password": password} if password else None
9+
r = requests.post(url, data=params)
10+
if r.status_code == 200:
11+
return json.loads(r.text)
12+
else:
13+
raise Exception(f"Error {r.status_code}: {r.text}")

py_spoo_url/_internal/exporters.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import pandas as pd
2+
import json
3+
import os
4+
import shutil
5+
import zipfile
6+
from typing import Literal
7+
from .utils import create_dataframes_from_data, create_general_info_dataframe, STANDARD_DATAFRAME_CONFIGS
8+
9+
10+
def export_to_excel(data, filename: str = "export.xlsx") -> None:
11+
# Create all standard DataFrames using utility function
12+
dataframes = create_dataframes_from_data(data, STANDARD_DATAFRAME_CONFIGS)
13+
14+
# Create general info DataFrame
15+
df_general_info = create_general_info_dataframe(data, "URL")
16+
17+
# Excel sheet name mapping
18+
sheet_mapping = {
19+
"df_browser": "Browser",
20+
"df_counter": "Counter",
21+
"df_country": "Country",
22+
"df_os_name": "OS_Name",
23+
"df_referrer": "Referrer",
24+
"df_unique_browser": "Unique_Browser",
25+
"df_unique_counter": "Unique_Counter",
26+
"df_unique_country": "Unique_Country",
27+
"df_unique_os_name": "Unique_OS_Name",
28+
"df_unique_referrer": "Unique_Referrer",
29+
}
30+
31+
with pd.ExcelWriter(filename, engine="openpyxl") as writer:
32+
# Write all standard DataFrames
33+
for df_name, sheet_name in sheet_mapping.items():
34+
if df_name in dataframes:
35+
dataframes[df_name].to_excel(writer, sheet_name=sheet_name, index=False)
36+
37+
# Write general info
38+
df_general_info.to_excel(writer, sheet_name="General_Info", index=False)
39+
40+
print(f"Data successfully written to {filename}")
41+
42+
43+
def export_to_csv(data, filename: str = "export.csv") -> None:
44+
csv_directory = "csv_files"
45+
os.makedirs(csv_directory, exist_ok=True)
46+
47+
# Create all standard DataFrames using utility function
48+
dataframes = create_dataframes_from_data(data, STANDARD_DATAFRAME_CONFIGS)
49+
50+
# Create general info DataFrame (with empty string for URL column in CSV)
51+
df_general_info = create_general_info_dataframe(data, "")
52+
53+
# CSV file name mapping
54+
csv_mapping = {
55+
"df_browser": "browser.csv",
56+
"df_counter": "counter.csv",
57+
"df_country": "country.csv",
58+
"df_os_name": "os_name.csv",
59+
"df_referrer": "referrer.csv",
60+
"df_unique_browser": "unique_browser.csv",
61+
"df_unique_counter": "unique_counter.csv",
62+
"df_unique_country": "unique_country.csv",
63+
"df_unique_os_name": "unique_os_name.csv",
64+
"df_unique_referrer": "unique_referrer.csv",
65+
}
66+
67+
# Save all DataFrames to CSV files
68+
for df_name, csv_filename in csv_mapping.items():
69+
if df_name in dataframes:
70+
dataframes[df_name].to_csv(os.path.join(csv_directory, csv_filename), index=False)
71+
72+
# Save general info
73+
df_general_info.to_csv(os.path.join(csv_directory, "general_info.csv"), index=False)
74+
75+
# Create zip file
76+
with zipfile.ZipFile(f"{filename}.zip", "w") as zipf:
77+
for root, dirs, files in os.walk(csv_directory):
78+
for file in files:
79+
file_path = os.path.join(root, file)
80+
arcname = os.path.relpath(file_path, csv_directory)
81+
zipf.write(file_path, arcname=arcname)
82+
83+
# Clean up temporary directory
84+
shutil.rmtree(csv_directory)
85+
print(f"Data successfully written to {filename}.zip")
86+
87+
88+
def export_to_json(data, filename: str = "export.json") -> None:
89+
with open(filename, "w") as w:
90+
w.write(json.dumps(data, indent=4))
91+
print(f"Data successfully written to {filename}")
92+
93+
94+
def export_data(
95+
data,
96+
filename: str = "export.xlsx",
97+
filetype: Literal["csv", "xlsx", "json"] = "xlsx",
98+
) -> None:
99+
if filetype == "xlsx":
100+
export_to_excel(data, filename)
101+
elif filetype == "json":
102+
export_to_json(data, filename)
103+
elif filetype == "csv":
104+
export_to_csv(data, filename)
105+
else:
106+
raise ValueError("Invalid file type. Choose either 'csv', 'json' or 'xlsx'.")

py_spoo_url/_internal/plotting.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import matplotlib.pyplot as plt # type: ignore
2+
import matplotlib # type: ignore
3+
from mpl_toolkits.axes_grid1 import make_axes_locatable # type: ignore
4+
import geopandas as gpd # type: ignore
5+
from typing import Literal, Dict
6+
7+
8+
def make_chart(
9+
chart_data: Dict,
10+
chart_type: Literal["bar", "pie", "line", "scatter", "hist", "box", "area"] = "bar",
11+
data_label: str = None,
12+
**kwargs,
13+
) -> plt.Figure:
14+
matplotlib.rcParams["font.size"] = 15
15+
matplotlib.rcParams["axes.labelcolor"] = "Black"
16+
17+
if chart_type == "bar":
18+
plt.bar(chart_data.keys(), chart_data.values(), **kwargs)
19+
if data_label in [
20+
"last_n_days_analysis",
21+
"clicks_analysis",
22+
"unique_clicks_analysis",
23+
]:
24+
plt.xticks(rotation=90)
25+
elif chart_type == "pie":
26+
plt.pie(chart_data.values(), labels=chart_data.keys(), **kwargs)
27+
elif chart_type == "line":
28+
plt.plot(chart_data.keys(), chart_data.values(), **kwargs)
29+
elif chart_type == "scatter":
30+
plt.scatter(chart_data.keys(), chart_data.values(), **kwargs)
31+
elif chart_type == "hist":
32+
plt.hist(list(chart_data.values()), **kwargs)
33+
elif chart_type == "box":
34+
plt.boxplot(list(chart_data.values()), **kwargs)
35+
elif chart_type == "area":
36+
plt.stackplot(chart_data.keys(), chart_data.values(), **kwargs)
37+
else:
38+
raise Exception(
39+
"Invalid chart type. Valid chart types are: bar, pie, line, scatter, hist, box, area"
40+
)
41+
return plt
42+
43+
44+
def _create_heatmap(
45+
data_analysis: Dict[str, int],
46+
title: str,
47+
merge_column: str = "NAME",
48+
cmap: Literal["YlOrRd", "viridis", "plasma", "inferno", "RdPu_r"] = "YlOrRd",
49+
) -> plt.Figure:
50+
matplotlib.rcParams["font.size"] = 15
51+
matplotlib.rcParams["axes.labelcolor"] = "White"
52+
world = gpd.read_file("py_spoo_url/data/ne_110m_admin_0_countries.zip")
53+
world = world.merge(
54+
gpd.GeoDataFrame(data_analysis.items(), columns=["Country", "Value"]),
55+
how="left",
56+
left_on=merge_column,
57+
right_on="Country",
58+
)
59+
fig, ax = plt.subplots(
60+
1, 1, figsize=(15, 10), facecolor=(32 / 255, 34 / 255, 37 / 255, 0.5)
61+
)
62+
plt.subplots_adjust(left=0.05, right=0.90, bottom=0.05, top=0.95)
63+
for spine in ax.spines.values():
64+
spine.set_color((46 / 255, 48 / 255, 53 / 255))
65+
spine.set_linewidth(2)
66+
ax.tick_params(labelcolor="white")
67+
world.boundary.plot(ax=ax, linewidth=1)
68+
divider = make_axes_locatable(ax)
69+
cax = divider.append_axes("right", size="5%", pad=0.1)
70+
p = world.plot(
71+
column="Value",
72+
ax=ax,
73+
legend=True,
74+
cax=cax,
75+
cmap=cmap,
76+
edgecolor=None,
77+
legend_kwds={"label": "Clicks"},
78+
alpha=0.9,
79+
)
80+
p.set_facecolor((32 / 255, 34 / 255, 37 / 255, 0.5))
81+
cbax = cax
82+
cbax.tick_params(labelcolor="white")
83+
plt.suptitle(title, x=0.5, y=0.95, fontsize=20, fontweight=3, color="white")
84+
return plt
85+
86+
87+
def make_countries_heatmap(
88+
country_analysis: Dict[str, int],
89+
cmap: Literal["YlOrRd", "viridis", "plasma", "inferno", "RdPu_r"] = "YlOrRd",
90+
) -> plt.Figure:
91+
return _create_heatmap(
92+
data_analysis=country_analysis,
93+
title="Countries Heatmap",
94+
merge_column="NAME",
95+
cmap=cmap,
96+
)
97+
98+
99+
def make_unique_countries_heatmap(
100+
unique_country_analysis: Dict[str, int],
101+
cmap: Literal["YlOrRd", "viridis", "plasma", "inferno", "RdPu_r"] = "YlOrRd",
102+
) -> plt.Figure:
103+
return _create_heatmap(
104+
data_analysis=unique_country_analysis,
105+
title="Unique Countries Heatmap",
106+
merge_column="NAME",
107+
cmap=cmap,
108+
)

py_spoo_url/_internal/utils.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import pandas as pd
2+
from typing import Dict, List, Tuple
3+
4+
5+
def create_dataframes_from_data(data: Dict, dataframe_configs: List[Tuple[str, str, List[str]]]) -> Dict[str, pd.DataFrame]:
6+
"""
7+
Create multiple DataFrames from data dictionary based on configuration.
8+
9+
Args:
10+
data: Raw data dictionary
11+
dataframe_configs: List of tuples (data_key, df_name, column_names)
12+
13+
Returns:
14+
Dictionary mapping DataFrame names to DataFrames
15+
"""
16+
dataframes = {}
17+
for data_key, df_name, columns in dataframe_configs:
18+
if data_key in data:
19+
dataframes[df_name] = pd.DataFrame(data[data_key].items(), columns=columns)
20+
return dataframes
21+
22+
23+
def create_general_info_dataframe(data: Dict, url_column_name: str = "URL") -> pd.DataFrame:
24+
"""
25+
Create the general info DataFrame with standardized structure.
26+
27+
Args:
28+
data: Raw data dictionary
29+
url_column_name: Name for the URL column (use "" for CSV export)
30+
31+
Returns:
32+
DataFrame with general information
33+
"""
34+
return pd.DataFrame({
35+
"TOTAL CLICKS": [data["total-clicks"]],
36+
"TOTAL UNIQUE CLICKS": [data["total_unique_clicks"]],
37+
url_column_name: [data["url"]],
38+
"SHORT CODE": [data["_id"]],
39+
"MAX CLICKS": [data["max-clicks"]],
40+
"PASSWORD": [data["password"]],
41+
"CREATION DATE": [data["creation-date"]],
42+
"EXPIRED": [data["expired"]],
43+
"AVERAGE DAILY CLICKS": [data["average_daily_clicks"]],
44+
"AVERAGE MONTHLY CLICKS": [data["average_monthly_clicks"]],
45+
"AVERAGE WEEKLY CLICKS": [data["average_weekly_clicks"]],
46+
"LAST CLICK": [data["last-click"]],
47+
"LAST CLICK BROSWER": [data["last-click-browser"]],
48+
"LAST CLICK OS": [data["last-click-os"]],
49+
})
50+
51+
52+
# Configuration for standard DataFrames
53+
STANDARD_DATAFRAME_CONFIGS = [
54+
("browser", "df_browser", ["Browser", "Count"]),
55+
("counter", "df_counter", ["Date", "Count"]),
56+
("country", "df_country", ["Country", "Count"]),
57+
("os_name", "df_os_name", ["OS_Name", "Count"]),
58+
("referrer", "df_referrer", ["Referrer", "Count"]),
59+
("unique_browser", "df_unique_browser", ["Browser", "Count"]),
60+
("unique_counter", "df_unique_counter", ["Date", "Count"]),
61+
("unique_country", "df_unique_country", ["Country", "Count"]),
62+
("unique_os_name", "df_unique_os_name", ["OS_Name", "Count"]),
63+
("unique_referrer", "df_unique_referrer", ["Referrer", "Count"]),
64+
]

0 commit comments

Comments
 (0)