From c05b1112697b735e44b7d7584c8c6de40a5e8ae6 Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Sat, 22 Aug 2026 18:13:31 -0700 Subject: [PATCH 1/5] chore: enable ruff C4, PIE and UP rules for analytics (#4942) Adds the flake8-comprehensions, flake8-pie and pyupgrade rule categories to the analytics ruff selection, bringing it closer to the selection used in clevercanary/hca-validation-tools. All three are already clean, so this commit changes no code. Co-Authored-By: Claude Opus 5 (1M context) --- analytics/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analytics/pyproject.toml b/analytics/pyproject.toml index 8b1ee0ccf..98c237b0c 100644 --- a/analytics/pyproject.toml +++ b/analytics/pyproject.toml @@ -18,4 +18,4 @@ analytics = { workspace = true } dev = ["ruff==0.16.3"] [tool.ruff.lint] -select = ["E4", "E7", "E9", "F", "I", "W", "B"] +select = ["E4", "E7", "E9", "F", "I", "W", "B", "C4", "PIE", "UP"] From 55af762313b97eb9053fbf205c842e0c9ffdbded Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Sat, 22 Aug 2026 18:14:53 -0700 Subject: [PATCH 2/5] chore: enable ruff SIM rules for analytics (#4942) Adds the flake8-simplify rule category to the analytics ruff selection and collapses the one `if`/`else` block it flags into a ternary. Co-Authored-By: Claude Opus 5 (1M context) --- analytics/analytics_package/analytics/api.py | 5 +---- analytics/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/analytics/analytics_package/analytics/api.py b/analytics/analytics_package/analytics/api.py index 5637bf712..36ad65220 100644 --- a/analytics/analytics_package/analytics/api.py +++ b/analytics/analytics_package/analytics/api.py @@ -68,10 +68,7 @@ def authenticate( global next_port if port is None: - if next_port is None: - port = 8082 - else: - port = next_port + port = 8082 if next_port is None else next_port next_port = port + 1 elif next_port is None: next_port = port + 1 diff --git a/analytics/pyproject.toml b/analytics/pyproject.toml index 98c237b0c..d5f6d4e53 100644 --- a/analytics/pyproject.toml +++ b/analytics/pyproject.toml @@ -18,4 +18,4 @@ analytics = { workspace = true } dev = ["ruff==0.16.3"] [tool.ruff.lint] -select = ["E4", "E7", "E9", "F", "I", "W", "B", "C4", "PIE", "UP"] +select = ["E4", "E7", "E9", "F", "I", "W", "B", "C4", "PIE", "SIM", "UP"] From e2fed3112ebe0b025532ad1e9ffd4efdbeb8420f Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Sat, 22 Aug 2026 18:16:37 -0700 Subject: [PATCH 3/5] chore: enable ruff RUF rules for analytics (#4942) Adds the ruff-specific rule category to the analytics ruff selection, replacing a single-element tuple concatenation with unpacking and sorting the `static_site` package's `__all__`. Co-Authored-By: Claude Opus 5 (1M context) --- analytics/analytics_package/analytics/api.py | 2 +- analytics/analytics_package/analytics/static_site/__init__.py | 4 ++-- analytics/pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/analytics/analytics_package/analytics/api.py b/analytics/analytics_package/analytics/api.py index 36ad65220..59c6c433d 100644 --- a/analytics/analytics_package/analytics/api.py +++ b/analytics/analytics_package/analytics/api.py @@ -53,7 +53,7 @@ def authenticate( *other_service_params, port=None, ): - service_param_sets = (first_service_params,) + other_service_params + service_param_sets = (first_service_params, *other_service_params) all_scopes = { scope for service_params in service_param_sets for scope in service_params[0] diff --git a/analytics/analytics_package/analytics/static_site/__init__.py b/analytics/analytics_package/analytics/static_site/__init__.py index 34efb2690..068bf1505 100644 --- a/analytics/analytics_package/analytics/static_site/__init__.py +++ b/analytics/analytics_package/analytics/static_site/__init__.py @@ -3,8 +3,8 @@ from .resolve import enrich_detail_records, fetch_entity_title_map __all__ = [ - "generate_site", - "fetch_entity_title_map", "enrich_detail_records", + "fetch_entity_title_map", + "generate_site", "make_event_charts", ] diff --git a/analytics/pyproject.toml b/analytics/pyproject.toml index d5f6d4e53..300cf358e 100644 --- a/analytics/pyproject.toml +++ b/analytics/pyproject.toml @@ -18,4 +18,4 @@ analytics = { workspace = true } dev = ["ruff==0.16.3"] [tool.ruff.lint] -select = ["E4", "E7", "E9", "F", "I", "W", "B", "C4", "PIE", "SIM", "UP"] +select = ["E4", "E7", "E9", "F", "I", "W", "B", "C4", "PIE", "RUF", "SIM", "UP"] From a3e849cc28579c116104a90e037e448bb9670b03 Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Sat, 22 Aug 2026 20:04:23 -0700 Subject: [PATCH 4/5] chore: enable ruff RET rules for analytics (#4942) Adds the flake8-return rule category to the analytics ruff selection, returning expressions directly instead of assigning them first and dropping two `else` branches that follow a `return`. Co-Authored-By: Claude Opus 5 (1M context) --- analytics/analytics_package/analytics/_report_utils.py | 3 +-- analytics/analytics_package/analytics/api.py | 8 ++------ analytics/analytics_package/analytics/report_elements.py | 6 ++---- analytics/pyproject.toml | 2 +- 4 files changed, 6 insertions(+), 13 deletions(-) diff --git a/analytics/analytics_package/analytics/_report_utils.py b/analytics/analytics_package/analytics/_report_utils.py index 3b49feff4..d4768ad1c 100644 --- a/analytics/analytics_package/analytics/_report_utils.py +++ b/analytics/analytics_package/analytics/_report_utils.py @@ -137,10 +137,9 @@ def get_one_period_change_series( series_previous_reindexed = ( series_previous.reindex(combined_index) * current_length / previous_length ) - change = ((series_current_reindexed / series_previous_reindexed) - 1).replace( + return ((series_current_reindexed / series_previous_reindexed) - 1).replace( {np.inf: np.nan} ) - return change def get_change_over_time_df( diff --git a/analytics/analytics_package/analytics/api.py b/analytics/analytics_package/analytics/api.py index 59c6c433d..2625e6e35 100644 --- a/analytics/analytics_package/analytics/api.py +++ b/analytics/analytics_package/analytics/api.py @@ -189,9 +189,7 @@ def get_metrics_by_dimensions_v3_style( results.append(result) params[start_index_key] += params[max_results_key] - df = results_to_df(results) - - return df + return results_to_df(results) def get_metrics_by_dimensions_v4_style( @@ -255,9 +253,7 @@ def get_metrics_by_dimensions_v4_style( offset += max_results params["offset"] = offset - df = v4_results_to_df(results, dimensions, metrics) - - return df + return v4_results_to_df(results, dimensions, metrics) def v4_results_to_df(results, dimensions, metrics): diff --git a/analytics/analytics_package/analytics/report_elements.py b/analytics/analytics_package/analytics/report_elements.py index 83e716f08..5f9bd3024 100644 --- a/analytics/analytics_package/analytics/report_elements.py +++ b/analytics/analytics_package/analytics/report_elements.py @@ -129,8 +129,7 @@ def get_outbound_links_df(analytics_params, ignore_index=True): if not ignore_index: return df_all_links.set_index(dimension_aliases_to_keep) - else: - return df_all_links.reset_index(drop=True) + return df_all_links.reset_index(drop=True) def get_outbound_links_change( @@ -288,8 +287,7 @@ def get_one_period_change_df( ) if ignore_index: return df_current_with_changes - else: - return df_current_with_changes.reset_index() + return df_current_with_changes.reset_index() def get_page_views_over_time_df( diff --git a/analytics/pyproject.toml b/analytics/pyproject.toml index 300cf358e..14ab6ca08 100644 --- a/analytics/pyproject.toml +++ b/analytics/pyproject.toml @@ -18,4 +18,4 @@ analytics = { workspace = true } dev = ["ruff==0.16.3"] [tool.ruff.lint] -select = ["E4", "E7", "E9", "F", "I", "W", "B", "C4", "PIE", "RUF", "SIM", "UP"] +select = ["E4", "E7", "E9", "F", "I", "W", "B", "C4", "PIE", "RET", "RUF", "SIM", "UP"] From 366d4c06fb4fd715b6114b318475565fe9a81b1d Mon Sep 17 00:00:00 2001 From: hunterckx <118154470+hunterckx@users.noreply.github.com> Date: Sat, 22 Aug 2026 20:19:53 -0700 Subject: [PATCH 5/5] chore: enable ruff PTH rules for analytics (#4942) Adds the flake8-use-pathlib rule category to the analytics ruff selection and moves the static site's path handling and file I/O from `os.path`, `os` and `glob` to `pathlib`. `output_dir` is normalized to a `Path` where it enters `generate_site` and `export_data`, so callers can keep passing strings. `os.makedirs` becomes `Path.mkdir(parents=True, ...)` to preserve parent creation, and the absolute path printed at the end of a run now comes from `Path.resolve()`, which additionally resolves symlinks. Co-Authored-By: Claude Opus 5 (1M context) --- .../analytics/static_site/export.py | 36 +++++++++---------- .../analytics/static_site/generator.py | 17 ++++----- analytics/pyproject.toml | 2 +- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/analytics/analytics_package/analytics/static_site/export.py b/analytics/analytics_package/analytics/static_site/export.py index 1edb37e52..66edb123f 100644 --- a/analytics/analytics_package/analytics/static_site/export.py +++ b/analytics/analytics_package/analytics/static_site/export.py @@ -1,9 +1,8 @@ """Export analytics DataFrames to JSON files for the static site.""" -import glob import json -import os from datetime import datetime +from pathlib import Path import pandas as pd from pandas.api.types import is_object_dtype, is_string_dtype @@ -28,7 +27,7 @@ def export_df_as_json(df, col_map, change_col, filename, output_dir): col_map: Dict mapping source column names to output names. change_col: Source column name for the change metric (may be absent). filename: Output JSON filename. - output_dir: Output directory. + output_dir: Output directory, as a Path. """ if df is None or len(df) == 0: records = [] @@ -56,7 +55,7 @@ def export_df_as_json(df, col_map, change_col, filename, output_dir): if pd.isna(record.get("change")): record["change"] = None - with open(os.path.join(output_dir, filename), "w") as f: + with (output_dir / filename).open("w") as f: json.dump(records, f, indent=2) print(f" Wrote {filename} ({len(records)} records)") @@ -78,12 +77,13 @@ def export_data( current_month: Current month string (YYYY-MM). analytics_start: Analytics start date string. custom_events: List of custom event dicts with results. - output_dir: Output directory for JSON files. + output_dir: Output directory for JSON files, as a str or Path. """ - os.makedirs(output_dir, exist_ok=True) + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) - for old_file in glob.glob(os.path.join(output_dir, "event_*_detail.json")): - os.remove(old_file) + for old_file in output_dir.glob("event_*_detail.json"): + old_file.unlink() df_monthly_traffic = data["monthly_traffic"] dates = data.get("dates", {}) @@ -96,7 +96,7 @@ def export_data( traffic_data["users"] = traffic_data["users"].fillna(0).astype(int) traffic_data["pageviews"] = traffic_data["pageviews"].fillna(0).astype(int) - with open(os.path.join(output_dir, "monthly_traffic.json"), "w") as f: + with (output_dir / "monthly_traffic.json").open("w") as f: json.dump(traffic_data.to_dict(orient="records"), f, indent=2) print(f" Wrote monthly_traffic.json ({len(traffic_data)} records)") @@ -138,28 +138,28 @@ def export_data( # File downloads print("Exporting file downloads data...") file_downloads = data.get("file_downloads", 0) - with open(os.path.join(output_dir, "file_downloads.json"), "w") as f: + with (output_dir / "file_downloads.json").open("w") as f: json.dump({"total": file_downloads}, f, indent=2) print(f" Wrote file_downloads.json (total: {file_downloads})") # Access requests print("Exporting access requests data...") access_requests = data.get("access_requests", []) - with open(os.path.join(output_dir, "access_requests.json"), "w") as f: + with (output_dir / "access_requests.json").open("w") as f: json.dump(access_requests, f, indent=2) print(f" Wrote access_requests.json ({len(access_requests)} records)") # File download events (GA4 enhanced measurement) print("Exporting file download events data...") file_download_events = data.get("file_download_events", 0) - with open(os.path.join(output_dir, "file_download_events.json"), "w") as f: + with (output_dir / "file_download_events.json").open("w") as f: json.dump({"total": file_download_events}, f, indent=2) print(f" Wrote file_download_events.json (total: {file_download_events})") # Search queries print("Exporting search queries data...") search_queries = data.get("search_queries", {"total": 0, "queries": []}) - with open(os.path.join(output_dir, "search_queries.json"), "w") as f: + with (output_dir / "search_queries.json").open("w") as f: json.dump(search_queries, f, indent=2) print( f" Wrote search_queries.json ({len(search_queries.get('queries', []))} queries)" @@ -180,7 +180,7 @@ def export_data( event_entry["detail_file_column"] = event["detail_file_column"] events_output.append(event_entry) - with open(os.path.join(output_dir, "custom_events.json"), "w") as f: + with (output_dir / "custom_events.json").open("w") as f: json.dump(events_output, f, indent=2) print(f" Wrote custom_events.json ({len(events_output)} events)") @@ -190,7 +190,7 @@ def export_data( detail = data.get(f"event_{key}_detail") if detail is not None: filename = f"event_{key}_detail.json" - with open(os.path.join(output_dir, filename), "w") as f: + with (output_dir / filename).open("w") as f: json.dump(detail, f, indent=2) print(f" Wrote {filename} ({len(detail)} records)") @@ -218,13 +218,13 @@ def export_data( ) chart_output["charts"].append(chart_data) - with open(os.path.join(output_dir, "event_charts.json"), "w") as f: + with (output_dir / "event_charts.json").open("w") as f: json.dump(chart_output, f, indent=2) print(f" Wrote event_charts.json ({len(chart_output['charts'])} charts)") # Config (for the HTML template) print("Exporting site config...") - with open(os.path.join(output_dir, "config.json"), "w") as f: + with (output_dir / "config.json").open("w") as f: json.dump(config, f, indent=2) print(" Wrote config.json") @@ -242,6 +242,6 @@ def export_data( "engagement_rate": data.get("engagement_rate", {}), } - with open(os.path.join(output_dir, "meta.json"), "w") as f: + with (output_dir / "meta.json").open("w") as f: json.dump(meta, f, indent=2) print(" Wrote meta.json") diff --git a/analytics/analytics_package/analytics/static_site/generator.py b/analytics/analytics_package/analytics/static_site/generator.py index e8e9e7fee..abd2f42b9 100644 --- a/analytics/analytics_package/analytics/static_site/generator.py +++ b/analytics/analytics_package/analytics/static_site/generator.py @@ -1,12 +1,12 @@ """Generate a static analytics site from GA4 data.""" -import os import shutil +from pathlib import Path from .export import export_data from .fetch import fetch_data -TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "template") +TEMPLATE_DIR = Path(__file__).parent / "template" def generate_site( @@ -39,7 +39,7 @@ def generate_site( property_id: GA4 property ID. current_month: Current month string (YYYY-MM). analytics_start: Start date for all-time data (YYYY-MM-DD). - output_dir: Output directory for the generated site. + output_dir: Output directory for the generated site, as a str or Path. custom_events: List of dicts with "event_name" and "label" keys. Example: [{"event_name": "chat_submitted", "label": "Chat Submissions"}] historic_data_path: Path to historic UA data JSON file (optional). @@ -55,6 +55,7 @@ def generate_site( """ if custom_events is None: custom_events = [] + output_dir = Path(output_dir) print("=" * 50) print(f"Generating analytics site: {config['site_title']}") @@ -80,13 +81,13 @@ def generate_site( print("Resolving entity titles...") title_resolver(data) - os.makedirs(output_dir, exist_ok=True) - template_html = os.path.join(TEMPLATE_DIR, "index.html") - output_html = os.path.join(output_dir, "index.html") + output_dir.mkdir(parents=True, exist_ok=True) + template_html = TEMPLATE_DIR / "index.html" + output_html = output_dir / "index.html" shutil.copy2(template_html, output_html) print(f"Copied template to {output_html}") - data_dir = os.path.join(output_dir, "data") + data_dir = output_dir / "data" export_data( data=data, config=config, @@ -99,7 +100,7 @@ def generate_site( print("\n" + "=" * 50) print("Static site generation complete!") - print(f"Files written to: {os.path.abspath(output_dir)}") + print(f"Files written to: {output_dir.resolve()}") print("\nTo view the site locally, run:") print(f" cd {output_dir} && python -m http.server 8080") print("Then open http://localhost:8080 in your browser.") diff --git a/analytics/pyproject.toml b/analytics/pyproject.toml index 14ab6ca08..89f6b3c86 100644 --- a/analytics/pyproject.toml +++ b/analytics/pyproject.toml @@ -18,4 +18,4 @@ analytics = { workspace = true } dev = ["ruff==0.16.3"] [tool.ruff.lint] -select = ["E4", "E7", "E9", "F", "I", "W", "B", "C4", "PIE", "RET", "RUF", "SIM", "UP"] +select = ["E4", "E7", "E9", "F", "I", "W", "B", "C4", "PIE", "PTH", "RET", "RUF", "SIM", "UP"]