diff --git a/.github/workflows/run-checks.yml b/.github/workflows/run-checks.yml index db866b2d2..29acf089c 100644 --- a/.github/workflows/run-checks.yml +++ b/.github/workflows/run-checks.yml @@ -17,3 +17,20 @@ jobs: cd files npm ci npx tsc --noEmit + + analytics: + runs-on: ubuntu-latest + defaults: + run: + working-directory: analytics + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + enable-cache: true + - run: | + # Run Ruff checks before installing all dependencies + uv run --locked --only-group dev ruff check . + uv run --locked --only-group dev ruff format --check . + uv sync --locked + uv run --no-sync python -c "import analytics.static_site" diff --git a/.gitignore b/.gitignore index 076a0be1a..c261d674f 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,8 @@ __pycache__/ *.egg-info/ /analytics/**/site/ /analytics/venv +/analytics/.venv +.ruff_cache/ # Credentials .credentials/ diff --git a/.husky/pre-commit b/.husky/pre-commit index d9d8a4ccd..d8441b320 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -21,3 +21,21 @@ npx tsc --noEmit || false; ) echo 'TypeScript compile succeeded!' + +# Check Python, only if uv is installed +if command -v uv >/dev/null 2>&1; then + # Run Ruff lint before format check + npm run lint:python || + ( + echo '🤔 Ruff Check Failed. Make the required changes listed above, add changes and try to commit again.' + false; + ) + + npm run check-format:python || + ( + echo '🤔 Ruff Format Check Failed. Run npm run format:python and try commit again.'; + false; + ) +else + echo 'uv not installed, skipping Python checks' +fi diff --git a/analytics/.python-version b/analytics/.python-version new file mode 100644 index 000000000..e4fba2183 --- /dev/null +++ b/analytics/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/analytics/analytics_package/analytics/_report_utils.py b/analytics/analytics_package/analytics/_report_utils.py index edb31987e..3b49feff4 100644 --- a/analytics/analytics_package/analytics/_report_utils.py +++ b/analytics/analytics_package/analytics/_report_utils.py @@ -1,18 +1,28 @@ import datetime as dt -from . import api as ga -from .entities import ADDITIONAL_DATA_BEHAVIOR + import numpy as np import pandas as pd +from . import api as ga +from .entities import ADDITIONAL_DATA_BEHAVIOR + -def get_data_df(metrics, dimensions, percentage_metrics=None, percentage_suffix="_percentage", num_keep_dimensions=None, df_processor=None, **other_params): +def get_data_df( + metrics, + dimensions, + percentage_metrics=None, + percentage_suffix="_percentage", + num_keep_dimensions=None, + df_processor=None, + **other_params, +): if metrics is None: df = pd.DataFrame() else: df = ga.get_metrics_by_dimensions(metrics, dimensions, **other_params) - + if dimensions: - if len(dimensions) > 1 and not num_keep_dimensions is None: + if len(dimensions) > 1 and num_keep_dimensions is not None: df.drop(columns=dimensions[num_keep_dimensions:], inplace=True) df.set_index(dimensions[:num_keep_dimensions], inplace=True) for metric in metrics: @@ -22,32 +32,40 @@ def get_data_df(metrics, dimensions, percentage_metrics=None, percentage_suffix= except ValueError: num_column = str_column.astype(float) df[metric] = num_column - + if percentage_metrics: for metric in percentage_metrics: - df.insert(list(df.columns).index(metric) + 1, metric + percentage_suffix, df[metric] / df[metric].sum() * 100) - + df.insert( + list(df.columns).index(metric) + 1, + metric + percentage_suffix, + df[metric] / df[metric].sum() * 100, + ) + if df_processor: df = df_processor(df) - + return df + def strings_to_lists(*vals): return [[v] if isinstance(v, str) else v for v in vals] + def get_df_over_time(xlabels, metrics, dimensions, df_filter=None, **other_params): xlabels, metrics = strings_to_lists(xlabels, metrics) - + df = get_data_df(metrics, dimensions, **other_params) - + # Convert date to datetime object df.index = pd.to_datetime(df.index) - if (not df_filter is None): + if df_filter is not None: df = df_filter(df) # Rename for display - df.rename(columns={name: xlabels[i] for i, name in enumerate(df.columns)}, inplace=True) + df.rename( + columns={name: xlabels[i] for i, name in enumerate(df.columns)}, inplace=True + ) return df @@ -59,27 +77,32 @@ def get_data_df_from_fields(metrics, dimensions, **other_params): :param metrics: the metrics to get :param dimensions: the dimensions to get :param other_params: any other parameters to be passed to the get_data_df function, including service params - :return: a DataFrame with the data from the Analytics API. - The DF has an arbitrary RangeIndex, - string columns containing dimensions with names equal to the dimension alias value, + :return: a DataFrame with the data from the Analytics API. + The DF has an arbitrary RangeIndex, + string columns containing dimensions with names equal to the dimension alias value, and int columns containing metrics with names equal to the metric alias value. """ df = get_data_df( [metric["id"] for metric in metrics], [dimension["id"] for dimension in dimensions], - **other_params + **other_params, ) - return df.reset_index().rename(columns=get_rename_dict(dimensions+metrics)).copy() + return df.reset_index().rename(columns=get_rename_dict(dimensions + metrics)).copy() -def get_rename_dict(dimensions): +def get_rename_dict(fields): """Get a dictionary to rename the columns of a DataFrame.""" - return dict( - zip([dimension["id"] for dimension in dimensions], [dimension["alias"] for dimension in dimensions]) - ) + return {field["id"]: field["alias"] for field in fields} -def get_one_period_change_series(series_current, series_previous, start_current, end_current, start_previous, end_previous): +def get_one_period_change_series( + series_current, + series_previous, + start_current, + end_current, + start_previous, + end_previous, +): """ Get the percent change between two serieses, accounting for different numbers of days in the month. :param series_current: the series representing the current month @@ -94,18 +117,40 @@ def get_one_period_change_series(series_current, series_previous, start_current, assert series_current.index.names == series_previous.index.names # Reindex both serieses to have the same index combined_index = series_current.index.union(series_previous.index) - current_length = float((dt.datetime.fromisoformat(end_current) - dt.datetime.fromisoformat(start_current)).days + 1) - previous_length = float((dt.datetime.fromisoformat(end_previous) - dt.datetime.fromisoformat(start_previous)).days + 1) + current_length = float( + ( + dt.datetime.fromisoformat(end_current) + - dt.datetime.fromisoformat(start_current) + ).days + + 1 + ) + previous_length = float( + ( + dt.datetime.fromisoformat(end_previous) + - dt.datetime.fromisoformat(start_previous) + ).days + + 1 + ) assert current_length != 0 and previous_length != 0 series_current_reindexed = series_current.reindex(combined_index).fillna(0) # Adjust the values from the prior series to account for the different number of days in the month - series_previous_reindexed = (series_previous.reindex(combined_index) * current_length / previous_length) - change = ((series_current_reindexed / series_previous_reindexed) - 1).replace({np.inf: np.nan}) + series_previous_reindexed = ( + series_previous.reindex(combined_index) * current_length / previous_length + ) + change = ((series_current_reindexed / series_previous_reindexed) - 1).replace( + {np.inf: np.nan} + ) return change def get_change_over_time_df( - metrics, time_dimension, include_changes=True, additional_data_path=None, additional_data_behavior=None, strftime_format="%Y-%m", **other_params + metrics, + time_dimension, + include_changes=True, + additional_data_path=None, + additional_data_behavior=None, + strftime_format="%Y-%m", + **other_params, ): """ Get a DataFrame with the change over time for the given metrics, renamed to match metric_titles @@ -124,8 +169,10 @@ def get_change_over_time_df( [metric["id"] for metric in metrics], time_dimension["id"], sort_results=[time_dimension["id"]], - df_processor=(lambda df: df.set_index(df.index + "01").sort_index(ascending=False)), - **other_params + df_processor=( + lambda df: df.set_index(df.index + "01").sort_index(ascending=False) + ), + **other_params, ).rename({time_dimension["id"]: time_dimension["alias"]}) df_combined = pd.DataFrame() @@ -137,23 +184,28 @@ def get_change_over_time_df( df_combined = df_api.add(df_saved.astype(int), fill_value=0)[::-1] elif additional_data_behavior == ADDITIONAL_DATA_BEHAVIOR.REPLACE: df_combined = pd.concat([df_saved, df_api], ignore_index=False) - df_combined = df_combined.loc[~df_combined.index.duplicated(keep="first")].sort_index(ascending=False) + df_combined = df_combined.loc[ + ~df_combined.index.duplicated(keep="first") + ].sort_index(ascending=False) else: df_combined = df_api if include_changes: - df_combined[ - [metric["change_alias"] for metric in metrics] - ] = df_combined[ - [metric["alias"] for metric in metrics] - ].pct_change(periods=-1).replace({np.inf: np.nan}) + df_combined[[metric["change_alias"] for metric in metrics]] = ( + df_combined[[metric["alias"] for metric in metrics]] + .pct_change(periods=-1) + .replace({np.inf: np.nan}) + ) if strftime_format is not None: df_combined.index = pd.to_datetime(df_combined.index).strftime(strftime_format) return df_combined.reset_index(names=time_dimension["alias"]) -def get_change_over_time_df_multiple_events(metric, events, time_dimension, **change_over_time_args): + +def get_change_over_time_df_multiple_events( + metric, events, time_dimension, **change_over_time_args +): """ Get a DataFrame with the change over time for the given metrics, renamed to match metric_titles :param metrics: the metrics to be displayed @@ -170,11 +222,16 @@ def get_change_over_time_df_multiple_events(metric, events, time_dimension, **ch [metric], time_dimension, **change_over_time_args, - dimension_filter=f"eventName=={event['id']}" - ).rename( - columns={metric["alias"]: event["alias"], metric["change_alias"]: event["change_alias"]} - ).set_index(time_dimension["alias"]) + dimension_filter=f"eventName=={event['id']}", + ) + .rename( + columns={ + metric["alias"]: event["alias"], + metric["change_alias"]: event["change_alias"], + } + ) + .set_index(time_dimension["alias"]) for event in events ], - axis=1 - ) \ No newline at end of file + axis=1, + ) diff --git a/analytics/analytics_package/analytics/api.py b/analytics/analytics_package/analytics/api.py index 86f8dd9b5..5637bf712 100644 --- a/analytics/analytics_package/analytics/api.py +++ b/analytics/analytics_package/analytics/api.py @@ -1,211 +1,300 @@ -from google_auth_oauthlib.flow import InstalledAppFlow -from googleapiclient.discovery import build import os -import pandas as pd import re +import pandas as pd +from google_auth_oauthlib.flow import InstalledAppFlow +from googleapiclient.discovery import build ga_service_params = ( - ['https://www.googleapis.com/auth/analytics.readonly'], - 'analytics', 'v3', - {}, - lambda service, params: service.data().ga().get(**params).execute() + ["https://www.googleapis.com/auth/analytics.readonly"], + "analytics", + "v3", + {}, + lambda service, params: service.data().ga().get(**params).execute(), ) yt_service_params = ( - ['https://www.googleapis.com/auth/yt-analytics.readonly'], - 'youtubeAnalytics', 'v2', - { - 'start_date': 'startDate', - 'end_date': 'endDate', - 'start_index': 'startIndex', - 'max_results': 'maxResults', - 'segment': None - }, - lambda service, params: service.reports().query(**params).execute() + ["https://www.googleapis.com/auth/yt-analytics.readonly"], + "youtubeAnalytics", + "v2", + { + "start_date": "startDate", + "end_date": "endDate", + "start_index": "startIndex", + "max_results": "maxResults", + "segment": None, + }, + lambda service, params: service.reports().query(**params).execute(), ) drive_service_params = ( - ["https://www.googleapis.com/auth/drive", "https://www.googleapis.com/auth/spreadsheets"], - "drive", "v3", - {}, + [ + "https://www.googleapis.com/auth/drive", + "https://www.googleapis.com/auth/spreadsheets", + ], + "drive", + "v3", + {}, ) sheets_service_params = ( - ["https://www.googleapis.com/auth/spreadsheets"], - "sheets", "v4", - {} + ["https://www.googleapis.com/auth/spreadsheets"], + "sheets", + "v4", + {}, ) next_port = None default_service_system = None -def authenticate(secret_name, first_service_params=ga_service_params, *other_service_params, port=None): - 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]} +def authenticate( + secret_name, + first_service_params=ga_service_params, + *other_service_params, + port=None, +): + 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] + } + + ANALYTICS_REPORTING_CLIENT_SECRET_PATH = os.getenv(secret_name) + + flow = InstalledAppFlow.from_client_secrets_file( + ANALYTICS_REPORTING_CLIENT_SECRET_PATH, scopes=all_scopes + ) + + global next_port - ANALYTICS_REPORTING_CLIENT_SECRET_PATH=os.getenv(secret_name) + if port is None: + if next_port is None: + port = 8082 + else: + port = next_port + next_port = port + 1 + elif next_port is None: + next_port = port + 1 - flow = InstalledAppFlow.from_client_secrets_file(ANALYTICS_REPORTING_CLIENT_SECRET_PATH, - scopes=all_scopes) - - global next_port + credentials = flow.run_local_server(port=port) - if port is None: - if next_port is None: - port = 8082 - else: - port = next_port - next_port = port + 1 - elif next_port is None: - next_port = port + 1 - - credentials = flow.run_local_server(port=port) + built_systems = [ + build_service_system(service_params, credentials) + for service_params in service_param_sets + ] - built_systems = [build_service_system(service_params, credentials) for service_params in service_param_sets] + return built_systems if len(built_systems) > 1 else built_systems[0] - return built_systems if len(built_systems) > 1 else built_systems[0] def build_service_system(service_params, credentials): - if len(service_params) == 4: - service_name, service_version, param_subs_or_alt_api = service_params[1:] - query_func = None - else: - service_name, service_version, param_subs_or_alt_api, query_func = service_params[1:] - - # Build the service object. - service = build(service_name, service_version, credentials=credentials) - - service_system = (service, query_func, param_subs_or_alt_api, credentials) - - global default_service_system - if default_service_system is None: - default_service_system = service_system - - return service_system - - -def get_metrics_by_dimensions(metrics, dimensions, service_system=None, sort_results=None, **other_params): - if service_system is None: - service_system = default_service_system - - service, query_func, param_subs_or_alt_api = service_system[:3] - - metrics = normalize_id_list(metrics) - dimensions = normalize_id_list(dimensions) - sort_results = normalize_id_list(sort_results) - - if query_func is None: - return param_subs_or_alt_api(service, metrics, dimensions, sort_results=sort_results, **other_params) - - return get_metrics_by_dimensions_v3_style(service, query_func, param_subs_or_alt_api, metrics, dimensions, sort_results=sort_results, **other_params) - - -def get_metrics_by_dimensions_v3_style(service, query_func, param_subs, metrics, dimensions, property, start_date, end_date, sort_results, filters=None, segment=None, property_prefix='ga:', max_results=1000, **other_params): - metrics = join_id_list(metrics) - dimensions = join_id_list(dimensions) - sort_results = join_id_list(sort_results) - - # Dimensions and Metrics... - # Dimensions are atrributes, Metrics are quantitative measurements. e.g. city is a Dimension - # Sessions is a metric. - #https://support.google.com/analytics/answer/1033861?hl=en#site-search-attribution&zippy=%2Cin-this-article - - # Required other params: ids, start_date, end_date - # Other notable ones: filters, segment - - params = build_params({ - 'ids': property_prefix + property, - 'dimensions': dimensions, - 'metrics': metrics, - 'start_date': start_date, - 'end_date': end_date, - 'filters': filters, - 'segment': segment, - 'start_index': 1, - 'max_results': max_results, - 'sort': sort_results - }, param_subs) - - start_index_key = param_subs.get('start_index', 'start_index') - max_results_key = param_subs.get('max_results', 'max_results') - - results = [] - has_more = True - - while has_more: - result = query_func(service, params) - has_more = ('rows' in result) and (len(result['rows']) > 0) - if has_more or len(results) == 0: - results.append(result) - params[start_index_key] += params[max_results_key] - - df = results_to_df(results) - - return df - - -def get_metrics_by_dimensions_v4_style(service, metrics, dimensions, property, start_date, end_date, sort_results, metric_filter=None, dimension_filter=None, base_metric_filter=None, base_dimension_filter=None, property_prefix="properties/", max_results=1000, **other_params): - property = property_prefix + property - - params = { - "dateRanges": [{"startDate": start_date, "endDate": end_date}], - "metrics": [{"name": metric} for metric in metrics], - "dimensions": [{"name": dimension} for dimension in dimensions], - "metricFilter": parse_filter_expressions([base_metric_filter, metric_filter], True), - "dimensionFilter": parse_filter_expressions([base_dimension_filter, dimension_filter], False), - "orderBys": [({"dimension": {"dimensionName": field}} if field in dimensions else {"metric": {"metricName": field}}) for field in sort_results], - "limit": max_results - } - - offset = 0 - results = [] - rows_left = None - has_more = True - - while has_more: - result = service.properties().runReport(property=property, body=params).execute() - if rows_left is None: - rows_left = result.get("rowCount", 0) - page_row_count = len(result["rows"]) if "rows" in result else 0 - has_more = page_row_count > 0 - if has_more: - results.append(result) - rows_left -= page_row_count - if rows_left <= 0: - has_more = False - else: - offset += max_results - params["offset"] = offset - - df = v4_results_to_df(results, dimensions, metrics) - - return df + if len(service_params) == 4: + service_name, service_version, param_subs_or_alt_api = service_params[1:] + query_func = None + else: + service_name, service_version, param_subs_or_alt_api, query_func = ( + service_params[1:] + ) + + # Build the service object. + service = build(service_name, service_version, credentials=credentials) + + service_system = (service, query_func, param_subs_or_alt_api, credentials) + + global default_service_system + if default_service_system is None: + default_service_system = service_system + + return service_system + + +def get_metrics_by_dimensions( + metrics, dimensions, service_system=None, sort_results=None, **other_params +): + if service_system is None: + service_system = default_service_system + + service, query_func, param_subs_or_alt_api = service_system[:3] + + metrics = normalize_id_list(metrics) + dimensions = normalize_id_list(dimensions) + sort_results = normalize_id_list(sort_results) + + if query_func is None: + return param_subs_or_alt_api( + service, metrics, dimensions, sort_results=sort_results, **other_params + ) + + return get_metrics_by_dimensions_v3_style( + service, + query_func, + param_subs_or_alt_api, + metrics, + dimensions, + sort_results=sort_results, + **other_params, + ) + + +def get_metrics_by_dimensions_v3_style( + service, + query_func, + param_subs, + metrics, + dimensions, + property, + start_date, + end_date, + sort_results, + filters=None, + segment=None, + property_prefix="ga:", + max_results=1000, + **other_params, +): + metrics = join_id_list(metrics) + dimensions = join_id_list(dimensions) + sort_results = join_id_list(sort_results) + + # Dimensions and Metrics... + # Dimensions are atrributes, Metrics are quantitative measurements. e.g. city is a Dimension + # Sessions is a metric. + # https://support.google.com/analytics/answer/1033861?hl=en#site-search-attribution&zippy=%2Cin-this-article + + # Required other params: ids, start_date, end_date + # Other notable ones: filters, segment + + params = build_params( + { + "ids": property_prefix + property, + "dimensions": dimensions, + "metrics": metrics, + "start_date": start_date, + "end_date": end_date, + "filters": filters, + "segment": segment, + "start_index": 1, + "max_results": max_results, + "sort": sort_results, + }, + param_subs, + ) + + start_index_key = param_subs.get("start_index", "start_index") + max_results_key = param_subs.get("max_results", "max_results") + + results = [] + has_more = True + + while has_more: + result = query_func(service, params) + has_more = ("rows" in result) and (len(result["rows"]) > 0) + if has_more or len(results) == 0: + results.append(result) + params[start_index_key] += params[max_results_key] + + df = results_to_df(results) + + return df + + +def get_metrics_by_dimensions_v4_style( + service, + metrics, + dimensions, + property, + start_date, + end_date, + sort_results, + metric_filter=None, + dimension_filter=None, + base_metric_filter=None, + base_dimension_filter=None, + property_prefix="properties/", + max_results=1000, + **other_params, +): + property = property_prefix + property + + params = { + "dateRanges": [{"startDate": start_date, "endDate": end_date}], + "metrics": [{"name": metric} for metric in metrics], + "dimensions": [{"name": dimension} for dimension in dimensions], + "metricFilter": parse_filter_expressions( + [base_metric_filter, metric_filter], True + ), + "dimensionFilter": parse_filter_expressions( + [base_dimension_filter, dimension_filter], False + ), + "orderBys": [ + ( + {"dimension": {"dimensionName": field}} + if field in dimensions + else {"metric": {"metricName": field}} + ) + for field in sort_results + ], + "limit": max_results, + } + + offset = 0 + results = [] + rows_left = None + has_more = True + + while has_more: + result = ( + service.properties().runReport(property=property, body=params).execute() + ) + if rows_left is None: + rows_left = result.get("rowCount", 0) + page_row_count = len(result["rows"]) if "rows" in result else 0 + has_more = page_row_count > 0 + if has_more: + results.append(result) + rows_left -= page_row_count + if rows_left <= 0: + has_more = False + else: + offset += max_results + params["offset"] = offset + + df = v4_results_to_df(results, dimensions, metrics) + + return df + def v4_results_to_df(results, dimensions, metrics): - if (len(results) == 0): - return pd.DataFrame(columns=dimensions + metrics) + if len(results) == 0: + return pd.DataFrame(columns=dimensions + metrics) + + df = pd.DataFrame() + for result in results: + # Collect column names + column_names = [ + header["name"] for header in result.get("dimensionHeaders", []) + ] + [header["name"] for header in result.get("metricHeaders", [])] + + # Get data + if "rows" in result: + data = [ + [cell["value"] for cell in row.get("dimensionValues", [])] + + [cell["value"] for cell in row.get("metricValues", [])] + for row in result["rows"] + ] + else: + data = None - df = pd.DataFrame() - for result in results: - # Collect column names - column_names = [header["name"] for header in result.get("dimensionHeaders", [])] + [header["name"] for header in result.get("metricHeaders", [])] - - # Get data - if "rows" in result: - data = [[cell["value"] for cell in row.get("dimensionValues", [])] + [cell["value"] for cell in row.get("metricValues", [])] for row in result["rows"]] - else: - data = None + # Crete the dataframe + df = pd.concat([df, pd.DataFrame(data, columns=column_names)]) - # Crete the dataframe - df = pd.concat([df, pd.DataFrame(data, columns = column_names)]) + return df - return df ga4_service_params = ( - ['https://www.googleapis.com/auth/analytics.readonly'], - 'analyticsdata', 'v1beta', - get_metrics_by_dimensions_v4_style + ["https://www.googleapis.com/auth/analytics.readonly"], + "analyticsdata", + "v1beta", + get_metrics_by_dimensions_v4_style, ) filter_match_re = re.compile(r"^(\w+)(?:(==|>|<|>=|<=|=@|=~)|(!=|!@|!~))(.*)$") @@ -213,102 +302,114 @@ def v4_results_to_df(results, dimensions, metrics): filter_and_re = re.compile(r"((?:[^\\;]|\\(?:\\\\)*.)+)(?:;|$)") filter_or_re = re.compile(r"((?:[^\\,]|\\(?:\\\\)*.)+)(?:,|$)") filter_op_names = { - "==": "EQUAL", - "!=": "EQUAL", - ">": "GREATER_THAN", - "<": "LESS_THAN", - ">=": "GREATER_THAN_OR_EQUAL", - "<=": "LESS_THAN_OR_EQUAL", - "=@": "CONTAINS", - "!@": "CONTAINS", - "=~": "PARTIAL_REGEXP", - "!~": "PARTIAL_REGEXP" + "==": "EQUAL", + "!=": "EQUAL", + ">": "GREATER_THAN", + "<": "LESS_THAN", + ">=": "GREATER_THAN_OR_EQUAL", + "<=": "LESS_THAN_OR_EQUAL", + "=@": "CONTAINS", + "!@": "CONTAINS", + "=~": "PARTIAL_REGEXP", + "!~": "PARTIAL_REGEXP", } + def parse_filter_expression(text, is_metric): - if not isinstance(text, str): - return text - - def unescape(value): - return filter_escape_re.sub(r"\1", value) - - def parse_match(text): - field_name, plain_op, inverted_op, value = filter_match_re.match(text).groups() - op_name = filter_op_names[plain_op or inverted_op] - if is_metric: - plain_expression = { - "filter": { - "fieldName": field_name, - "numericFilter": { - "operation": op_name, - "value": { - "int64Value": value - } - } - } - } - else: - plain_expression = { - "filter": { - "fieldName": field_name, - "stringFilter": { - "matchType": "EXACT" if op_name == "EQUAL" else op_name, - "value": unescape(value), - "caseSensitive": True - } - } - } - return plain_expression if plain_op else {"notExpression": plain_expression} - - def parse_or(text): - or_terms = [parse_match(t) for t in filter_or_re.findall(text)] - return or_terms[0] if len(or_terms) == 1 else {"orGroup": {"expressions": or_terms}} - - and_terms = [parse_or(t) for t in filter_and_re.findall(text)] - return and_terms[0] if len(and_terms) == 1 else {"andGroup": {"expressions": and_terms}} + if not isinstance(text, str): + return text + + def unescape(value): + return filter_escape_re.sub(r"\1", value) + + def parse_match(text): + field_name, plain_op, inverted_op, value = filter_match_re.match(text).groups() + op_name = filter_op_names[plain_op or inverted_op] + if is_metric: + plain_expression = { + "filter": { + "fieldName": field_name, + "numericFilter": { + "operation": op_name, + "value": {"int64Value": value}, + }, + } + } + else: + plain_expression = { + "filter": { + "fieldName": field_name, + "stringFilter": { + "matchType": "EXACT" if op_name == "EQUAL" else op_name, + "value": unescape(value), + "caseSensitive": True, + }, + } + } + return plain_expression if plain_op else {"notExpression": plain_expression} + + def parse_or(text): + or_terms = [parse_match(t) for t in filter_or_re.findall(text)] + return ( + or_terms[0] + if len(or_terms) == 1 + else {"orGroup": {"expressions": or_terms}} + ) + + and_terms = [parse_or(t) for t in filter_and_re.findall(text)] + return ( + and_terms[0] + if len(and_terms) == 1 + else {"andGroup": {"expressions": and_terms}} + ) + def parse_filter_expressions(filters, is_metric): - result = None - for filter in filters: - parsed = parse_filter_expression(filter, is_metric) - if parsed: - result = parsed if result is None else {"andGroup": {"expressions": [result, parsed]}} - return result + result = None + for filter in filters: + parsed = parse_filter_expression(filter, is_metric) + if parsed: + result = ( + parsed + if result is None + else {"andGroup": {"expressions": [result, parsed]}} + ) + return result def normalize_id_list(ids): - return (ids.split(",") if isinstance(ids, str) else ids) if ids else [] + return (ids.split(",") if isinstance(ids, str) else ids) if ids else [] + def join_id_list(ids): - return ",".join(ids) if len(ids) > 0 else None + return ",".join(ids) if len(ids) > 0 else None def build_params(source, subs): - result = {} - - for key, value in source.items(): - if key in subs: - if not subs[key] is None: - result[subs[key]] = value - else: - result[key] = value - - return result + result = {} + for key, value in source.items(): + if key in subs: + if subs[key] is not None: + result[subs[key]] = value + else: + result[key] = value -def results_to_df(results): - df = pd.DataFrame() - for result in results: - # Collect column nmes - column_names = [] - for header in result.get('columnHeaders'): - column_names.append(header.get('name')) + return result - # Get data - data = result.get('rows') - # Crete the dataframe - df = pd.concat([df, pd.DataFrame(data, columns = column_names)]) +def results_to_df(results): + df = pd.DataFrame() + for result in results: + # Collect column nmes + column_names = [] + for header in result.get("columnHeaders"): + column_names.append(header.get("name")) + + # Get data + data = result.get("rows") - return df + # Crete the dataframe + df = pd.concat([df, pd.DataFrame(data, columns=column_names)]) + return df diff --git a/analytics/analytics_package/analytics/entities.py b/analytics/analytics_package/analytics/entities.py index 75dffa850..3341ada7e 100644 --- a/analytics/analytics_package/analytics/entities.py +++ b/analytics/analytics_package/analytics/entities.py @@ -45,21 +45,15 @@ # Event Names # The builtin outbound link click event. Stores the clicked URL in DIMENSION_BUILTIN_URL # Triggers under some circumstances where custom click does not, but does not include url fragments in any dimensions -EVENT_BUILTIN_CLICK = { - "id": "click", - "alias": "Builtin Outbound Link Click" -} +EVENT_BUILTIN_CLICK = {"id": "click", "alias": "Builtin Outbound Link Click"} # The custom outbound link click event. Stores the clicked URL DIMENSION_CUSTOM_URL # Includes url fragments, sometimes has a slightly different count to the built in click event EVENT_CUSTOM_CLICK = { "id": "outbound_link_clicked", - "alias": "Custom Outbound Link Click" -} -# The builtin page view event. -EVENT_PAGE_VIEW = { - "id": "page_view", - "alias": "Page View" + "alias": "Custom Outbound Link Click", } +# The builtin page view event. +EVENT_PAGE_VIEW = {"id": "page_view", "alias": "Page View"} EVENT_INDEX_BULK_DOWNLOAD_SELECTED = { "id": "index_bulk_download_selected", "alias": "Bulk Download Selected", @@ -212,7 +206,8 @@ "alias": "Outbound Link", } + # Used as arguments in get_change_over_time_df class ADDITIONAL_DATA_BEHAVIOR(Enum): - ADD = "add" # Sum the cached data with the api data - REPLACE = "replace"# Replace the api data with the cached data \ No newline at end of file + ADD = "add" # Sum the cached data with the api data + REPLACE = "replace" # Replace the api data with the cached data diff --git a/analytics/analytics_package/analytics/report_elements.py b/analytics/analytics_package/analytics/report_elements.py index adca2b7ca..83e716f08 100644 --- a/analytics/analytics_package/analytics/report_elements.py +++ b/analytics/analytics_package/analytics/report_elements.py @@ -1,13 +1,15 @@ +from urllib.parse import urlparse + import pandas as pd +from . import entities as e from ._report_utils import ( - get_data_df_from_fields, - get_one_period_change_series, get_change_over_time_df, get_change_over_time_df_multiple_events, + get_data_df_from_fields, + get_one_period_change_series, ) -from . import entities as e -from urllib.parse import urlparse + def get_bounds_for_month_and_prev(month): """ @@ -24,9 +26,10 @@ def get_bounds_for_month_and_prev(month): "start_current": start_current.strftime("%Y-%m-%d"), "end_current": end_current.strftime("%Y-%m-%d"), "start_previous": start_previous.strftime("%Y-%m-%d"), - "end_previous": end_previous.strftime("%Y-%m-%d") + "end_previous": end_previous.strftime("%Y-%m-%d"), } + def get_outbound_links_df(analytics_params, ignore_index=True): """ Get a DataFrame with outbound links from the Analytics API. Merges the builtin and custom events for outbound links. @@ -40,44 +43,62 @@ def get_outbound_links_df(analytics_params, ignore_index=True): """ assert "dimension_filter" not in analytics_params # Get the builtin "Click" event - df_builtin_links =get_data_df_from_fields( - [e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], - [e.DIMENSION_PAGE_PATH, e.DIMENSION_BUILTIN_URL, e.DIMENSION_EVENT_NAME], - dimension_filter=f"eventName=={e.EVENT_BUILTIN_CLICK['id']}", - **analytics_params, - ).groupby( - [e.DIMENSION_PAGE_PATH["alias"], e.DIMENSION_BUILTIN_URL["alias"]] - ).sum().reset_index() + df_builtin_links = ( + get_data_df_from_fields( + [e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], + [e.DIMENSION_PAGE_PATH, e.DIMENSION_BUILTIN_URL, e.DIMENSION_EVENT_NAME], + dimension_filter=f"eventName=={e.EVENT_BUILTIN_CLICK['id']}", + **analytics_params, + ) + .groupby([e.DIMENSION_PAGE_PATH["alias"], e.DIMENSION_BUILTIN_URL["alias"]]) + .sum() + .reset_index() + ) # Get the custom "outbound_link_click" event - df_custom_links = get_data_df_from_fields( - [e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], - [e.DIMENSION_EVENT_NAME, e.DIMENSION_CUSTOM_URL, e.DIMENSION_PAGE_PATH], - dimension_filter=f"eventName=={e.EVENT_CUSTOM_CLICK['id']}", - **analytics_params, - ).groupby( - [e.DIMENSION_PAGE_PATH["alias"], e.DIMENSION_CUSTOM_URL["alias"]] - ).sum().reset_index() + df_custom_links = ( + get_data_df_from_fields( + [e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], + [e.DIMENSION_EVENT_NAME, e.DIMENSION_CUSTOM_URL, e.DIMENSION_PAGE_PATH], + dimension_filter=f"eventName=={e.EVENT_CUSTOM_CLICK['id']}", + **analytics_params, + ) + .groupby([e.DIMENSION_PAGE_PATH["alias"], e.DIMENSION_CUSTOM_URL["alias"]]) + .sum() + .reset_index() + ) # Concatenate the two dataframes, avoiding duplicates # Keep the link from the builtin event, unless the link contains a #fragment, in which case keep the link from the custom event df_builtin_links["builtin"] = True - df_builtin_links["truncated_url"] = df_builtin_links[e.DIMENSION_BUILTIN_URL["alias"]] - df_custom_links["truncated_url"] = df_custom_links[e.DIMENSION_CUSTOM_URL["alias"]].str.replace(r"#.*", "", regex=True) - df_outbound_links_fragments = df_custom_links.loc[df_custom_links[e.DIMENSION_CUSTOM_URL["alias"]].str.contains("#")].copy() + df_builtin_links["truncated_url"] = df_builtin_links[ + e.DIMENSION_BUILTIN_URL["alias"] + ] + df_custom_links["truncated_url"] = df_custom_links[ + e.DIMENSION_CUSTOM_URL["alias"] + ].str.replace(r"#.*", "", regex=True) + df_outbound_links_fragments = df_custom_links.loc[ + df_custom_links[e.DIMENSION_CUSTOM_URL["alias"]].str.contains("#") + ].copy() df_outbound_links_fragments["is_fragment"] = True df_all_links = pd.concat( [df_builtin_links, df_outbound_links_fragments], ignore_index=True ) # Use the builtin link, unless the link is not in the custom links, in which case use the custom link df_all_links = df_all_links.loc[ - ~(df_all_links["truncated_url"].isin(df_outbound_links_fragments["truncated_url"]) & df_all_links["builtin"]) + ~( + df_all_links["truncated_url"].isin( + df_outbound_links_fragments["truncated_url"] + ) + & df_all_links["builtin"] + ) ].sort_values(e.METRIC_EVENT_COUNT["alias"], ascending=False) df_all_links["is_fragment"] = df_all_links["is_fragment"].fillna(False).astype(bool) # Use the builtin link, unless the link is a fragment, in which case use the custom link - df_all_links["complete_url"] = df_all_links[e.DIMENSION_BUILTIN_URL["alias"]].where( - ~df_all_links["is_fragment"], - df_all_links[e.DIMENSION_CUSTOM_URL["alias"]] + df_all_links["complete_url"] = df_all_links[e.DIMENSION_BUILTIN_URL["alias"]].where( + ~df_all_links["is_fragment"], df_all_links[e.DIMENSION_CUSTOM_URL["alias"]] + ) + df_all_links["hostname"] = df_all_links["complete_url"].map( + lambda x: urlparse(x).hostname ) - df_all_links["hostname"] = df_all_links["complete_url"].map(lambda x: urlparse(x).hostname) dimension_aliases_to_keep = [ e.DIMENSION_PAGE_PATH["alias"], e.SYNTHETIC_DIMENSION_CLICKED_LINK["alias"], @@ -87,24 +108,34 @@ def get_outbound_links_df(analytics_params, ignore_index=True): e.SYNTHETIC_METRIC_CLICKS["alias"], e.METRIC_TOTAL_USERS["alias"], ] - df_all_links = df_all_links.drop( - columns=[e.DIMENSION_BUILTIN_URL["alias"], e.DIMENSION_CUSTOM_URL["alias"], "builtin", "is_fragment"] - ).rename( - columns={ - "complete_url": e.SYNTHETIC_DIMENSION_CLICKED_LINK["alias"], - e.METRIC_EVENT_COUNT["alias"]: e.SYNTHETIC_METRIC_CLICKS["alias"], - "hostname": e.SYNTHETIC_DIMENSION_CLICKED_HOSTNAME["alias"], - } - )[[ - *dimension_aliases_to_keep, *metric_aliases_to_keep - ]].copy() + df_all_links = ( + df_all_links.drop( + columns=[ + e.DIMENSION_BUILTIN_URL["alias"], + e.DIMENSION_CUSTOM_URL["alias"], + "builtin", + "is_fragment", + ] + ) + .rename( + columns={ + "complete_url": e.SYNTHETIC_DIMENSION_CLICKED_LINK["alias"], + e.METRIC_EVENT_COUNT["alias"]: e.SYNTHETIC_METRIC_CLICKS["alias"], + "hostname": e.SYNTHETIC_DIMENSION_CLICKED_HOSTNAME["alias"], + } + )[[*dimension_aliases_to_keep, *metric_aliases_to_keep]] + .copy() + ) if not ignore_index: return df_all_links.set_index(dimension_aliases_to_keep) else: return df_all_links.reset_index(drop=True) -def get_outbound_links_change(analytics_params, start_current, end_current, start_previous, end_previous): + +def get_outbound_links_change( + analytics_params, start_current, end_current, start_previous, end_previous +): """ Get a DataFrame with outbound links from the Analytics API and a comparison for the prior period @@ -113,23 +144,24 @@ def get_outbound_links_change(analytics_params, start_current, end_current, star :param end_current: the end date for the current month :param start_previous: the start date for the previous month :param end_previous: the end date for the previous month - :return: a DataFrame with the outbound links from the Analytics API. + :return: a DataFrame with the outbound links from the Analytics API. By default, dimensions and metrics both form columns. Columns are present for both metric values and metric changes from the prior period Dimensions: DIMENSION_PAGE_PATH, SYNTHETIC_DIMENSION_CLICKED_HOSTNAME, SYNTHETIC_DIMENSION_CLICKED_LINK Metrics: SYNTHETIC_METRIC_CLICKS, METRIC_TOTAL_USERS """ return get_one_period_change_df( - get_outbound_links_df, + get_outbound_links_df, [e.SYNTHETIC_METRIC_CLICKS, e.METRIC_TOTAL_USERS], - analytics_params, - start_current, - end_current, - start_previous, + analytics_params, + start_current, + end_current, + start_previous, end_previous, - sort_results=[e.SYNTHETIC_METRIC_CLICKS, e.METRIC_TOTAL_USERS] + sort_results=[e.SYNTHETIC_METRIC_CLICKS, e.METRIC_TOTAL_USERS], ) + def get_page_views_df(analytics_params, ignore_index=False): """ Get a DataFrame with page views from the Analytics API @@ -146,12 +178,21 @@ def get_page_views_df(analytics_params, ignore_index=False): [e.DIMENSION_PAGE_PATH, e.DIMENSION_EVENT_NAME], **analytics_params, dimension_filter=f"eventName=={e.EVENT_PAGE_VIEW['id']}", - )[[e.DIMENSION_PAGE_PATH["alias"], e.METRIC_PAGE_VIEWS["alias"], e.METRIC_TOTAL_USERS["alias"]]].copy() + )[ + [ + e.DIMENSION_PAGE_PATH["alias"], + e.METRIC_PAGE_VIEWS["alias"], + e.METRIC_TOTAL_USERS["alias"], + ] + ].copy() if not ignore_index: df_response = df_response.set_index(e.DIMENSION_PAGE_PATH["alias"]) return df_response -def get_page_views_change(analytics_params, start_current, end_current, start_previous, end_previous): + +def get_page_views_change( + analytics_params, start_current, end_current, start_previous, end_previous +): """ Get a DataFrame with page views from the Analytics API and a comparison for the prior month @@ -166,17 +207,29 @@ def get_page_views_change(analytics_params, start_current, end_current, start_pr Metrics: METRIC_PAGE_VIEWS, METRIC_TOTAL_USERS """ return get_one_period_change_df( - get_page_views_df, + get_page_views_df, [e.METRIC_PAGE_VIEWS, e.METRIC_TOTAL_USERS], - analytics_params, - start_current, - end_current, - start_previous, + analytics_params, + start_current, + end_current, + start_previous, end_previous, - sort_results=[e.METRIC_PAGE_VIEWS, e.METRIC_TOTAL_USERS] + sort_results=[e.METRIC_PAGE_VIEWS, e.METRIC_TOTAL_USERS], ) -def get_one_period_change_df(df_function, change_metrics, analytics_params, start_current, end_current, start_previous, end_previous, sort_results=None, sort_ascending=False, ignore_index=False): + +def get_one_period_change_df( + df_function, + change_metrics, + analytics_params, + start_current, + end_current, + start_previous, + end_previous, + sort_results=None, + sort_ascending=False, + ignore_index=False, +): """ Get a DataFrame with the change between two periods for the given metrics, renamed to match titles :param df_function: a function that returns a dataframe, with numerical columns matching the aliases of change_metrics @@ -194,7 +247,7 @@ def get_one_period_change_df(df_function, change_metrics, analytics_params, star :return: a DataFrame with the change between two periods for the given metrics, renamed to match titles Columns are dimension aliases (as strings), metric aliases (as ints), and metric change aliases (as floats) """ - + analytics_params_current = { **analytics_params, "start_date": start_current, @@ -206,45 +259,49 @@ def get_one_period_change_df(df_function, change_metrics, analytics_params, star "end_date": end_previous, } - df_current = df_function( - analytics_params_current, - ignore_index=False - ) - df_previous = df_function( - analytics_params_previous, - ignore_index=False - ) + df_current = df_function(analytics_params_current, ignore_index=False) + df_previous = df_function(analytics_params_previous, ignore_index=False) df_changes = pd.concat( [ get_one_period_change_series( - df_current[metric["alias"]], df_previous[metric["alias"]], start_current, end_current, start_previous, end_previous - ) for metric in change_metrics + df_current[metric["alias"]], + df_previous[metric["alias"]], + start_current, + end_current, + start_previous, + end_previous, + ) + for metric in change_metrics ], axis=1, ).rename( columns={metric["alias"]: metric["change_alias"] for metric in change_metrics} ) df_current_with_changes = pd.concat( - [df_current.reindex(df_changes.index).fillna(0), df_changes], - axis=1 + [df_current.reindex(df_changes.index).fillna(0), df_changes], axis=1 ) if sort_results: df_current_with_changes = df_current_with_changes.sort_values( - [metric["alias"] for metric in sort_results], ascending=sort_ascending, kind="stable" + [metric["alias"] for metric in sort_results], + ascending=sort_ascending, + kind="stable", ) if ignore_index: return df_current_with_changes else: return df_current_with_changes.reset_index() -def get_page_views_over_time_df(analytics_params, additional_data_path=None, additional_data_behavior=None): + +def get_page_views_over_time_df( + analytics_params, additional_data_path=None, additional_data_behavior=None +): """ Get a DataFrame with pageviews and total active users over time from the Analytics API. :param analytics_params: the parameters for the Analytics API, including service params, start dates, and end dates :param additional_data_path: the path to a JSON file with additional data to be added to the DataFrame, defaults to None :param additional_data_behavior: the behavior to use when adding the additional data, as an instance of ADDITIONAL_DATA_BEHAVIOR, defaults to None - :return: a DataFrame with the pageviews and total active users over time from the Analytics API. + :return: a DataFrame with the pageviews and total active users over time from the Analytics API. Columns are the dimension aliases, metrics (as ints), and change metrics (as floats) Dimensions: DIMENSION_YEAR_MONTH (as a datetime) Metrics: METRIC_ACTIVE_USERS, METRIC_PAGE_VIEWS @@ -254,9 +311,10 @@ def get_page_views_over_time_df(analytics_params, additional_data_path=None, add e.DIMENSION_YEAR_MONTH, additional_data_path=additional_data_path, additional_data_behavior=additional_data_behavior, - **analytics_params + **analytics_params, ) + def get_landing_page_df(analytics_params, ignore_index=True): """ Get a DataFrame with landing pages from the Analytics API. @@ -277,7 +335,10 @@ def get_landing_page_df(analytics_params, ignore_index=True): df_response = df_response.set_index(e.DIMENSION_LANDING_PAGE["alias"]) return df_response -def get_landing_page_change(analytics_params, start_current, end_current, start_previous, end_previous): + +def get_landing_page_change( + analytics_params, start_current, end_current, start_previous, end_previous +): """ Get a DataFrame with landing pages from the Analytics API and a comparison for the prior month :param analytics_params: the parameters for the Analytics API, including authentication and property ids @@ -285,7 +346,7 @@ def get_landing_page_change(analytics_params, start_current, end_current, start_ :param end_current: the end date for the current month :param start_previous: the start date for the previous month :param end_previous: the end date for the previous month - :return: a DataFrame with the landing pages from the Analytics API. + :return: a DataFrame with the landing pages from the Analytics API. By default, dimensions and metrics both form columns Columns are present for both metric values and metric changes from the prior period Dimensions: DIMENSION_LANDING_PAGE @@ -299,9 +360,10 @@ def get_landing_page_change(analytics_params, start_current, end_current, start_ end_current, start_previous, end_previous, - sort_results=[e.METRIC_SESSIONS] + sort_results=[e.METRIC_SESSIONS], ) - + + def get_index_table_download_df(analytics_params, ignore_index=True): """ Get a DataFrame with firect file downloads from the Analytics API. @@ -316,15 +378,28 @@ def get_index_table_download_df(analytics_params, ignore_index=True): assert "dimension_filter" not in analytics_params df_response = get_data_df_from_fields( [e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], - [e.DIMENSION_ENTITY_NAME, e.DIMENSION_RELATED_ENTITY_ID, e.DIMENSION_RELATED_ENTITY_NAME], + [ + e.DIMENSION_ENTITY_NAME, + e.DIMENSION_RELATED_ENTITY_ID, + e.DIMENSION_RELATED_ENTITY_NAME, + ], **analytics_params, dimension_filter=f"eventName=={e.EVENT_FILE_DOWNLOADED['id']}", ) if not ignore_index: - df_response = df_response.set_index([e.DIMENSION_ENTITY_NAME["alias"], e.DIMENSION_RELATED_ENTITY_ID["alias"], e.DIMENSION_RELATED_ENTITY_NAME["alias"]]) + df_response = df_response.set_index( + [ + e.DIMENSION_ENTITY_NAME["alias"], + e.DIMENSION_RELATED_ENTITY_ID["alias"], + e.DIMENSION_RELATED_ENTITY_NAME["alias"], + ] + ) return df_response -def get_index_table_download_change(analytics_params, start_current, end_current, start_previous, end_previous): + +def get_index_table_download_change( + analytics_params, start_current, end_current, start_previous, end_previous +): """ Get a DataFrame with firect file downloads from the Analytics API and a comparison for the prior month :param analytics_params: the parameters for the Analytics API, including authentication and property ids @@ -332,7 +407,7 @@ def get_index_table_download_change(analytics_params, start_current, end_current :param end_current: the end date for the current month :param start_previous: the start date for the previous month :param end_previous: the end date for the previous month - :return: a DataFrame with the landing pages from the Analytics API. + :return: a DataFrame with the landing pages from the Analytics API. By default, dimensions and metrics both form columns Columns are present for both metric values and metric changes from the prior period Dimensions: DIMENSION_ENTITY_NAME, DIMENSION_RELATED_ENTITY_ID, DIMENSION_RELATED_ENTITY_NAME @@ -346,9 +421,10 @@ def get_index_table_download_change(analytics_params, start_current, end_current end_current, start_previous, end_previous, - sort_results=[e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS] + sort_results=[e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], ) + def get_index_entity_selected_df(analytics_params, ignore_index=True): """ Get a DataFrame with index tab selections from the Analytics API. @@ -371,7 +447,10 @@ def get_index_entity_selected_df(analytics_params, ignore_index=True): df_response = df_response.set_index([e.DIMENSION_ENTITY_NAME_TAB["alias"]]) return df_response -def get_index_entity_selected_change(analytics_params, start_current, end_current, start_previous, end_previous): + +def get_index_entity_selected_change( + analytics_params, start_current, end_current, start_previous, end_previous +): """ Get a DataFrame with index tab selections from the Analytics API and a comparison for the prior month :param analytics_params: the parameters for the Analytics API, including authentication and property ids @@ -379,7 +458,7 @@ def get_index_entity_selected_change(analytics_params, start_current, end_curren :param end_current: the end date for the current month :param start_previous: the start date for the previous month :param end_previous: the end date for the previous month - :return: a DataFrame with the landing pages from the Analytics API. + :return: a DataFrame with the landing pages from the Analytics API. By default, dimensions and metrics both form columns Columns are present for both metric values and metric changes from the prior period Dimensions: DIMENSION_ENTITY_NAME_TAB @@ -393,9 +472,10 @@ def get_index_entity_selected_change(analytics_params, start_current, end_curren end_current, start_previous, end_previous, - sort_results=[e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS] + sort_results=[e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], ) + def get_index_entity_table_sorted_df(analytics_params, ignore_index=True): """ Get a DataFrame with index table sortings from the Analytics API. @@ -410,15 +490,28 @@ def get_index_entity_table_sorted_df(analytics_params, ignore_index=True): assert "dimension_filter" not in analytics_params df_response = get_data_df_from_fields( [e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], - [e.DIMENSION_ENTITY_NAME_TAB, e.DIMENSION_COLUMN_NAME, e.DIMENSION_SORT_DIRECTION], + [ + e.DIMENSION_ENTITY_NAME_TAB, + e.DIMENSION_COLUMN_NAME, + e.DIMENSION_SORT_DIRECTION, + ], **analytics_params, dimension_filter=f"eventName=={e.EVENT_ENTITY_TABLE_SORTED['id']}", ) if not ignore_index: - df_response = df_response.set_index([e.DIMENSION_ENTITY_NAME_TAB["alias"], e.DIMENSION_COLUMN_NAME["alias"], e.DIMENSION_SORT_DIRECTION["alias"]]) + df_response = df_response.set_index( + [ + e.DIMENSION_ENTITY_NAME_TAB["alias"], + e.DIMENSION_COLUMN_NAME["alias"], + e.DIMENSION_SORT_DIRECTION["alias"], + ] + ) return df_response -def get_index_entity_table_sorted_change(analytics_params, start_current, end_current, start_previous, end_previous): + +def get_index_entity_table_sorted_change( + analytics_params, start_current, end_current, start_previous, end_previous +): """ Get a DataFrame with index table sortings from the Analytics API and a comparison for the prior month @@ -441,9 +534,10 @@ def get_index_entity_table_sorted_change(analytics_params, start_current, end_cu end_current, start_previous, end_previous, - sort_results=[e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS] + sort_results=[e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], ) + def get_index_entity_table_paginated_df(analytics_params, ignore_index=True): """ Get a DataFrame with index table paginations from the Analytics API. @@ -463,10 +557,18 @@ def get_index_entity_table_paginated_df(analytics_params, ignore_index=True): dimension_filter=f"eventName=={e.EVENT_ENTITY_TABLE_PAGINATED['id']}", ) if not ignore_index: - df_response = df_response.set_index([e.DIMENSION_ENTITY_NAME_TAB["alias"], e.DIMENSION_PAGINATION_DIRECTION["alias"]]) + df_response = df_response.set_index( + [ + e.DIMENSION_ENTITY_NAME_TAB["alias"], + e.DIMENSION_PAGINATION_DIRECTION["alias"], + ] + ) return df_response -def get_index_entity_table_paginated_change(analytics_params, start_current, end_current, start_previous, end_previous): + +def get_index_entity_table_paginated_change( + analytics_params, start_current, end_current, start_previous, end_previous +): """ Get a DataFrame with index table paginations from the Analytics API and a comparison for the prior month @@ -475,7 +577,7 @@ def get_index_entity_table_paginated_change(analytics_params, start_current, end :param end_current: the end date for the current month :param start_previous: the start date for the previous month :param end_previous: the end date for the previous month - :return: a DataFrame with the landing pages from the Analytics API. + :return: a DataFrame with the landing pages from the Analytics API. By default, dimensions and metrics both form columns Columns are present for both metric values and metric changes from the prior period Dimensions: DIMENSION_ENTITY_NAME_TAB, DIMENSION_PAGINATION_DIRECTION @@ -490,9 +592,10 @@ def get_index_entity_table_paginated_change(analytics_params, start_current, end start_previous, end_previous, sort_results=[e.DIMENSION_ENTITY_NAME_TAB, e.DIMENSION_PAGINATION_DIRECTION], - sort_ascending=True + sort_ascending=True, ) + def get_index_filter_selected_df(analytics_params, ignore_index=True): """ Get a DataFrame with index filter selections from the Analytics API. @@ -512,10 +615,15 @@ def get_index_filter_selected_df(analytics_params, ignore_index=True): dimension_filter=f"eventName=={e.EVENT_FILTER_SELECTED['id']}", ) if not ignore_index: - df_response = df_response.set_index([e.DIMENSION_FILTER_NAME["alias"], e.DIMENSION_FILTER_VALUE["alias"]]) + df_response = df_response.set_index( + [e.DIMENSION_FILTER_NAME["alias"], e.DIMENSION_FILTER_VALUE["alias"]] + ) return df_response -def get_index_filter_selected_change(analytics_params, start_current, end_current, start_previous, end_previous): + +def get_index_filter_selected_change( + analytics_params, start_current, end_current, start_previous, end_previous +): """ Get a DataFrame with index filter selections from the Analytics API and a comparison for the prior month @@ -524,7 +632,7 @@ def get_index_filter_selected_change(analytics_params, start_current, end_curren :param end_current: the end date for the current month :param start_previous: the start date for the previous month :param end_previous: the end date for the previous month - :return: a DataFrame with the landing pages from the Analytics API. + :return: a DataFrame with the landing pages from the Analytics API. By default, dimensions and metrics both form columns Columns are present for both metric values and metric changes from the prior period Dimensions: DIMENSION_ENTITY_NAME_TAB, DIMENSION_FILTER_NAME, DIMENSION_FILTER_VALUE @@ -538,28 +646,30 @@ def get_index_filter_selected_change(analytics_params, start_current, end_curren end_current, start_previous, end_previous, - sort_results=[e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS] + sort_results=[e.METRIC_EVENT_COUNT, e.METRIC_TOTAL_USERS], ) -def get_event_count_over_time_df(analytics_params, events, additional_data_path=None, additional_data_behavior=None): +def get_event_count_over_time_df( + analytics_params, events, additional_data_path=None, additional_data_behavior=None +): """ Get a DataFrame with pageviews and total active users over time from the Analytics API. :param analytics_params: the parameters for the Analytics API, including service params, start dates, and end dates :param additional_data_path: the path to a JSON file with additional data to be added to the DataFrame, defaults to None :param additional_data_behavior: the behavior to use when adding the additional data, as an instance of ADDITIONAL_DATA_BEHAVIOR, defaults to None - :return: a DataFrame with the pageviews and total active users over time from the Analytics API. + :return: a DataFrame with the pageviews and total active users over time from the Analytics API. Columns are the dimension aliases, metrics (as ints), and change metrics (as floats) Dimensions: DIMENSION_YEAR_MONTH (as a datetime) Metrics: METRIC_ACTIVE_USERS, METRIC_PAGE_VIEWS """ - + return get_change_over_time_df_multiple_events( e.METRIC_EVENT_COUNT, events, e.DIMENSION_YEAR_MONTH, additional_data_path=additional_data_path, additional_data_behavior=additional_data_behavior, - **analytics_params - ) \ No newline at end of file + **analytics_params, + ) diff --git a/analytics/analytics_package/analytics/static_site/__init__.py b/analytics/analytics_package/analytics/static_site/__init__.py index 786a236ef..34efb2690 100644 --- a/analytics/analytics_package/analytics/static_site/__init__.py +++ b/analytics/analytics_package/analytics/static_site/__init__.py @@ -1,5 +1,10 @@ from .charts import make_event_charts from .generator import generate_site -from .resolve import fetch_entity_title_map, enrich_detail_records +from .resolve import enrich_detail_records, fetch_entity_title_map -__all__ = ["generate_site", "fetch_entity_title_map", "enrich_detail_records", "make_event_charts"] +__all__ = [ + "generate_site", + "fetch_entity_title_map", + "enrich_detail_records", + "make_event_charts", +] diff --git a/analytics/analytics_package/analytics/static_site/charts.py b/analytics/analytics_package/analytics/static_site/charts.py index 95b6c5fd3..983df6c46 100644 --- a/analytics/analytics_package/analytics/static_site/charts.py +++ b/analytics/analytics_package/analytics/static_site/charts.py @@ -19,22 +19,48 @@ def make_event_charts(entity_label, entity_path, chart_start): { "title": "Export to Terra", "series": [ - {"label": f"Single {entity_label}", "event_key": "dataset_analyze_in_terra_requested", "event_name": "dataset_analyze_in_terra_requested"}, - {"label": f"Cross-{entity_label}", "event_key": "index_analyze_in_terra_requested", "event_name": "index_analyze_in_terra_requested"}, + { + "label": f"Single {entity_label}", + "event_key": "dataset_analyze_in_terra_requested", + "event_name": "dataset_analyze_in_terra_requested", + }, + { + "label": f"Cross-{entity_label}", + "event_key": "index_analyze_in_terra_requested", + "event_name": "index_analyze_in_terra_requested", + }, ], }, { "title": "curl Command", "series": [ - {"label": f"Single {entity_label}", "event_key": "dataset_bulk_download_requested", "event_name": "bulk_download_requested", "page_path_regex": rf"^/{path_segment}/[0-9a-f-]+"}, - {"label": f"Cross-{entity_label}", "event_key": "index_bulk_download_requested", "event_name": "bulk_download_requested", "page_path_regex": rf"^(?!/{path_segment}/[0-9a-f-]+)"}, + { + "label": f"Single {entity_label}", + "event_key": "dataset_bulk_download_requested", + "event_name": "bulk_download_requested", + "page_path_regex": rf"^/{path_segment}/[0-9a-f-]+", + }, + { + "label": f"Cross-{entity_label}", + "event_key": "index_bulk_download_requested", + "event_name": "bulk_download_requested", + "page_path_regex": rf"^(?!/{path_segment}/[0-9a-f-]+)", + }, ], }, { "title": "File Manifest", "series": [ - {"label": f"Single {entity_label}", "event_key": "dataset_file_manifest_requested", "event_name": "dataset_file_manifest_requested"}, - {"label": f"Cross-{entity_label}", "event_key": "index_file_manifest_requested", "event_name": "index_file_manifest_requested"}, + { + "label": f"Single {entity_label}", + "event_key": "dataset_file_manifest_requested", + "event_name": "dataset_file_manifest_requested", + }, + { + "label": f"Cross-{entity_label}", + "event_key": "index_file_manifest_requested", + "event_name": "index_file_manifest_requested", + }, ], }, ], diff --git a/analytics/analytics_package/analytics/static_site/export.py b/analytics/analytics_package/analytics/static_site/export.py index 3920743fa..1edb37e52 100644 --- a/analytics/analytics_package/analytics/static_site/export.py +++ b/analytics/analytics_package/analytics/static_site/export.py @@ -9,9 +9,9 @@ from pandas.api.types import is_object_dtype, is_string_dtype from ..entities import ( - DIMENSION_PAGE_PATH, DIMENSION_FILTER_NAME, DIMENSION_FILTER_VALUE, + DIMENSION_PAGE_PATH, METRIC_EVENT_COUNT, METRIC_PAGE_VIEWS, SYNTHETIC_DIMENSION_CLICKED_LINK, @@ -44,7 +44,11 @@ def export_df_as_json(df, col_map, change_col, filename, output_dir): for col in output_names: col_dtype = export[col].dtype - if not (col == "change" or is_object_dtype(col_dtype) or is_string_dtype(col_dtype)): + if not ( + col == "change" + or is_object_dtype(col_dtype) + or is_string_dtype(col_dtype) + ): export[col] = export[col].fillna(0).astype(int) records = export.to_dict(orient="records") @@ -57,7 +61,15 @@ def export_df_as_json(df, col_map, change_col, filename, output_dir): print(f" Wrote {filename} ({len(records)} records)") -def export_data(data, config, current_month, analytics_start, custom_events, output_dir, event_charts=None): +def export_data( + data, + config, + current_month, + analytics_start, + custom_events, + output_dir, + event_charts=None, +): """Export all analytics data to JSON files. Args: @@ -101,7 +113,10 @@ def export_data(data, config, current_month, analytics_start, custom_events, out print("Exporting outbound links data...") export_df_as_json( data["outbound"], - {SYNTHETIC_DIMENSION_CLICKED_LINK["alias"]: "link", SYNTHETIC_METRIC_CLICKS["alias"]: "clicks"}, + { + SYNTHETIC_DIMENSION_CLICKED_LINK["alias"]: "link", + SYNTHETIC_METRIC_CLICKS["alias"]: "clicks", + }, SYNTHETIC_METRIC_CLICKS["change_alias"], "outbound_links.json", output_dir, @@ -110,7 +125,11 @@ def export_data(data, config, current_month, analytics_start, custom_events, out print("Exporting filter selections data...") export_df_as_json( data.get("filter_selected"), - {DIMENSION_FILTER_NAME["alias"]: "filterName", DIMENSION_FILTER_VALUE["alias"]: "filterValue", METRIC_EVENT_COUNT["alias"]: "count"}, + { + DIMENSION_FILTER_NAME["alias"]: "filterName", + DIMENSION_FILTER_VALUE["alias"]: "filterValue", + METRIC_EVENT_COUNT["alias"]: "count", + }, METRIC_EVENT_COUNT["change_alias"], "filter_selected.json", output_dir, @@ -142,7 +161,9 @@ def export_data(data, config, current_month, analytics_start, custom_events, out search_queries = data.get("search_queries", {"total": 0, "queries": []}) with open(os.path.join(output_dir, "search_queries.json"), "w") as f: json.dump(search_queries, f, indent=2) - print(f" Wrote search_queries.json ({len(search_queries.get('queries', []))} queries)") + print( + f" Wrote search_queries.json ({len(search_queries.get('queries', []))} queries)" + ) # Custom events print("Exporting custom events data...") @@ -188,11 +209,13 @@ def export_data(data, config, current_month, analytics_start, custom_events, out for series in chart.get("series", []): key = series["event_key"] monthly_counts = data.get(f"event_chart_{key}", []) - chart_data["series"].append({ - "label": series["label"], - "event_key": key, - "data": monthly_counts, - }) + chart_data["series"].append( + { + "label": series["label"], + "event_key": key, + "data": monthly_counts, + } + ) chart_output["charts"].append(chart_data) with open(os.path.join(output_dir, "event_charts.json"), "w") as f: diff --git a/analytics/analytics_package/analytics/static_site/fetch.py b/analytics/analytics_package/analytics/static_site/fetch.py index dfb7aa283..a5fddaac1 100644 --- a/analytics/analytics_package/analytics/static_site/fetch.py +++ b/analytics/analytics_package/analytics/static_site/fetch.py @@ -2,22 +2,22 @@ import re from datetime import date -from urllib.parse import urlparse, parse_qs +from urllib.parse import parse_qs, urlparse from .. import report_elements as elements -from ..api import parse_filter_expressions from .._report_utils import get_data_df_from_fields +from ..api import parse_filter_expressions from ..entities import ( + ADDITIONAL_DATA_BEHAVIOR, + DIMENSION_CUSTOM_URL, + DIMENSION_ENTITY_NAME, + DIMENSION_EVENT_NAME, + DIMENSION_PAGE_PATH, + DIMENSION_PAGE_PATH_PLUS_QUERY, DIMENSION_YEAR_MONTH, METRIC_EVENT_COUNT, METRIC_PAGE_VIEWS, METRIC_SESSIONS, - DIMENSION_EVENT_NAME, - DIMENSION_PAGE_PATH, - DIMENSION_PAGE_PATH_PLUS_QUERY, - DIMENSION_CUSTOM_URL, - DIMENSION_ENTITY_NAME, - ADDITIONAL_DATA_BEHAVIOR, ) METRIC_ENGAGEMENT_RATE = { @@ -29,15 +29,15 @@ # broken markdown links, asset requests, etc.). SUSPICIOUS_PAGE_PATH_RE = re.compile( r"(" - r"^/?\].*" # broken markdown links e.g. /](https://...) - r"|.*https?://.*" # concatenated URLs e.g. /overview/securityhttps://... - r"|^/[^/]*@[^/]*" # email-as-path e.g. /help@lists... - r"|^//.*" # double-slash probes e.g. //checkout/ - r"|^/[^/]*\.[^/]+$" # file extensions at root e.g. /robots.txt, /favicon-32x32.png - r"|^/feed$" # RSS probes - r"|^/\).*" # broken parens e.g. /), /). - r"|^/[^/]*\).*" # broken parens e.g. /events) - r"|^/docs(-\w+)?/" # CMS probes e.g. /docs/, /docs-EN/ + r"^/?\].*" # broken markdown links e.g. /](https://...) + r"|.*https?://.*" # concatenated URLs e.g. /overview/securityhttps://... + r"|^/[^/]*@[^/]*" # email-as-path e.g. /help@lists... + r"|^//.*" # double-slash probes e.g. //checkout/ + r"|^/[^/]*\.[^/]+$" # file extensions at root e.g. /robots.txt, /favicon-32x32.png + r"|^/feed$" # RSS probes + r"|^/\).*" # broken parens e.g. /), /). + r"|^/[^/]*\).*" # broken parens e.g. /events) + r"|^/docs(-\w+)?/" # CMS probes e.g. /docs/, /docs-EN/ r")" ) @@ -71,12 +71,16 @@ def _count_events(event_name, params, page_path_regex=None, click_url_regex=None df = df[df[DIMENSION_PAGE_PATH["alias"]].str.match(page_path_regex, na=False)] if click_url_regex: - df = df[df[DIMENSION_CUSTOM_URL["alias"]].str.contains(click_url_regex, na=False)] + df = df[ + df[DIMENSION_CUSTOM_URL["alias"]].str.contains(click_url_regex, na=False) + ] return int(df[METRIC_EVENT_COUNT["alias"]].sum()) -def get_custom_event_change(event_name, params_current, params_prior, page_path_regex=None, click_url_regex=None): +def get_custom_event_change( + event_name, params_current, params_prior, page_path_regex=None, click_url_regex=None +): """Fetch a custom event count with month-over-month change. Args: @@ -89,8 +93,12 @@ def get_custom_event_change(event_name, params_current, params_prior, page_path_ Returns: Dict with "current", "prior", and "change" keys. """ - current_count = _count_events(event_name, params_current, page_path_regex, click_url_regex) - prior_count = _count_events(event_name, params_prior, page_path_regex, click_url_regex) + current_count = _count_events( + event_name, params_current, page_path_regex, click_url_regex + ) + prior_count = _count_events( + event_name, params_prior, page_path_regex, click_url_regex + ) change = None if prior_count > 0: @@ -99,7 +107,9 @@ def get_custom_event_change(event_name, params_current, params_prior, page_path_ return {"current": current_count, "prior": prior_count, "change": change} -def get_event_detail_table(event_name, params, page_path_regex=None, click_url_regex=None): +def get_event_detail_table( + event_name, params, page_path_regex=None, click_url_regex=None +): """Fetch event details broken down by page path and entity name. Args: @@ -130,7 +140,9 @@ def get_event_detail_table(event_name, params, page_path_regex=None, click_url_r df = df[df[DIMENSION_PAGE_PATH["alias"]].str.match(page_path_regex, na=False)] if click_url_regex: - df = df[df[DIMENSION_CUSTOM_URL["alias"]].str.contains(click_url_regex, na=False)] + df = df[ + df[DIMENSION_CUSTOM_URL["alias"]].str.contains(click_url_regex, na=False) + ] if len(df) == 0: return [] @@ -186,17 +198,23 @@ def get_access_requests(params, url_patterns): return [] url_col = DIMENSION_CUSTOM_URL["alias"] - mask = df[url_col].str.contains("|".join(re.escape(p) for p in url_patterns), case=False, na=False) + mask = df[url_col].str.contains( + "|".join(re.escape(p) for p in url_patterns), case=False, na=False + ) df = df[mask] if len(df) == 0: return [] - result = df[[DIMENSION_PAGE_PATH["alias"], url_col, METRIC_EVENT_COUNT["alias"]]].copy() + result = df[ + [DIMENSION_PAGE_PATH["alias"], url_col, METRIC_EVENT_COUNT["alias"]] + ].copy() result.columns = ["page_path", "click_url", "count"] result["count"] = result["count"].astype(int) # Normalize page paths to entity base path (e.g., /projects/UUID/sub-page -> /projects/UUID). - result["page_path"] = result["page_path"].str.replace(_ENTITY_PATH_RE, r"\1", regex=True) + result["page_path"] = result["page_path"].str.replace( + _ENTITY_PATH_RE, r"\1", regex=True + ) result = result.groupby(["page_path", "click_url"], as_index=False)["count"].sum() result = result.sort_values("count", ascending=False) return result.to_dict(orient="records") @@ -261,6 +279,7 @@ def get_search_queries(params, search_path="/search"): def _generate_month_range(start_date, end_date): """Generate a list of YYYY-MM strings covering the given date range.""" from datetime import datetime + start = datetime.strptime(start_date[:7], "%Y-%m") end = datetime.strptime(end_date[:7], "%Y-%m") months = [] @@ -301,7 +320,7 @@ def _monthly_counts_from_df(df, start_date, end_date, page_path_regex=None): grouped[month_col] = grouped[month_col].apply( lambda m: f"{m[:4]}-{m[4:]}" if len(m) == 6 and "-" not in m else m ) - counts_by_month = dict(zip(grouped[month_col], grouped[count_col].astype(int))) + counts_by_month = grouped.set_index(month_col)[count_col].astype(int).to_dict() return [{"month": m, "count": counts_by_month.get(m, 0)} for m in all_months] @@ -367,7 +386,9 @@ def fetch_data( if exclude_dates: for d in exclude_dates: if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", d): - raise ValueError(f"exclude_dates entries must be YYYY-MM-DD, got: {d!r}") + raise ValueError( + f"exclude_dates entries must be YYYY-MM-DD, got: {d!r}" + ) date.fromisoformat(d) base_dimension_filter = parse_filter_expressions( [ @@ -395,16 +416,37 @@ def fetch_data( } if base_dimension_filter: params["base_dimension_filter"] = base_dimension_filter - params_all_time = {**params, "start_date": analytics_start, "end_date": end_date_current} - params_prior = {**params, "start_date": start_date_prior, "end_date": end_date_prior} + params_all_time = { + **params, + "start_date": analytics_start, + "end_date": end_date_current, + } + params_prior = { + **params, + "start_date": start_date_prior, + "end_date": end_date_prior, + } print("Fetching monthly traffic data...") - historic_kwargs = {"additional_data_path": historic_data_path, "additional_data_behavior": ADDITIONAL_DATA_BEHAVIOR.ADD} if historic_data_path else {} - df_monthly_traffic = elements.get_page_views_over_time_df(params_all_time, **historic_kwargs) + historic_kwargs = ( + { + "additional_data_path": historic_data_path, + "additional_data_behavior": ADDITIONAL_DATA_BEHAVIOR.ADD, + } + if historic_data_path + else {} + ) + df_monthly_traffic = elements.get_page_views_over_time_df( + params_all_time, **historic_kwargs + ) print("Fetching pageviews data...") df_pageviews = elements.get_page_views_change( - params, start_date_current, end_date_current, start_date_prior, end_date_prior, + params, + start_date_current, + end_date_current, + start_date_prior, + end_date_prior, ) if exclude_pages and df_pageviews is not None and len(df_pageviews) > 0: @@ -414,7 +456,9 @@ def fetch_data( if df_pageviews is not None and len(df_pageviews) > 0: page_col = DIMENSION_PAGE_PATH["alias"] - suspicious_mask = df_pageviews[page_col].str.match(SUSPICIOUS_PAGE_PATH_RE, na=False) + suspicious_mask = df_pageviews[page_col].str.match( + SUSPICIOUS_PAGE_PATH_RE, na=False + ) n_suspicious = suspicious_mask.sum() if n_suspicious > 0: df_pageviews = df_pageviews[~suspicious_mask] @@ -422,13 +466,21 @@ def fetch_data( print("Fetching outbound links data...") df_outbound = elements.get_outbound_links_change( - params, start_date_current, end_date_current, start_date_prior, end_date_prior, + params, + start_date_current, + end_date_current, + start_date_prior, + end_date_prior, ) print("Fetching filter selections data...") try: df_filter_selected = elements.get_index_filter_selected_change( - params, start_date_current, end_date_current, start_date_prior, end_date_prior, + params, + start_date_current, + end_date_current, + start_date_prior, + end_date_prior, ) except Exception as e: print(f" Skipped (not available for this property): {e}") @@ -436,15 +488,35 @@ def fetch_data( print("Fetching sessions and engagement data...") df_sessions_current = get_data_df_from_fields( - [METRIC_SESSIONS, METRIC_ENGAGEMENT_RATE], [], **params, + [METRIC_SESSIONS, METRIC_ENGAGEMENT_RATE], + [], + **params, ) df_sessions_prior = get_data_df_from_fields( - [METRIC_SESSIONS, METRIC_ENGAGEMENT_RATE], [], **params_prior, + [METRIC_SESSIONS, METRIC_ENGAGEMENT_RATE], + [], + **params_prior, + ) + sessions_current = ( + int(df_sessions_current[METRIC_SESSIONS["alias"]].sum()) + if len(df_sessions_current) > 0 + else 0 + ) + sessions_prior = ( + int(df_sessions_prior[METRIC_SESSIONS["alias"]].sum()) + if len(df_sessions_prior) > 0 + else 0 + ) + engagement_current = ( + float(df_sessions_current[METRIC_ENGAGEMENT_RATE["alias"]].mean()) + if len(df_sessions_current) > 0 + else 0 + ) + engagement_prior = ( + float(df_sessions_prior[METRIC_ENGAGEMENT_RATE["alias"]].mean()) + if len(df_sessions_prior) > 0 + else 0 ) - sessions_current = int(df_sessions_current[METRIC_SESSIONS["alias"]].sum()) if len(df_sessions_current) > 0 else 0 - sessions_prior = int(df_sessions_prior[METRIC_SESSIONS["alias"]].sum()) if len(df_sessions_prior) > 0 else 0 - engagement_current = float(df_sessions_current[METRIC_ENGAGEMENT_RATE["alias"]].mean()) if len(df_sessions_current) > 0 else 0 - engagement_prior = float(df_sessions_prior[METRIC_ENGAGEMENT_RATE["alias"]].mean()) if len(df_sessions_prior) > 0 else 0 print("Fetching file downloads data...") try: @@ -502,21 +574,28 @@ def fetch_data( key = event_key(event) print(f"Fetching {event['label']} data...") data[f"event_{key}"] = get_custom_event_change( - event["event_name"], params, params_prior, + event["event_name"], + params, + params_prior, page_path_regex=event.get("page_path_regex"), click_url_regex=event.get("click_url_regex"), ) if event.get("detail_table"): print(f"Fetching {event['label']} detail table...") data[f"event_{key}_detail"] = get_event_detail_table( - event["event_name"], params, + event["event_name"], + params, page_path_regex=event.get("page_path_regex"), click_url_regex=event.get("click_url_regex"), ) if event_charts: chart_start = event_charts.get("chart_start", analytics_start) - params_chart = {**params, "start_date": chart_start, "end_date": end_date_current} + params_chart = { + **params, + "start_date": chart_start, + "end_date": end_date_current, + } data["event_chart_config"] = event_charts # Group series by event_name to avoid duplicate API calls @@ -532,7 +611,9 @@ def fetch_data( df = _fetch_event_monthly_df(event_name, params_chart, needs_page_path) for series in series_list: data[f"event_chart_{series['event_key']}"] = _monthly_counts_from_df( - df, chart_start, end_date_current, + df, + chart_start, + end_date_current, page_path_regex=series.get("page_path_regex"), ) diff --git a/analytics/analytics_package/analytics/static_site/generator.py b/analytics/analytics_package/analytics/static_site/generator.py index eab09a44c..e8e9e7fee 100644 --- a/analytics/analytics_package/analytics/static_site/generator.py +++ b/analytics/analytics_package/analytics/static_site/generator.py @@ -3,8 +3,8 @@ import os import shutil -from .fetch import fetch_data from .export import export_data +from .fetch import fetch_data TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), "template") diff --git a/analytics/analytics_package/pyproject.toml b/analytics/analytics_package/pyproject.toml new file mode 100644 index 000000000..37faa2baf --- /dev/null +++ b/analytics/analytics_package/pyproject.toml @@ -0,0 +1,21 @@ +[build-system] +requires = ["setuptools>=77"] +build-backend = "setuptools.build_meta" + +[project] +name = "analytics" +version = "5.0.4" +requires-python = ">=3.12" +dependencies = [ + "pandas>=3,<4", + "numpy", + "google-auth-oauthlib", + "google-api-python-client", + "requests", +] + +[tool.setuptools] +packages = ["analytics", "analytics.static_site"] + +[tool.setuptools.package-data] +"analytics.static_site" = ["template/*.html"] diff --git a/analytics/analytics_package/setup.py b/analytics/analytics_package/setup.py deleted file mode 100644 index 1262e6d72..000000000 --- a/analytics/analytics_package/setup.py +++ /dev/null @@ -1,15 +0,0 @@ -from setuptools import setup - -setup( - name="analytics", - version="5.0.4", - packages=["analytics", "analytics.static_site"], - package_data={"analytics.static_site": ["template/*.html"]}, - install_requires=[ - "pandas>=3,<4", - "numpy", - "google-auth-oauthlib", - "google-api-python-client", - "requests", - ], -) diff --git a/analytics/anvil-catalog/generate_static_site.py b/analytics/anvil-catalog/generate_static_site.py index 3a055fa5c..16786ea30 100644 --- a/analytics/anvil-catalog/generate_static_site.py +++ b/analytics/anvil-catalog/generate_static_site.py @@ -5,7 +5,14 @@ import analytics.api as ga from analytics.static_site import generate_site -from constants import CURRENT_MONTH, ANVIL_CATALOG_ID, SECRET_NAME, ANALYTICS_START, OAUTH_PORT, HISTORIC_UA_DATA_PATH +from constants import ( + ANALYTICS_START, + ANVIL_CATALOG_ID, + CURRENT_MONTH, + HISTORIC_UA_DATA_PATH, + OAUTH_PORT, + SECRET_NAME, +) os.environ.setdefault(SECRET_NAME, "../../.credentials/anvil_ga4_credentials.json") diff --git a/analytics/anvil-explorer/constants.py b/analytics/anvil-explorer/constants.py index 0a558f55b..dfa2338c9 100644 --- a/analytics/anvil-explorer/constants.py +++ b/analytics/anvil-explorer/constants.py @@ -6,7 +6,7 @@ # Dates of known synthetic/bot traffic (headless-browser burst, likely a load test) # to exclude from reports; see DataBiosphere/data-browser#4907. EXCLUDE_BOT_TRAFFIC_DATES = ["2025-02-10", "2025-02-11"] -SECRET_NAME = 'ANVIL_ANALYTICS_REPORTING_CLIENT_SECRET_PATH' +SECRET_NAME = "ANVIL_ANALYTICS_REPORTING_CLIENT_SECRET_PATH" ANALYTICS_START = "2024-01-01" OAUTH_PORT = 8082 diff --git a/analytics/anvil-explorer/generate_static_site.py b/analytics/anvil-explorer/generate_static_site.py index 47b3a731f..6faba7cca 100644 --- a/analytics/anvil-explorer/generate_static_site.py +++ b/analytics/anvil-explorer/generate_static_site.py @@ -4,8 +4,20 @@ import os import analytics.api as ga -from analytics.static_site import generate_site, fetch_entity_title_map, enrich_detail_records, make_event_charts -from constants import CURRENT_MONTH, ANVIL_EXPLORER_ID, SECRET_NAME, ANALYTICS_START, OAUTH_PORT, EXCLUDE_BOT_TRAFFIC_DATES +from analytics.static_site import ( + enrich_detail_records, + fetch_entity_title_map, + generate_site, + make_event_charts, +) +from constants import ( + ANALYTICS_START, + ANVIL_EXPLORER_ID, + CURRENT_MONTH, + EXCLUDE_BOT_TRAFFIC_DATES, + OAUTH_PORT, + SECRET_NAME, +) ANVIL_DATASETS_API_URL = "https://service.explore.anvilproject.org/index/datasets" @@ -19,7 +31,9 @@ def resolve_dataset_titles(data): - title_map = fetch_entity_title_map(ANVIL_DATASETS_API_URL, "datasets", "title", page_size=1000) + title_map = fetch_entity_title_map( + ANVIL_DATASETS_API_URL, "datasets", "title", page_size=1000 + ) count = enrich_detail_records(data, title_map, r"/datasets/([0-9a-f-]+)") print(f" Enriched {count} records with dataset titles") @@ -38,21 +52,47 @@ def resolve_dataset_titles(data): "summary_stats": [ { "label": "Cohort Export Requests", - "event_keys": ["index_bulk_download_requested", "index_file_manifest_requested", "index_analyze_in_terra_requested"], + "event_keys": [ + "index_bulk_download_requested", + "index_file_manifest_requested", + "index_analyze_in_terra_requested", + ], }, { "label": "Dataset Export Requests", - "event_keys": ["dataset_bulk_download_requested", "dataset_file_manifest_requested", "dataset_analyze_in_terra_requested"], + "event_keys": [ + "dataset_bulk_download_requested", + "dataset_file_manifest_requested", + "dataset_analyze_in_terra_requested", + ], }, ], "file_downloads_position": 3, "event_counts": [ - {"label": "Export to Terra\n(Single Dataset)", "event_key": "dataset_analyze_in_terra_requested"}, - {"label": "Export to Terra\n(Cross-Dataset)", "event_key": "index_analyze_in_terra_requested"}, - {"label": "curl Command\n(Single Dataset)", "event_key": "dataset_bulk_download_requested"}, - {"label": "curl Command\n(Cross-Dataset)", "event_key": "index_bulk_download_requested"}, - {"label": "File Manifest\n(Single Dataset)", "event_key": "dataset_file_manifest_requested"}, - {"label": "File Manifest\n(Cross-Dataset)", "event_key": "index_file_manifest_requested"}, + { + "label": "Export to Terra\n(Single Dataset)", + "event_key": "dataset_analyze_in_terra_requested", + }, + { + "label": "Export to Terra\n(Cross-Dataset)", + "event_key": "index_analyze_in_terra_requested", + }, + { + "label": "curl Command\n(Single Dataset)", + "event_key": "dataset_bulk_download_requested", + }, + { + "label": "curl Command\n(Cross-Dataset)", + "event_key": "index_bulk_download_requested", + }, + { + "label": "File Manifest\n(Single Dataset)", + "event_key": "dataset_file_manifest_requested", + }, + { + "label": "File Manifest\n(Cross-Dataset)", + "event_key": "index_file_manifest_requested", + }, ], }, property_id=ANVIL_EXPLORER_ID, diff --git a/analytics/hca-explorer/constants.py b/analytics/hca-explorer/constants.py index daabae2dd..18887f758 100644 --- a/analytics/hca-explorer/constants.py +++ b/analytics/hca-explorer/constants.py @@ -4,7 +4,15 @@ HCA_ID = "361323030" # Filter to exclude the Data Explorer -HCA_BROWSER_ONLY_FILTER = {"filter": {"fieldName": "hostName", "stringFilter": {"matchType": "EXACT", "value": "explore.data.humancellatlas.org"}}} +HCA_BROWSER_ONLY_FILTER = { + "filter": { + "fieldName": "hostName", + "stringFilter": { + "matchType": "EXACT", + "value": "explore.data.humancellatlas.org", + }, + } +} SECRET_NAME = "HCA_ANALYTICS_REPORTING_CLIENT_SECRET_PATH" ANALYTICS_START = "2024-02-01" diff --git a/analytics/hca-explorer/generate_static_site.py b/analytics/hca-explorer/generate_static_site.py index 1ec6dfe83..58dc76e42 100644 --- a/analytics/hca-explorer/generate_static_site.py +++ b/analytics/hca-explorer/generate_static_site.py @@ -4,8 +4,20 @@ import os import analytics.api as ga -from analytics.static_site import generate_site, fetch_entity_title_map, enrich_detail_records, make_event_charts -from constants import CURRENT_MONTH, HCA_ID, SECRET_NAME, ANALYTICS_START, OAUTH_PORT, HCA_BROWSER_ONLY_FILTER +from analytics.static_site import ( + enrich_detail_records, + fetch_entity_title_map, + generate_site, + make_event_charts, +) +from constants import ( + ANALYTICS_START, + CURRENT_MONTH, + HCA_BROWSER_ONLY_FILTER, + HCA_ID, + OAUTH_PORT, + SECRET_NAME, +) AZUL_PROJECTS_URL = "https://service.azul.data.humancellatlas.org/index/projects" @@ -38,21 +50,47 @@ def resolve_project_titles(data): "summary_stats": [ { "label": "Cohort Export Requests", - "event_keys": ["index_bulk_download_requested", "index_file_manifest_requested", "index_analyze_in_terra_requested"], + "event_keys": [ + "index_bulk_download_requested", + "index_file_manifest_requested", + "index_analyze_in_terra_requested", + ], }, { "label": "Project Export Requests", - "event_keys": ["dataset_bulk_download_requested", "dataset_file_manifest_requested", "dataset_analyze_in_terra_requested"], + "event_keys": [ + "dataset_bulk_download_requested", + "dataset_file_manifest_requested", + "dataset_analyze_in_terra_requested", + ], }, ], "file_downloads_position": 3, "event_counts": [ - {"label": "Export to Terra\n(Single Project)", "event_key": "dataset_analyze_in_terra_requested"}, - {"label": "Export to Terra\n(Cross-Project)", "event_key": "index_analyze_in_terra_requested"}, - {"label": "curl Command\n(Single Project)", "event_key": "dataset_bulk_download_requested"}, - {"label": "curl Command\n(Cross-Project)", "event_key": "index_bulk_download_requested"}, - {"label": "File Manifest\n(Single Project)", "event_key": "dataset_file_manifest_requested"}, - {"label": "File Manifest\n(Cross-Project)", "event_key": "index_file_manifest_requested"}, + { + "label": "Export to Terra\n(Single Project)", + "event_key": "dataset_analyze_in_terra_requested", + }, + { + "label": "Export to Terra\n(Cross-Project)", + "event_key": "index_analyze_in_terra_requested", + }, + { + "label": "curl Command\n(Single Project)", + "event_key": "dataset_bulk_download_requested", + }, + { + "label": "curl Command\n(Cross-Project)", + "event_key": "index_bulk_download_requested", + }, + { + "label": "File Manifest\n(Single Project)", + "event_key": "dataset_file_manifest_requested", + }, + { + "label": "File Manifest\n(Cross-Project)", + "event_key": "index_file_manifest_requested", + }, ], }, property_id=HCA_ID, diff --git a/analytics/lungmap/constants.py b/analytics/lungmap/constants.py index 1ff20d9d7..777c8c612 100644 --- a/analytics/lungmap/constants.py +++ b/analytics/lungmap/constants.py @@ -4,7 +4,7 @@ HISTORIC_UA_DATA_PATH = "users_over_time_history.json" LUNGMAP_ID = "362871218" -SECRET_NAME = 'ANALYTICS_REPORTING_CLIENT_SECRET_PATH' +SECRET_NAME = "ANALYTICS_REPORTING_CLIENT_SECRET_PATH" ANALYTICS_START = "2023-07-01" OAUTH_PORT = 8082 diff --git a/analytics/lungmap/generate_static_site.py b/analytics/lungmap/generate_static_site.py index 61a8f467a..5bac5c780 100644 --- a/analytics/lungmap/generate_static_site.py +++ b/analytics/lungmap/generate_static_site.py @@ -4,8 +4,20 @@ import os import analytics.api as ga -from analytics.static_site import generate_site, fetch_entity_title_map, enrich_detail_records, make_event_charts -from constants import CURRENT_MONTH, LUNGMAP_ID, SECRET_NAME, ANALYTICS_START, OAUTH_PORT, HISTORIC_UA_DATA_PATH +from analytics.static_site import ( + enrich_detail_records, + fetch_entity_title_map, + generate_site, + make_event_charts, +) +from constants import ( + ANALYTICS_START, + CURRENT_MONTH, + HISTORIC_UA_DATA_PATH, + LUNGMAP_ID, + OAUTH_PORT, + SECRET_NAME, +) AZUL_PROJECTS_URL = "https://service.azul.data.humancellatlas.org/index/projects" @@ -19,7 +31,9 @@ def resolve_project_titles(data): - title_map = fetch_entity_title_map(AZUL_PROJECTS_URL, "projects", "projectTitle", catalog="lm10") + title_map = fetch_entity_title_map( + AZUL_PROJECTS_URL, "projects", "projectTitle", catalog="lm10" + ) count = enrich_detail_records(data, title_map, r"/projects/([0-9a-f-]+)") print(f" Enriched {count} records with project titles") @@ -38,21 +52,47 @@ def resolve_project_titles(data): "summary_stats": [ { "label": "Cohort Export Requests", - "event_keys": ["index_bulk_download_requested", "index_file_manifest_requested", "index_analyze_in_terra_requested"], + "event_keys": [ + "index_bulk_download_requested", + "index_file_manifest_requested", + "index_analyze_in_terra_requested", + ], }, { "label": "Project Export Requests", - "event_keys": ["dataset_bulk_download_requested", "dataset_file_manifest_requested", "dataset_analyze_in_terra_requested"], + "event_keys": [ + "dataset_bulk_download_requested", + "dataset_file_manifest_requested", + "dataset_analyze_in_terra_requested", + ], }, ], "file_downloads_position": 3, "event_counts": [ - {"label": "Export to Terra\n(Single Project)", "event_key": "dataset_analyze_in_terra_requested"}, - {"label": "Export to Terra\n(Cross-Project)", "event_key": "index_analyze_in_terra_requested"}, - {"label": "curl Command\n(Single Project)", "event_key": "dataset_bulk_download_requested"}, - {"label": "curl Command\n(Cross-Project)", "event_key": "index_bulk_download_requested"}, - {"label": "File Manifest\n(Single Project)", "event_key": "dataset_file_manifest_requested"}, - {"label": "File Manifest\n(Cross-Project)", "event_key": "index_file_manifest_requested"}, + { + "label": "Export to Terra\n(Single Project)", + "event_key": "dataset_analyze_in_terra_requested", + }, + { + "label": "Export to Terra\n(Cross-Project)", + "event_key": "index_analyze_in_terra_requested", + }, + { + "label": "curl Command\n(Single Project)", + "event_key": "dataset_bulk_download_requested", + }, + { + "label": "curl Command\n(Cross-Project)", + "event_key": "index_bulk_download_requested", + }, + { + "label": "File Manifest\n(Single Project)", + "event_key": "dataset_file_manifest_requested", + }, + { + "label": "File Manifest\n(Cross-Project)", + "event_key": "index_file_manifest_requested", + }, ], }, property_id=LUNGMAP_ID, diff --git a/analytics/pyproject.toml b/analytics/pyproject.toml new file mode 100644 index 000000000..8b1ee0ccf --- /dev/null +++ b/analytics/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "analytics-reports" +version = "0.0.0" +description = "Environment for generating the static analytics site reports." +requires-python = ">=3.12" +dependencies = ["analytics"] + +[tool.uv] +package = false + +[tool.uv.workspace] +members = ["analytics_package"] + +[tool.uv.sources] +analytics = { workspace = true } + +[dependency-groups] +dev = ["ruff==0.16.3"] + +[tool.ruff.lint] +select = ["E4", "E7", "E9", "F", "I", "W", "B"] diff --git a/analytics/readme.md b/analytics/readme.md index b1ce27dd5..b80ed06c7 100644 --- a/analytics/readme.md +++ b/analytics/readme.md @@ -1,14 +1,27 @@ ## Installing the environment -- Use Python 3.12.4. -- Run `python -m venv ./venv` to create a new environment under `./venv`. -- Run `source ./venv/bin/activate` to activate the environment. -- Run `pip install -r ./requirements.txt` to install requirements. +Dependencies are managed with [uv](https://docs.astral.sh/uv/), and pinned in `uv.lock`. -## Deactivating/reactivating +- Install uv, e.g. with `brew install uv` or `curl -LsSf https://astral.sh/uv/install.sh | sh`. +- From this folder, run `uv sync`. This creates `./.venv`, provisions the Python version given in + `.python-version`, and installs the `analytics_package` in editable mode along with its dependencies. -- To deactivate the environment, run `deactivate`. -- To activate the environment again, run `source ./venv/bin/activate`. +There's no need to activate the environment: prefixing a command with `uv run` runs it in the +environment, re-syncing first if the lockfile has changed. If you'd rather activate it anyway, run +`source ../.venv/bin/activate` from an app subfolder (or `source ./.venv/bin/activate` from here), and +`deactivate` to exit. + +To change the package's dependencies, edit `analytics_package/pyproject.toml` and run `uv lock` to +update `uv.lock`. + +## Linting and formatting + +Linting and formatting are handled by [Ruff](https://docs.astral.sh/ruff/), configured in +`pyproject.toml` and enforced by the `analytics` job in `run-checks.yml`. NPM scripts can be used +as shortcuts to run Ruff: + +- `npm run lint:python` to lint, or `npm run lint:python -- --fix` to apply the automatic fixes. +- `npm run format:python` to format, or `npm run check-format:python` to check without writing. ## Generating reports for the static site @@ -21,7 +34,7 @@ Each app-specific analytics subfolder (e.g. `anvil-explorer`, `hca-explorer`, `l - `.credentials/hca_ga4_credentials.json`, for HCA Data Explorer and LungMAP. - Alternatively: A path set in the environment variable specified by the `SECRET_NAME` variable in the folder's `constants.py`. - Update the `CURRENT_MONTH` variable in the folder's `constants.py` to the month you wish to generate the report for. -- From within the folder, run `python generate_static_site.py`. +- From within the folder, run `uv run python generate_static_site.py`. A browser window will open for Google OAuth. After authenticating, the script fetches data from GA4 and writes the site to the configured folder. With the exception of `anvil-catalog` (which is a retired app and writes to `anvil-catalog/site`), this will be a subfolder of `gh-pages` under the repository root. diff --git a/analytics/requirements.txt b/analytics/requirements.txt deleted file mode 100644 index 01f61dc35..000000000 --- a/analytics/requirements.txt +++ /dev/null @@ -1,28 +0,0 @@ --e ./analytics_package -certifi==2026.7.22 -cffi==2.1.1 -charset-normalizer==3.5.0 -cryptography==50.0.0 -google-api-core==2.34.0 -google-api-python-client==2.198.0 -google-auth==2.56.3 -google-auth-httplib2==0.4.1 -google-auth-oauthlib==1.4.0 -googleapis-common-protos==1.75.1 -httplib2==0.32.0 -idna==3.18 -numpy==2.5.2 -oauthlib==3.3.1 -pandas==3.0.5 -proto-plus==1.28.3 -protobuf==7.35.1 -pyasn1==0.6.4 -pyasn1_modules==0.4.2 -pycparser==3.0 -pyparsing==3.3.2 -python-dateutil==2.9.0.post0 -requests==2.34.2 -requests-oauthlib==2.0.0 -six==1.17.0 -uritemplate==4.2.0 -urllib3==2.7.0 diff --git a/analytics/uv.lock b/analytics/uv.lock new file mode 100644 index 000000000..d1b44f084 --- /dev/null +++ b/analytics/uv.lock @@ -0,0 +1,731 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform == 'emscripten'", + "python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[manifest] +members = [ + "analytics", + "analytics-reports", +] + +[[package]] +name = "analytics" +version = "5.0.4" +source = { editable = "analytics_package" } +dependencies = [ + { name = "google-api-python-client" }, + { name = "google-auth-oauthlib" }, + { name = "numpy" }, + { name = "pandas" }, + { name = "requests" }, +] + +[package.metadata] +requires-dist = [ + { name = "google-api-python-client" }, + { name = "google-auth-oauthlib" }, + { name = "numpy" }, + { name = "pandas", specifier = ">=3,<4" }, + { name = "requests" }, +] + +[[package]] +name = "analytics-reports" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "analytics" }, +] + +[package.dev-dependencies] +dev = [ + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [{ name = "analytics", editable = "analytics_package" }] + +[package.metadata.requires-dev] +dev = [{ name = "ruff", specifier = "==0.16.3" }] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" }, + { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" }, + { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" }, + { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" }, + { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" }, + { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" }, + { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" }, + { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" }, + { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" }, + { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" }, + { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" }, + { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, + { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, + { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, + { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" }, + { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" }, + { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" }, + { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" }, + { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" }, + { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, +] + +[[package]] +name = "cryptography" +version = "50.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" }, + { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" }, + { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" }, + { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" }, + { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" }, + { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" }, + { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" }, + { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" }, + { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" }, + { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" }, + { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" }, + { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" }, + { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" }, + { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" }, + { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" }, + { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" }, + { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" }, + { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" }, + { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" }, + { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" }, + { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" }, + { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" }, + { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" }, + { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" }, + { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" }, + { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" }, + { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" }, +] + +[[package]] +name = "google-api-core" +version = "2.34.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/7c/9be3903e3d45415e8ca493c75f8990a0f6f579d168015d44c379350d0ab0/google_api_core-2.34.0.tar.gz", hash = "sha256:98a779fe72de956eb1c9c2f47ff4c4432a668ece1a002ec38bed07ec2698ae59", size = 187953, upload-time = "2026-08-06T06:23:58.128Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/c1/a8a92ae1bc4b1a8f804c776d7d3f0c771b78a62c3ad4df1be41b3fd8c767/google_api_core-2.34.0-py3-none-any.whl", hash = "sha256:cdf9c67e7ca2402d86ccbfde5f2503fc83e3cc3f58cc78456ae96cad24a6d2de", size = 180545, upload-time = "2026-08-06T06:22:47.502Z" }, +] + +[[package]] +name = "google-api-python-client" +version = "2.198.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-auth-httplib2" }, + { name = "httplib2" }, + { name = "uritemplate" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6b/53/0cd38e3a29d72ce45e27feba2ce1cd8049d69af9c48cb14fb164f1be9133/google_api_python_client-2.198.0.tar.gz", hash = "sha256:dfe3e16fb241af6e9c460a33f65085b3450e05cea09364f6b5d8997fb7e43e2a", size = 15060142, upload-time = "2026-06-25T14:32:42.953Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/92/0fc9e7a09eb240c31b879bd8d2e43f81ed1f86c4798b79ead4a083921ab3/google_api_python_client-2.198.0-py3-none-any.whl", hash = "sha256:fabac935474e817da5e662ff61bf7139439d6f92b32d332a7318a2d45931e03e", size = 15644203, upload-time = "2026-06-25T14:32:39.963Z" }, +] + +[[package]] +name = "google-auth" +version = "2.56.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/db/4c/fa42116a48bab3f7a143cf5042ecff7df9c8b73f8a376203cd534d1dc966/google_auth-2.56.3.tar.gz", hash = "sha256:40e229fc901f0a305b553050e5fce562d509bee0435be053abfa91582b51b90c", size = 367110, upload-time = "2026-08-06T06:24:01.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/b3/6117b2f24065cd7e2c4f140e9a193e215f089ca8ba314cf91eb9d0b7fe0a/google_auth-2.56.3-py3-none-any.whl", hash = "sha256:8ec438808f813ad034535000261eed1067475d229d05bbf4216e78c3f2362e53", size = 259116, upload-time = "2026-08-06T06:22:51.788Z" }, +] + +[[package]] +name = "google-auth-httplib2" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "httplib2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/46/79983cb738f0eb14e6ab4f43457aa9652f8d46bc4376b178f676b68c5c37/google_auth_httplib2-0.4.1.tar.gz", hash = "sha256:125b1bb4fcfdd2d97f19b673c1f46f831603d0acaffe415c8a35dadb312552a1", size = 11158, upload-time = "2026-08-06T06:24:00.452Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/8a/96410d0c3e02b584d1e7f5d4000b2e1710855650b226cd0601f2b154940a/google_auth_httplib2-0.4.1-py3-none-any.whl", hash = "sha256:67088a8387dc3f66016a690634f1cb9b7a48a31406f8998e088efc64203e7a12", size = 9529, upload-time = "2026-08-06T06:22:50.53Z" }, +] + +[[package]] +name = "google-auth-oauthlib" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "requests-oauthlib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/18/90c7fac516e63cf2058166fce0c88c353647c677b51cc036c09c49bb5cbb/google_auth_oauthlib-1.4.0.tar.gz", hash = "sha256:18b5e28880eb8eba9065c436becdc0ee8e4b59117a73a510679c82f70cd363d2", size = 21675, upload-time = "2026-05-07T08:03:47.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/d3/d7dff0d58a9e9244b48044bfb6a898bfcc8ecc42e0031d1bebc695344725/google_auth_oauthlib-1.4.0-py3-none-any.whl", hash = "sha256:251314f213a9ee46a5ae73988e84fd7cca8bb68e7ecf4bfd45940f9e7f51d070", size = 19261, upload-time = "2026-05-07T08:02:13.798Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.75.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/73/74bcab964c9a7a61f2bb71e8179b0f13e6fa98f7ce00fd168aab291e4a2e/googleapis_common_protos-1.75.1.tar.gz", hash = "sha256:d3042c6c5a2d4e67113104d6b6818b59b6bd92a197f2a91508e801fe815cf071", size = 150967, upload-time = "2026-08-06T06:24:51.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/51/186c02b8549b69ccda44429cf6ff5081e4b61a602ddfe6a8020d1be31d1b/googleapis_common_protos-1.75.1-py3-none-any.whl", hash = "sha256:28a1934bcd33b9c9da66ac301a0a4227e3367f095a17d0375cb98f0a09d93b79", size = 300626, upload-time = "2026-08-06T06:23:46.696Z" }, +] + +[[package]] +name = "httplib2" +version = "0.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/f5/ccf58de92d61e3ad921119668f54ed36ca1d0cf5dcc5c1657dfb164fd78b/httplib2-0.32.0.tar.gz", hash = "sha256:48a0ef30a42db65d8f3399045e1d09ab0ba66e3b9efc360d07f80ea55d286025", size = 254283, upload-time = "2026-06-26T10:13:56.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/a0/550eec327e5f5c7b732531c489f5307efec41f047b0d703bd4ca1e5ad2db/httplib2-0.32.0-py3-none-any.whl", hash = "sha256:dc6705cacdf3fb0a2aba7629fa33c90fd93e30035db0c157325826be177e4816", size = 93148, upload-time = "2026-06-26T10:13:54.985Z" }, +] + +[[package]] +name = "idna" +version = "3.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860", size = 20773161, upload-time = "2026-08-09T13:48:27.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/72/dccb0aaf40972777283303919f613964227266d0c13adebb79ac124f1c3e/numpy-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14e373cfc6387177e8409dac3c7159be8eb05cd77096cd7c950268b86f62831c", size = 16891693, upload-time = "2026-08-09T13:44:51.702Z" }, + { url = "https://files.pythonhosted.org/packages/60/2e/b5aee50a1f74ac815cf8331812cb8251e29024025de462e0c047641c614c/numpy-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbd96c833ecc8cc069ce518078fc8c60cb9cbfb0fea5b7a803ad65035596d03", size = 11903109, upload-time = "2026-08-09T13:44:55.501Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f4/29e78102a80601cf034d4e9767022cffeca2c3b4c926e1754572ca95593d/numpy-2.5.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6e8172ddfcf5cf74b811d372b570b83c60bd2de87a6fbfbebdadb4a9bd9c6cbb", size = 5350202, upload-time = "2026-08-09T13:44:58.401Z" }, + { url = "https://files.pythonhosted.org/packages/11/4b/dcd3b7eadaf4035d2c7a4289d232523a6964f602598ef7674e4bd7291f93/numpy-2.5.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f188481f1669e26f62b701e8205d19e460fa4a9b52a1414ba382330e4a3414", size = 6687736, upload-time = "2026-08-09T13:45:00.813Z" }, + { url = "https://files.pythonhosted.org/packages/e5/21/4947e0e9d6c9fc2e2ff15b8949049ee44f63adb9cacc729ab8793f97e712/numpy-2.5.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee9c4eeb8454b3660a8b53493563c3e121c2fc94fbd72b848ef814ed7b676a9", size = 15612696, upload-time = "2026-08-09T13:45:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5f/62d28cf019460c7f1394105b4d49d9911a9c444cb77ab0bd95a204c5a6de/numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdec01fa790a186d430433fdd4d4ffb70eed6f0eeb4bf05c8dbe2dce0a9bcb8", size = 16722264, upload-time = "2026-08-09T13:45:07.714Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/3f0be4c1b9fdf5dd5e708a6806978564d7c46a055c000496309ff2a2f8af/numpy-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7999d4ddb0c4025018373fd787510d46e04c769467af22869707b3c1cfd459ab", size = 16974396, upload-time = "2026-08-09T13:45:11.316Z" }, + { url = "https://files.pythonhosted.org/packages/22/72/6262cbdeeb45da9d971e40715f579d791603ba8ec0b5e2db1ac55454421d/numpy-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1f017dc0875c9209d219f97feceb7d54c2661bb243deb4114478e1295808af7", size = 18476044, upload-time = "2026-08-09T13:45:14.869Z" }, + { url = "https://files.pythonhosted.org/packages/36/33/29208b8b075bde62d26a81d14b358c42b0f69b6cabd98d4ff97f37f22b05/numpy-2.5.2-cp312-cp312-win32.whl", hash = "sha256:d6a48072864e3324e194a8fbb3c657bcc5b5c869dbc64c9537b1d5c862572c0a", size = 6072817, upload-time = "2026-08-09T13:45:17.867Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/87fea2769fe1c47c1b5b01d8310772c9d1a85d485de7cf386ef7a3332b02/numpy-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:28ac63476ec7651484215ee7fa15a1f78b57c14621f01e392afe17b9a1390ce4", size = 12464674, upload-time = "2026-08-09T13:45:20.734Z" }, + { url = "https://files.pythonhosted.org/packages/14/52/032b97e00461ab0809bbe4c588b035620e5a14b8cdee47ecddefc7b17d33/numpy-2.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:27650bb0e7140fa3d37b9923b4803645e0b125d190f326eecfd3f4dad8e8ade1", size = 10397131, upload-time = "2026-08-09T13:45:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/6b24738a0ef4557d189b150046cd07823c50e4273e8aebd651222e24306f/numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15", size = 16886595, upload-time = "2026-08-09T13:45:27.323Z" }, + { url = "https://files.pythonhosted.org/packages/65/60/f2d208d366f263f39c6e69ed309290717aab41078b6d04c9be2a84fa2a07/numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080", size = 11896845, upload-time = "2026-08-09T13:45:31.638Z" }, + { url = "https://files.pythonhosted.org/packages/3c/79/81e0bf24f4d020a2b1d5cd297a9f60c3f24eeb116f9bba5870443f7b6a4a/numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740", size = 5343880, upload-time = "2026-08-09T13:45:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cc/e3141cf06d1a8a2c7e107543fe1269c1d1af760d4d683c0794a4ee1127c2/numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56", size = 6682264, upload-time = "2026-08-09T13:45:36.7Z" }, + { url = "https://files.pythonhosted.org/packages/29/f1/2a64a307d92c5d98f5255a4014eb43bb6103ee477087b61ecae44a3aa9b9/numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3", size = 15609566, upload-time = "2026-08-09T13:45:39.518Z" }, + { url = "https://files.pythonhosted.org/packages/7b/44/59a1eb68e773c4098d107ef34a0dbdeca501d72ffcfbff9a7707343921ce/numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee", size = 16709995, upload-time = "2026-08-09T13:45:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4c/3e54d4ddbc359a1295f8b633e8106bcd4d7d4a206e82df051bdfb3058755/numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59", size = 16972511, upload-time = "2026-08-09T13:45:47.094Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9f/02e371638ebf19b66d46231e4be52999e87f32d1961b113bc45656608b22/numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d", size = 18465609, upload-time = "2026-08-09T13:45:50.808Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/ad6645abc7a3510fe48e8ea1ab4598166f500057ef4ebf38bfad4f1577de/numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4", size = 6070204, upload-time = "2026-08-09T13:45:54.111Z" }, + { url = "https://files.pythonhosted.org/packages/15/20/f3489f86d81ea460b2bcdceaed094142ca6579f6be0ec527b781d39afe68/numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657", size = 12460532, upload-time = "2026-08-09T13:45:57.167Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/35b31dde1b283b79de828b80f876afd8c94e28fe1e9c375f89e261cc4c0d/numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2", size = 10396725, upload-time = "2026-08-09T13:46:00.478Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f8/c3b222bf075b50afd8e949a07a15c4b312a4a84bd8102a332bcd953cbbb4/numpy-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1", size = 16885180, upload-time = "2026-08-09T13:46:03.939Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/2c1d4b1987795a92b5bbf7c24fe249ab96aa2573ab0d7604802c189d7b86/numpy-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8", size = 11907878, upload-time = "2026-08-09T13:46:07.045Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ee/d08226fc858044355983a6e5b94f08ff6f3969e0a2b160a4a89f0ddb3445/numpy-2.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323", size = 5354922, upload-time = "2026-08-09T13:46:10.04Z" }, + { url = "https://files.pythonhosted.org/packages/94/f0/6d3d933056440ebbc5e6bad92065fc6c26a48a84a36b1208580e94eea76c/numpy-2.5.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e", size = 6679168, upload-time = "2026-08-09T13:46:12.275Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3b/ecd49dd90033cceb2704d88ca905d4d7d89b0e8c739608754ffd325fa820/numpy-2.5.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65", size = 15624501, upload-time = "2026-08-09T13:46:15.322Z" }, + { url = "https://files.pythonhosted.org/packages/c7/99/461bd36dbdfac6c1c53efa370bd55a83227542d0d118f1677dbf1a3dacd5/numpy-2.5.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee", size = 16713701, upload-time = "2026-08-09T13:46:18.949Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9c/2b251df9e8a5d647b62b0cbc1b90a91850c1cf4859ecb532fd0b4eacff6c/numpy-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68", size = 16986065, upload-time = "2026-08-09T13:46:23.006Z" }, + { url = "https://files.pythonhosted.org/packages/8f/25/20de43f53ff1390534a124475055a19f01fe10c920a0fd11b8e18d6d6052/numpy-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb", size = 18470031, upload-time = "2026-08-09T13:46:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/56/5e/0c577ca308d6da5eb79b546ba10bbe5b60148192194e2da060913b1de4f1/numpy-2.5.2-cp314-cp314-win32.whl", hash = "sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98", size = 6121028, upload-time = "2026-08-09T13:46:30.046Z" }, + { url = "https://files.pythonhosted.org/packages/15/5c/7bcbd5b11f94199073320410cddcbb80cee62415bfeb540874b265c2d922/numpy-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d", size = 12597627, upload-time = "2026-08-09T13:46:32.886Z" }, + { url = "https://files.pythonhosted.org/packages/87/bc/4d0b06fba0da90ccc75af62823cb9dcedb6c9ea0cffa058cb2c9ee773a77/numpy-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf", size = 10680414, upload-time = "2026-08-09T13:46:36.036Z" }, + { url = "https://files.pythonhosted.org/packages/cd/17/f429aac9dc08833a0d0f188eba38c532a751b1a1f2ca6018a37b455cb321/numpy-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1", size = 12026967, upload-time = "2026-08-09T13:46:39.084Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9f/d0849de96a2a4ceaa16662f18ee13eaa9c0aa418269fdc8c4857c56b11da/numpy-2.5.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f", size = 5473874, upload-time = "2026-08-09T13:46:42.075Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/8df216d4a4a5422a3de045301cf7df8ea47286d76f5cb7160b0128ac26b7/numpy-2.5.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf", size = 6789276, upload-time = "2026-08-09T13:46:44.387Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3a/20d7e9891c4ddfadd6ff8d95bf4b29f353d8e1770553de2099880551dfb9/numpy-2.5.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4", size = 15659154, upload-time = "2026-08-09T13:46:47.538Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d6/f3aa3d2688bf501b858835c6bd087ae9b51a56ae6fca8e2b0990abd177af/numpy-2.5.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce", size = 16748909, upload-time = "2026-08-09T13:46:51.442Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8f/1c5cae8d2baf86ab802ae97a00be55bc7e21ebc11b12bbc33376c5f05342/numpy-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26", size = 17027685, upload-time = "2026-08-09T13:46:55.095Z" }, + { url = "https://files.pythonhosted.org/packages/5c/27/71d3467404aedc1c24ce79610f91b52b0b0f466c43a701aa56fc75c145ab/numpy-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac", size = 18501181, upload-time = "2026-08-09T13:46:59.09Z" }, + { url = "https://files.pythonhosted.org/packages/14/2f/42921d27c40aea7e077f4a423ae509fd9220b028cd787bafefd8ab2b3a5f/numpy-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba", size = 6271085, upload-time = "2026-08-09T13:47:01.903Z" }, + { url = "https://files.pythonhosted.org/packages/75/e6/bad5f5d56de9b1971bac959963dda276d35c40f1854475005434bbe08692/numpy-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884", size = 12787971, upload-time = "2026-08-09T13:47:04.963Z" }, + { url = "https://files.pythonhosted.org/packages/df/05/f608795cb34391acd67e38d94a3c36abd8d8576293a3a80727d7595c372c/numpy-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e", size = 10750306, upload-time = "2026-08-09T13:47:07.976Z" }, + { url = "https://files.pythonhosted.org/packages/33/c6/28de0191c5f82b7d42a0a51390ba98587048aa93a39fafb05bdbe6e8d00c/numpy-2.5.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:078f9b027b478c9379b9677babbf0f8b8f1ecfada27636d7b9a93990c638739f", size = 16885274, upload-time = "2026-08-09T13:47:11.439Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/973ca116000d244897e468ea1aff30b589e5022e3c8744b71706fe33bd57/numpy-2.5.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:50a68f4bacd8a2b33d8da3d2269d0d78500f86ea582e4786dc10f5ef2c2c6842", size = 11907846, upload-time = "2026-08-09T13:47:15.128Z" }, + { url = "https://files.pythonhosted.org/packages/78/d9/8c4b3937ef204cb2fd88d389ccd0f265a2ffb11f35a01d2064cf46714bd6/numpy-2.5.2-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:e79aba74ffaf5f78a050d777c184cddf8fdffabab38acf5f3ef1fecbc17895d6", size = 5354892, upload-time = "2026-08-09T13:47:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/b6ee65ea2999fdb7023935e108e6fb776ee4082aa15f159acfa857e578c8/numpy-2.5.2-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:9a0731745a72a184490a582fb4af2533512bd071ace67785b5fdffc0ae58dce8", size = 6679309, upload-time = "2026-08-09T13:47:20.456Z" }, + { url = "https://files.pythonhosted.org/packages/43/f3/acb18d8b137a393c8e7803a8c994c9e64bde3930692a69d826993113a159/numpy-2.5.2-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec954036759bcee3aa484f8603bd9c14f3e776293b85578b8734c2d72777c69", size = 15625850, upload-time = "2026-08-09T13:47:24.365Z" }, + { url = "https://files.pythonhosted.org/packages/a9/bf/a8e9bb0db815a0e265b5744ebedd3af0bd5faad8604e5b50a1cd012f3c91/numpy-2.5.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc649493697006bc90614a5f0bbc8cb3cb1866715c474e473694968d7e6b99ab", size = 16713664, upload-time = "2026-08-09T13:47:27.965Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/6e913736b3dd6582344af32418b5fb9dab34282e8a8174ae1d54ceb0fc13/numpy-2.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cf7de32f486e4ac9e2d93b810f9e9ac72a728dd46a32a0bb403222f27f653514", size = 16986749, upload-time = "2026-08-09T13:47:31.541Z" }, + { url = "https://files.pythonhosted.org/packages/80/09/7d3b23eff5c7428ef6c01e6f7052bb60d504c4d33e317b36b8959c24ad97/numpy-2.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2ffa7bacab3e2ee1b19ed31766bb60bb380b68c23f051e199c5cc598afd68710", size = 18470495, upload-time = "2026-08-09T13:47:35.364Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a4/68a321d825374f6eb677ffe8ef8c6b9a328304e6fd2e39d9530822776607/numpy-2.5.2-cp315-cp315-win32.whl", hash = "sha256:6b588cc8f902d6bff201c19fd00c43ab8545671e3554d014e12e14139e5e8617", size = 6120696, upload-time = "2026-08-09T13:47:38.561Z" }, + { url = "https://files.pythonhosted.org/packages/c8/23/deafbb1700f79fae9cd1e91220f133d124cc267de1b584da3fbf6db2f6cd/numpy-2.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:07d4e89f3a9ab0a9ba24264ccdb642b3dd951b2281e8883a5481a4aa79cc31a7", size = 12597324, upload-time = "2026-08-09T13:47:41.401Z" }, + { url = "https://files.pythonhosted.org/packages/33/cd/3272ba105e3bbbdaeb11357eda31e7a6825ffe159e8171665660299a948f/numpy-2.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:a610dc7e3c52edd39c2bc2375ff9c3fd59cb3ad00e4472d36f83bc1457145788", size = 10680466, upload-time = "2026-08-09T13:47:44.873Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/58370637b1bb70a5c9ce2b43f4b521ccb224e36ccb76a6596b17ae4b447c/numpy-2.5.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:40f4d451aed46a8046a1aae41c4e55fb3612273df9c502480135e1501576a34b", size = 16993947, upload-time = "2026-08-09T13:47:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/93/2abcb807712b289d6d60fe4cf30532f98974a8396d885650f3ba5a13026e/numpy-2.5.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c081cbe16ba1ab53078e5ff29013621e33c509eedab055775d956427712c236e", size = 12025331, upload-time = "2026-08-09T13:47:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3a/2898e003a5fbaf87e76c039b4ee1f5eb390471b4ffe74887c1f34c4e791e/numpy-2.5.2-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:0090ccdd57ec2703e9b49d0bf554767370581c1dd0a6b2bb2b2d9def317d042a", size = 5472336, upload-time = "2026-08-09T13:47:55.403Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/23f69d07c544597b29758b31b55c27dc9d541012a2c1496189fef702aec2/numpy-2.5.2-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:6a9bb119fb8dd21ba30b3f0e555b7e2b081bd9883af21ec9c1c633d161cda3a8", size = 6788387, upload-time = "2026-08-09T13:47:58.192Z" }, + { url = "https://files.pythonhosted.org/packages/15/ea/c0dbdbcf22f43782510a3e492dd3da73c6112b69cac8929d16d127536fc4/numpy-2.5.2-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a839318485284a6fb31be4f8f2c91c8f2cb22f4543c4a8903f12b0671ffe07cc", size = 15667096, upload-time = "2026-08-09T13:48:01.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5e/29c73c31748cdb0f7566642125ba17fd5b56780cddf891b085dab27e4466/numpy-2.5.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba0a474801b8dc67b66bf465548abc90e82b44d2611b5770f33008dcabffe8ec", size = 16751730, upload-time = "2026-08-09T13:48:05.706Z" }, + { url = "https://files.pythonhosted.org/packages/47/95/02501e8454796bb58dadf7a99d3181e0b464bf264e1003039572f9779fac/numpy-2.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0a4035ae1129ff8777f08bfbd44f1e5d8e9c049ce0c2dd78fc0d92c13e7251c0", size = 17038686, upload-time = "2026-08-09T13:48:09.627Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b5/53a681d91b5c82687067d8ea5035e02d917b5509d6f334cb06484a954714/numpy-2.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:77843ca236b777e67f8d6b3660ea116e499612703a0ecd7093f316201eb9d8e2", size = 18507727, upload-time = "2026-08-09T13:48:13.744Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/6e11443f7b64ee376c860506091103bf68f92d2cab9e8d96d4501babf07c/numpy-2.5.2-cp315-cp315t-win32.whl", hash = "sha256:7354826bc6f8f69402e9b7fe28d15fcd34feebd74f856f111585c5b0c9fb0251", size = 6269775, upload-time = "2026-08-09T13:48:17.543Z" }, + { url = "https://files.pythonhosted.org/packages/f1/18/195d6b86cd72dbbc501edfa778005fa6b87afd34c153e46028cd3a0938f4/numpy-2.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:e5651f3f87add730ee6608d915009e19c911fba0cb000c7e3ea994b7d768eb12", size = 12782559, upload-time = "2026-08-09T13:48:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/458c344f0f0c178f4481dad5cca790626ffe4c34eabf9467069d06ee4999/numpy-2.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:5f8e00be2ec6f45f4e8a41a527f68d44a7d96fee92a650e4d8b1326f77f61e6e", size = 10748103, upload-time = "2026-08-09T13:48:24.21Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/54/1dc810ea558d1320b597aa140a514f2fdf1d2ea09c38cf556f13ea712ec9/pandas-3.0.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa290c16964d4963fbfbc358928239cf3bd755b20e988ce944877def2f44471d", size = 10411717, upload-time = "2026-07-22T22:18:08.307Z" }, + { url = "https://files.pythonhosted.org/packages/68/56/fbe81c09195924d8b7b8d4461a20458fe80a6a5ed6b24f0314da684277e1/pandas-3.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2e26bb46934b8a2ca0c3de1d3d606fc5f6746584791b2db264d58cf370e08dc", size = 9957095, upload-time = "2026-07-22T22:18:10.6Z" }, + { url = "https://files.pythonhosted.org/packages/e0/51/fac252f4a913ed5eabf3c11b880a9e8d5a6c10f0b2129d0462212d238b4d/pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73fa87b08a7ef706f8aafda39ddaccf2a99047bea62d8c88a0361bcafb2237bc", size = 10485458, upload-time = "2026-07-22T22:18:12.834Z" }, + { url = "https://files.pythonhosted.org/packages/12/98/e976540c1addf70442be7842a18cf70884a964abbf69442504f4d2939989/pandas-3.0.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d373ce03ffd84010ed9839fa73672a9c8256990532e158440c0085db7d914b34", size = 10998091, upload-time = "2026-07-22T22:18:15.209Z" }, + { url = "https://files.pythonhosted.org/packages/a4/8c/1f29b5be8d3fc47dd7567eb167fabba2085879b31e0287ce7cba6d3d2ff4/pandas-3.0.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a29c53d85ea98c5e792c59ef82ee9fbe6ca902c0d0adb6b23f45ef894cd7bf6", size = 11499501, upload-time = "2026-07-22T22:18:17.689Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e2/bd9c98ad2df7b38bde002adde4cdf353519da51881634323b126c55997f9/pandas-3.0.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ad3b02ed6bc7d7ae9b70804b2c6aa31827489d150f8e623ce82491b82085d7", size = 12060559, upload-time = "2026-07-22T22:18:20.147Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9a/ffbd852d58bd74a617fe2f8ee6a58a96982271ce41cf981eab22190b4a4b/pandas-3.0.5-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:b2acb4650527eec6822c3dadb2b771277b65e7dae7a267d4bccf65fd1bb3fbce", size = 7197652, upload-time = "2026-07-22T22:18:22.502Z" }, + { url = "https://files.pythonhosted.org/packages/70/b5/d2d3e9ae73362ba4229651b0ee1455cf78073a1ce585f6ff693782ce263e/pandas-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:80a611068e8a3ac23f7398c6c14eb46dc974e5cc9997f653e2dcfd1da74edd41", size = 9831691, upload-time = "2026-07-22T22:18:24.534Z" }, + { url = "https://files.pythonhosted.org/packages/52/51/dea1e89d6a6796b9c43f85a09b484ee03edb8a4c4842e73e200a8c11301c/pandas-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:25ff585b972a18ef1fe9ffa3ac6544d9950508aa76832e5147640b6022821e49", size = 9105796, upload-time = "2026-07-22T22:18:27.064Z" }, + { url = "https://files.pythonhosted.org/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b", size = 10385742, upload-time = "2026-07-22T22:18:29.346Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3", size = 9932067, upload-time = "2026-07-22T22:18:31.833Z" }, + { url = "https://files.pythonhosted.org/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b", size = 10466756, upload-time = "2026-07-22T22:18:34.195Z" }, + { url = "https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be", size = 10938525, upload-time = "2026-07-22T22:18:36.81Z" }, + { url = "https://files.pythonhosted.org/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58", size = 11489303, upload-time = "2026-07-22T22:18:39.287Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" }, + { url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" }, + { url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" }, + { url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" }, + { url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" }, + { url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" }, + { url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" }, + { url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" }, + { url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" }, + { url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" }, + { url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" }, + { url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" }, +] + +[[package]] +name = "proto-plus" +version = "1.28.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/26/6a/056256feb4bd000869aba5c16cf2aa911572ca2a2feb185f86e457b5171e/proto_plus-1.28.3.tar.gz", hash = "sha256:5f91b30dafa6bb38d432c5557a6ee1d35ffd40b4b1e0e3ca27260448560b91d9", size = 58051, upload-time = "2026-08-06T06:24:55.581Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/3a/cfee3c50294f55a2f0f9575052dec2c2a48891ad4b1c2a133b05a87026cd/proto_plus-1.28.3-py3-none-any.whl", hash = "sha256:dc76880b8ee951cca002098574376cf71e055f9f16d9ba6570fb8a06f726d281", size = 50795, upload-time = "2026-08-06T06:23:50.653Z" }, +] + +[[package]] +name = "protobuf" +version = "7.35.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/01/9ef0afd7999eb9badb3a768b4aedd78c86d4c65cfaf1958ab276199e76b4/protobuf-7.35.1.tar.gz", hash = "sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a", size = 458717, upload-time = "2026-06-11T21:55:40.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/03/8aeeb7458d22546bf64b5250ca1daeb5ff757d900e8e4a7476c6f0db843e/protobuf-7.35.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6", size = 433226, upload-time = "2026-06-11T21:55:31.719Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/dfb89eb0e652a1ff073c39a59fb5e3a83cfe9b57a2c83fa6d78270101767/protobuf-7.35.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799", size = 328847, upload-time = "2026-06-11T21:55:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/0f/58/dc12f2cd484951524af6e3382c785869b9b3fb5e52ee95ae23add53ee8f9/protobuf-7.35.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4", size = 344030, upload-time = "2026-06-11T21:55:34.941Z" }, + { url = "https://files.pythonhosted.org/packages/e4/be/5b3cfe508bfab6761414ff944e3366eb13be4fd71efcd69450f89ba39f43/protobuf-7.35.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4", size = 327130, upload-time = "2026-06-11T21:55:35.921Z" }, + { url = "https://files.pythonhosted.org/packages/d8/bc/6d6c7ba8709c85f8f2c390b2b118d6fb08a783676a572271851bf45a7d22/protobuf-7.35.1-cp310-abi3-win32.whl", hash = "sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30", size = 428945, upload-time = "2026-06-11T21:55:37.034Z" }, + { url = "https://files.pythonhosted.org/packages/0a/19/8d0cb6f20a1ef7b18f1c8986ad5783f22f84cce39c6ce9a6e645ea55192e/protobuf-7.35.1-cp310-abi3-win_amd64.whl", hash = "sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87", size = 439996, upload-time = "2026-06-11T21:55:38.123Z" }, + { url = "https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl", hash = "sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9", size = 171659, upload-time = "2026-06-11T21:55:39.155Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + +[[package]] +name = "requests-oauthlib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "oauthlib" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650, upload-time = "2024-03-22T20:32:29.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179, upload-time = "2024-03-22T20:32:28.055Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/61/b3/3213589383f8f1b3938781bd1278713f6d18621a14992b3e81fefb8a5ef9/ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2", size = 4891904, upload-time = "2026-08-13T15:17:13.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/96/493770daebd68c0a67f1549fdf519f53be51fc435186c0585bcc272fd76c/ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7", size = 10902799, upload-time = "2026-08-13T15:16:27.382Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/2becf3942fddc29a29b8df47691d456fb1085391a694f74d84513251418c/ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081", size = 11135539, upload-time = "2026-08-13T15:16:30.87Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1e/4b8b72f0d006dbf19326aa99f9ca0ee2ff374187c4d301cf529a51aa06fe/ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9", size = 10475095, upload-time = "2026-08-13T15:16:33.259Z" }, + { url = "https://files.pythonhosted.org/packages/92/32/2201fa49ba1f6c101ee321e83f051ac7a4b8d07b0ef6b4d3f2772b302275/ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84", size = 10668771, upload-time = "2026-08-13T15:16:35.65Z" }, + { url = "https://files.pythonhosted.org/packages/c3/66/4afc5c8363bd04d45effce1b7c8713ca037d7a6740b7451a2403a6e3a972/ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870", size = 10699568, upload-time = "2026-08-13T15:16:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/53/fd/c67d246bf36bf1698551c56de39e95cd07f70e64433e0098e6267d77061b/ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b", size = 11499365, upload-time = "2026-08-13T15:16:40.623Z" }, + { url = "https://files.pythonhosted.org/packages/67/0b/00ecbceb99a263af7b12f6f05ac3c92bc47b905e91adc3f207a836e3bc01/ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413", size = 12311728, upload-time = "2026-08-13T15:16:43.564Z" }, + { url = "https://files.pythonhosted.org/packages/54/b2/b7b3bb54f4d3f7db504e476ad4ab8de530dceebe2c061384b2757ee419e8/ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82", size = 11699896, upload-time = "2026-08-13T15:16:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/c7/30/4c468429ac195addc5ee1b717b6ab1b66632786737ca3b2ed3443fb0c26a/ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb", size = 11058736, upload-time = "2026-08-13T15:16:48.823Z" }, + { url = "https://files.pythonhosted.org/packages/43/67/7a113cdaddf24b64d7f75b1242a99d04c82fcef4f6921fdbb832beaffb5f/ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474", size = 11586911, upload-time = "2026-08-13T15:16:51.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c1/2e66f24c0f3ead25a5e660111778685e505e5da353c82802bf49f0cbe7b9/ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da", size = 10954265, upload-time = "2026-08-13T15:16:54.763Z" }, + { url = "https://files.pythonhosted.org/packages/c2/ba/4cee23bf52cba9a058d3726de623624daf50ef9638868edd86f4126157f6/ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50", size = 10709886, upload-time = "2026-08-13T15:16:57.339Z" }, + { url = "https://files.pythonhosted.org/packages/82/df/7da7194fa5d9dc0a285f7e6fa5a4722e7c63faac0b45b614ded9314363a1/ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506", size = 11210392, upload-time = "2026-08-13T15:17:00.171Z" }, + { url = "https://files.pythonhosted.org/packages/35/85/7795f6e817af050e7517bf3e7aa9b061cce70ef33d280aad902c956c1ecf/ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d", size = 11626910, upload-time = "2026-08-13T15:17:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/78/9b/475b927cf27a5cbbda3c7bafb69ed6ff77e1d7923d5d85f17c2749d7ae32/ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a", size = 10931415, upload-time = "2026-08-13T15:17:05.726Z" }, + { url = "https://files.pythonhosted.org/packages/b2/99/e2a2bfc4fbf0a1e8a916bc9ebe6fe6c58cc34c28e0ffc6ce281d572d1c2e/ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948", size = 11445993, upload-time = "2026-08-13T15:17:08.353Z" }, + { url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, +] + +[[package]] +name = "uritemplate" +version = "4.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/60/f174043244c5306c9988380d2cb10009f91563fc4b31293d27e17201af56/uritemplate-4.2.0.tar.gz", hash = "sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e", size = 33267, upload-time = "2025-06-02T15:12:06.318Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/99/3ae339466c9183ea5b8ae87b34c0b897eda475d2aec2307cae60e5cd4f29/uritemplate-4.2.0-py3-none-any.whl", hash = "sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686", size = 11488, upload-time = "2025-06-02T15:12:03.405Z" }, +] + +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] diff --git a/package.json b/package.json index ef813e578..f66b2e7a8 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,9 @@ "start": "npx serve out", "lint": "eslint .", "check-format": "prettier --check .", + "lint:python": "uv run --directory analytics --only-dev --locked ruff check .", + "check-format:python": "uv run --directory analytics --only-dev --locked ruff format --check .", + "format:python": "uv run --directory analytics --only-dev --locked ruff format .", "prepare": "husky", "test": "jest --watch", "test:e2e": "playwright test",