Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
5339ea3f97
|
|||
| 33199e35cd | |||
| 5da9ba4b0b | |||
| cb5c03ab74 | |||
| 7668f790aa | |||
| a79c245511 | |||
| c0b5e95d98 | |||
| c9c287aa8a | |||
| b573b3b9bc |
@@ -10,3 +10,4 @@ __pycache__/*
|
|||||||
/logs/*
|
/logs/*
|
||||||
.vscode/*
|
.vscode/*
|
||||||
*.coverage
|
*.coverage
|
||||||
|
.ruff_cache/*
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
3.13
|
||||||
-161
@@ -1,161 +0,0 @@
|
|||||||
'''Module to create a Dash app that displays visualizations of YNAB data.'''
|
|
||||||
|
|
||||||
import polars as pl
|
|
||||||
import plotly.express as px
|
|
||||||
from dash import Dash, html, dcc
|
|
||||||
import dash_bootstrap_components as dbc
|
|
||||||
import pandas as pd
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
import config.exit_codes as ec
|
|
||||||
|
|
||||||
try:
|
|
||||||
accounts = pl.read_parquet('data/warehouse/accounts.parquet')
|
|
||||||
categories = pl.read_parquet('data/warehouse/categories.parquet')
|
|
||||||
dates = pl.read_parquet('data/warehouse/dates.parquet')
|
|
||||||
payees = pl.read_parquet('data/warehouse/payees.parquet')
|
|
||||||
scheduled_transactions = pl.read_parquet('data/warehouse/scheduled_transactions.parquet')
|
|
||||||
transactions = pl.read_parquet('data/warehouse/transactions.parquet')
|
|
||||||
except FileNotFoundError:
|
|
||||||
logging.error('Data warehouse files not found. Run the data pipeline to create them.')
|
|
||||||
sys.exit(ec.MISSING_DATA_FILES)
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Join transactions with accounts, categories, and payees to create a master DataFrame
|
|
||||||
master_transactions = transactions.join(categories, left_on='category_id', right_on='category_id', suffix='_category')\
|
|
||||||
.join(accounts, left_on='account_id', right_on='account_id', suffix='_account')\
|
|
||||||
.join(payees, left_on='payee_id', right_on='payee_id', suffix='_payee')\
|
|
||||||
.join(dates, left_on='transaction_date', right_on='date_id', suffix='_date')
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(f'Error joining DataFrames: {e}')
|
|
||||||
sys.exit(ec.BAD_JOIN)
|
|
||||||
|
|
||||||
# Create aggregations
|
|
||||||
spend_per_day = master_transactions.sql('''
|
|
||||||
SELECT
|
|
||||||
date,
|
|
||||||
year,
|
|
||||||
month,
|
|
||||||
day,
|
|
||||||
ABS(SUM(transaction_amount)) as total
|
|
||||||
FROM self
|
|
||||||
WHERE category_name != 'Inflow: Ready to Assign'
|
|
||||||
GROUP BY date, year, month, day
|
|
||||||
ORDER BY date DESC
|
|
||||||
'''
|
|
||||||
)
|
|
||||||
|
|
||||||
spend_per_category = master_transactions.sql('''
|
|
||||||
SELECT
|
|
||||||
category_name,
|
|
||||||
ABS(SUM(transaction_amount)) as total
|
|
||||||
FROM self
|
|
||||||
WHERE category_name != 'Inflow: Ready to Assign'
|
|
||||||
GROUP BY category_name
|
|
||||||
ORDER BY total DESC
|
|
||||||
'''
|
|
||||||
)
|
|
||||||
|
|
||||||
spend_per_payee = master_transactions.sql('''
|
|
||||||
SELECT
|
|
||||||
payee_name,
|
|
||||||
ABS(SUM(transaction_amount)) as total
|
|
||||||
FROM self
|
|
||||||
WHERE payee_name != 'Starting Balance'
|
|
||||||
AND transaction_amount < 0
|
|
||||||
GROUP BY payee_name
|
|
||||||
ORDER BY total DESC
|
|
||||||
'''
|
|
||||||
)
|
|
||||||
|
|
||||||
# Convert DataFrame to list of dictionaries
|
|
||||||
spend_per_day_data = spend_per_day.to_dicts()
|
|
||||||
spend_per_category_data = spend_per_category.to_dicts()
|
|
||||||
spend_per_payee_data = spend_per_payee.to_dicts()
|
|
||||||
|
|
||||||
# Convert list of dictionaries to Pandas DataFrame
|
|
||||||
spend_per_day_df = pd.DataFrame(spend_per_day_data)
|
|
||||||
spend_per_category_df = pd.DataFrame(spend_per_category_data)
|
|
||||||
spend_per_payee_df = pd.DataFrame(spend_per_payee_data)
|
|
||||||
|
|
||||||
spend_per_day_line = px.line(spend_per_day_df, x="date", y="total")
|
|
||||||
spend_per_day_line.update_layout(
|
|
||||||
plot_bgcolor='black',
|
|
||||||
paper_bgcolor='black',
|
|
||||||
font_color='white'
|
|
||||||
)
|
|
||||||
|
|
||||||
spend_per_category_bar = px.bar(spend_per_category_df, x="category_name", y="total")
|
|
||||||
spend_per_category_bar.update_layout(
|
|
||||||
plot_bgcolor='black',
|
|
||||||
paper_bgcolor='black',
|
|
||||||
font_color='white'
|
|
||||||
)
|
|
||||||
|
|
||||||
spend_per_payee_bar = px.bar(spend_per_payee_df, x="payee_name", y="total")
|
|
||||||
spend_per_payee_bar.update_layout(
|
|
||||||
plot_bgcolor='black',
|
|
||||||
paper_bgcolor='black',
|
|
||||||
font_color='white'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Initialize the app with a dark theme
|
|
||||||
app = Dash(external_stylesheets=[dbc.themes.DARKLY])
|
|
||||||
|
|
||||||
# App layout
|
|
||||||
app.layout = dbc.Container(
|
|
||||||
[
|
|
||||||
dbc.Row(
|
|
||||||
dbc.Col(
|
|
||||||
html.Div("Data Pipeline For YNAB, Preview Visualisations",
|
|
||||||
className="text-center text-light"),
|
|
||||||
width=12
|
|
||||||
)
|
|
||||||
),
|
|
||||||
dbc.Row(
|
|
||||||
[
|
|
||||||
dbc.Col(
|
|
||||||
dbc.Card(
|
|
||||||
dbc.CardBody(
|
|
||||||
[
|
|
||||||
html.H4("Spend Per Day", className="card-title"),
|
|
||||||
dcc.Graph(figure=spend_per_day_line)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
className="mb-4"
|
|
||||||
),
|
|
||||||
width=12
|
|
||||||
)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
dbc.Row(
|
|
||||||
[
|
|
||||||
dbc.Col(
|
|
||||||
dbc.Card(
|
|
||||||
dbc.CardBody(
|
|
||||||
[
|
|
||||||
html.H4("Spend Per Category", className="card-title"),
|
|
||||||
dcc.Graph(figure=spend_per_category_bar)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
className="mb-4"
|
|
||||||
),
|
|
||||||
width=6
|
|
||||||
),
|
|
||||||
dbc.Col(
|
|
||||||
dbc.Card(
|
|
||||||
dbc.CardBody(
|
|
||||||
[
|
|
||||||
html.H4("Spend Per Payee", className="card-title"),
|
|
||||||
dcc.Graph(figure=spend_per_payee_bar)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
className="mb-4"
|
|
||||||
),
|
|
||||||
width=6
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
],
|
|
||||||
fluid=True
|
|
||||||
)
|
|
||||||
+72
-15
@@ -1,17 +1,74 @@
|
|||||||
import polars as pl
|
import yaml
|
||||||
|
|
||||||
df = pl.read_parquet('data/warehouse/transactions.parquet')
|
# accounts = pl.read_parquet('data/warehouse/accounts.parquet')
|
||||||
print("Data loaded from Parquet file:")
|
# categories = pl.read_parquet('data/warehouse/categories.parquet')
|
||||||
print(df)
|
# dates = pl.read_parquet('data/warehouse/dates.parquet')
|
||||||
|
# payees = pl.read_parquet('data/warehouse/payees.parquet')
|
||||||
|
# scheduled_transactions = pl.read_parquet('data/warehouse/scheduled_transactions.parquet')
|
||||||
|
# transactions = pl.read_parquet('data/warehouse/transactions.parquet')
|
||||||
|
|
||||||
relevant_data = df.sql('''
|
|
||||||
SELECT
|
# master_transactions = transactions.join(categories, left_on='category_id', right_on='category_id', suffix='_category')\
|
||||||
date,
|
# .join(accounts, left_on='account_id', right_on='account_id', suffix='_account')\
|
||||||
sum(transaction_amount) as total
|
# .join(payees, left_on='payee_id', right_on='payee_id', suffix='_payee')\
|
||||||
FROM self
|
# .join(dates, left_on='transaction_date', right_on='date_id', suffix='_date')
|
||||||
GROUP BY date
|
|
||||||
ORDER BY date DESC
|
|
||||||
'''
|
# # Create aggregations
|
||||||
)
|
# spend_per_day = master_transactions.sql('''
|
||||||
print("Data after SQL query:")
|
# SELECT
|
||||||
print(relevant_data)
|
# date,
|
||||||
|
# year,
|
||||||
|
# month,
|
||||||
|
# day,
|
||||||
|
# ABS(SUM(transaction_amount)) as total
|
||||||
|
# FROM self
|
||||||
|
# WHERE category_name != 'Inflow: Ready to Assign'
|
||||||
|
# GROUP BY date, year, month, day
|
||||||
|
# ORDER BY date DESC
|
||||||
|
# '''
|
||||||
|
# )
|
||||||
|
|
||||||
|
# spend_per_category = master_transactions.sql('''
|
||||||
|
# SELECT
|
||||||
|
# category_name,
|
||||||
|
# ABS(SUM(transaction_amount)) as total
|
||||||
|
# FROM self
|
||||||
|
# WHERE category_name != 'Inflow: Ready to Assign'
|
||||||
|
# GROUP BY category_name
|
||||||
|
# ORDER BY total DESC
|
||||||
|
# '''
|
||||||
|
# )
|
||||||
|
|
||||||
|
# spend_per_payee = master_transactions.sql('''
|
||||||
|
# SELECT
|
||||||
|
# payee_name,
|
||||||
|
# ABS(SUM(transaction_amount)) as total
|
||||||
|
# FROM self
|
||||||
|
# WHERE payee_name != 'Starting Balance'
|
||||||
|
# AND transaction_amount < 0
|
||||||
|
# GROUP BY payee_name
|
||||||
|
# ORDER BY total DESC
|
||||||
|
# '''
|
||||||
|
# )
|
||||||
|
|
||||||
|
# def update_dates(start_date, end_date):
|
||||||
|
# print("start date", start_date)
|
||||||
|
# print("end date", end_date)
|
||||||
|
# print(master_transactions)
|
||||||
|
# master_data = master_transactions.filter(
|
||||||
|
# pl.col("date").is_between(start_date, end_date)
|
||||||
|
# )
|
||||||
|
# return master_data
|
||||||
|
|
||||||
|
# today = date.today()
|
||||||
|
# one_year_ago = today - timedelta(days=5)
|
||||||
|
|
||||||
|
# data = update_dates(start_date=one_year_ago, end_date=today)
|
||||||
|
# print(data)
|
||||||
|
|
||||||
|
with open('config/config.yaml', 'r') as file:
|
||||||
|
config = yaml.safe_load(file)
|
||||||
|
|
||||||
|
for k,v in config.items():
|
||||||
|
print(k,v)
|
||||||
|
|||||||
@@ -5,70 +5,82 @@ import yaml
|
|||||||
import sys
|
import sys
|
||||||
import atexit
|
import atexit
|
||||||
import logging.config
|
import logging.config
|
||||||
import logging.handlers
|
|
||||||
|
|
||||||
import config.exit_codes as ec
|
import config.exit_codes as ec
|
||||||
from pipeline.pipeline_main import pipeline_main
|
from visuals.dash_app import app
|
||||||
|
|
||||||
|
|
||||||
def set_up_logging():
|
def set_up_logging():
|
||||||
try:
|
try:
|
||||||
with open('config/logging_config.yaml', 'r') as f:
|
with open("config/logging_config.yaml", "r") as f:
|
||||||
log_config = yaml.safe_load(f)
|
log_config = yaml.safe_load(f)
|
||||||
logging.config.dictConfig(log_config)
|
logging.config.dictConfig(log_config)
|
||||||
except yaml.YAMLError as e:
|
except yaml.YAMLError as e:
|
||||||
print(f"Error parsing logging configuration file: {e}")
|
print(f"Error parsing logging configuration file: {e}")
|
||||||
log_config = {} # Initialize log_config to an empty dictionary
|
log_config = {} # Initialize log_config to an empty dictionary
|
||||||
logging.basicConfig(level=logging.INFO) # Fallback to a basic configuration
|
# Fallback to a basic configuration
|
||||||
queue_handler = logging.getHandlerByName('queue_handler')
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
queue_handler = logging.getHandlerByName("queue_handler")
|
||||||
if queue_handler is not None:
|
if queue_handler is not None:
|
||||||
queue_handler.listener.start()
|
queue_handler.listener.start()
|
||||||
atexit.register(queue_handler.listener.stop)
|
atexit.register(queue_handler.listener.stop)
|
||||||
|
|
||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
try:
|
try:
|
||||||
with open('config/config.yaml', 'r') as file:
|
with open("config/config.yaml", "r") as file:
|
||||||
config = yaml.safe_load(file)
|
config = yaml.safe_load(file)
|
||||||
return config
|
return config
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
logging.error('config.yaml file not found')
|
logging.error("config.yaml file not found")
|
||||||
sys.exit(ec.MISSING_CONFIG_FILE)
|
sys.exit(ec.MISSING_CONFIG_FILE)
|
||||||
except yaml.YAMLError as e:
|
except yaml.YAMLError:
|
||||||
logging.error(f'Error loading config.yaml: {e}')
|
# Attempt to print a more informative error message
|
||||||
|
try:
|
||||||
|
parsed_yaml = yaml.safe_load_all(file)
|
||||||
|
for key, value in parsed_yaml:
|
||||||
|
print(f"Error parsing key '{key}': {value}")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error loading config.yaml: {e}")
|
||||||
sys.exit(ec.CORRUPTED_CONFIG_FILE)
|
sys.exit(ec.CORRUPTED_CONFIG_FILE)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"some other problem {e}")
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("data_pipeline_for_ynab")
|
logger = logging.getLogger("data_pipeline_for_ynab")
|
||||||
os.makedirs('logs', exist_ok=True)
|
os.makedirs("logs", exist_ok=True)
|
||||||
set_up_logging()
|
set_up_logging()
|
||||||
|
|
||||||
# Load environment variables
|
# Load environment variables
|
||||||
dotenv.load_dotenv()
|
dotenv.load_dotenv()
|
||||||
|
|
||||||
API_TOKEN = os.getenv('API_TOKEN')
|
API_TOKEN = os.getenv("API_TOKEN")
|
||||||
BUDGET_ID = os.getenv('BUDGET_ID')
|
BUDGET_ID = os.getenv("BUDGET_ID")
|
||||||
|
|
||||||
if not API_TOKEN or not BUDGET_ID:
|
if not API_TOKEN or not BUDGET_ID:
|
||||||
logging.error('API_TOKEN or BUDGET_ID is not set in .env file')
|
logging.error("API_TOKEN or BUDGET_ID is not set in .env file")
|
||||||
sys.exit(ec.MISSING_ENV_VARS)
|
sys.exit(ec.MISSING_ENV_VARS)
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
config = load_config()
|
config = load_config()
|
||||||
config['API_TOKEN'] = API_TOKEN
|
config["API_TOKEN"] = API_TOKEN
|
||||||
config['BUDGET_ID'] = BUDGET_ID
|
config["BUDGET_ID"] = BUDGET_ID
|
||||||
try:
|
try:
|
||||||
pipeline_main(config)
|
# pipeline_main(config)
|
||||||
|
|
||||||
# Check if the data was successfully created
|
# Check if the data was successfully created
|
||||||
data_exists = os.path.exists('data/processed') and os.listdir('data/processed')
|
data_exists = os.path.exists("data/processed") and os.listdir("data/processed")
|
||||||
if data_exists:
|
if data_exists:
|
||||||
from dash_app import app
|
app.run(debug=True)
|
||||||
app.run() # debug=True
|
|
||||||
else:
|
else:
|
||||||
logging.error('Data pipeline did not produce any data. Dash app will not run.')
|
logging.error(
|
||||||
|
"Data pipeline did not produce any data. Dash app will not run."
|
||||||
|
)
|
||||||
sys.exit(ec.NO_DATA_PRODUCED)
|
sys.exit(ec.NO_DATA_PRODUCED)
|
||||||
except SystemExit as e:
|
except SystemExit as e:
|
||||||
exit_code = e.code
|
exit_code = e.code
|
||||||
if exit_code == ec.SUCCESS:
|
if exit_code == ec.SUCCESS:
|
||||||
logging.info('Program exited successfully')
|
logging.info("Program exited successfully")
|
||||||
else:
|
else:
|
||||||
logging.error(f'Program exited with code {exit_code}')
|
logging.error(f"Program exited with code {exit_code}")
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
[project]
|
||||||
|
name = "data-pipeline-for-ynab"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"dash>=3.0.4",
|
||||||
|
"dash-bootstrap-components>=2.0.3",
|
||||||
|
"pandas>=2.2.3",
|
||||||
|
"polars>=1.30.0",
|
||||||
|
"pyarrow>=20.0.0",
|
||||||
|
"pytest>=8.3.5",
|
||||||
|
"python-dotenv>=1.1.0",
|
||||||
|
"pyyaml>=6.0.2",
|
||||||
|
"requests>=2.32.3",
|
||||||
|
"ruff>=0.11.12",
|
||||||
|
]
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
python-dotenv
|
|
||||||
polars
|
|
||||||
requests
|
|
||||||
pyyaml
|
|
||||||
#visualisation requirements below
|
|
||||||
dash
|
|
||||||
pandas
|
|
||||||
pyarrow
|
|
||||||
dash-bootstrap-components
|
|
||||||
# testing requirements below
|
|
||||||
pytest
|
|
||||||
+131
-83
@@ -2,112 +2,125 @@ import pytest
|
|||||||
from unittest.mock import patch, mock_open, MagicMock
|
from unittest.mock import patch, mock_open, MagicMock
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
from typing import Dict, Any
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from pipeline.ingest import Ingest
|
from pipeline.ingest import Ingest
|
||||||
import config.exit_codes as ec
|
import config.exit_codes as ec
|
||||||
|
|
||||||
# Mock configuration for initializing the Ingest class
|
# Mock configuration for initializing the Ingest class
|
||||||
mock_config = {
|
mock_config = {
|
||||||
'API_TOKEN': 'test_token',
|
"API_TOKEN": "test_token",
|
||||||
'BUDGET_ID': 'test_budget_id',
|
"BUDGET_ID": "test_budget_id",
|
||||||
'base_url': 'http://test_base_url',
|
"base_url": "http://test_base_url",
|
||||||
'knowledge_file': 'data/test_knowledge_file.json',
|
"knowledge_file": "data/test_knowledge_file.json",
|
||||||
'entities': ['entity1', 'entity2'],
|
"entities": ["entity1", "entity2"],
|
||||||
'raw_data_path': 'test_raw_data_path',
|
"raw_data_path": "test_raw_data_path",
|
||||||
'REQUESTS_MAX_RETRIES': 3,
|
"REQUESTS_MAX_RETRIES": 3,
|
||||||
'REQUESTS_RETRY_DELAY': 1
|
"REQUESTS_RETRY_DELAY": 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Test for load_knowledge_cache method
|
# Test for load_knowledge_cache method
|
||||||
|
|
||||||
|
|
||||||
def test_load_knowledge_cache_file_exists():
|
def test_load_knowledge_cache_file_exists():
|
||||||
mock_data = {"key": "value"}
|
mock_data = {"key": "value"}
|
||||||
with patch('os.path.exists', return_value=True), \
|
with (
|
||||||
patch('builtins.open', mock_open(read_data=json.dumps(mock_data))) as mock_file:
|
patch("os.path.exists", return_value=True),
|
||||||
|
patch("builtins.open", mock_open(read_data=json.dumps(mock_data))) as mock_file,
|
||||||
|
):
|
||||||
ingest_instance = Ingest(mock_config)
|
ingest_instance = Ingest(mock_config)
|
||||||
result = ingest_instance.load_knowledge_cache()
|
result = ingest_instance.load_knowledge_cache()
|
||||||
|
|
||||||
mock_file.assert_called_once_with(mock_config['knowledge_file'], 'r')
|
mock_file.assert_called_once_with(mock_config["knowledge_file"], "r")
|
||||||
assert result == mock_data
|
assert result == mock_data
|
||||||
|
|
||||||
def test_load_knowledge_cache_file_not_exists():
|
|
||||||
with patch('os.path.exists', return_value=False):
|
|
||||||
|
|
||||||
|
def test_load_knowledge_cache_file_not_exists():
|
||||||
|
with patch("os.path.exists", return_value=False):
|
||||||
ingest_instance = Ingest(mock_config)
|
ingest_instance = Ingest(mock_config)
|
||||||
result = ingest_instance.load_knowledge_cache()
|
result = ingest_instance.load_knowledge_cache()
|
||||||
|
|
||||||
assert result == {}
|
assert result == {}
|
||||||
|
|
||||||
|
|
||||||
# Test for save_entity_data_to_raw method
|
# Test for save_entity_data_to_raw method
|
||||||
|
|
||||||
|
|
||||||
def test_save_entity_data_to_raw_success():
|
def test_save_entity_data_to_raw_success():
|
||||||
entity = 'entity1'
|
entity = "entity1"
|
||||||
data = {"key": "value"}
|
data = {"key": "value"}
|
||||||
current_time = '20230101123000'
|
current_time = "20230101123000"
|
||||||
directory = os.path.join(mock_config['raw_data_path'], entity)
|
directory = os.path.join(mock_config["raw_data_path"], entity)
|
||||||
entity_file = f'{directory}/{current_time}.json'
|
entity_file = f"{directory}/{current_time}.json"
|
||||||
|
|
||||||
with patch('os.path.exists', return_value=False), \
|
|
||||||
patch('os.makedirs') as mock_makedirs, \
|
|
||||||
patch('builtins.open', mock_open()) as mock_file, \
|
|
||||||
patch('time.strftime', return_value=current_time), \
|
|
||||||
patch('logging.info') as mock_logging_info:
|
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("os.path.exists", return_value=False),
|
||||||
|
patch("os.makedirs") as mock_makedirs,
|
||||||
|
patch("builtins.open", mock_open()) as mock_file,
|
||||||
|
patch("time.strftime", return_value=current_time),
|
||||||
|
patch("logging.info") as mock_logging_info,
|
||||||
|
):
|
||||||
ingest_instance = Ingest(mock_config)
|
ingest_instance = Ingest(mock_config)
|
||||||
ingest_instance.save_entity_data_to_raw(entity, data)
|
ingest_instance.save_entity_data_to_raw(entity, data)
|
||||||
|
|
||||||
mock_makedirs.assert_called_once_with(directory)
|
mock_makedirs.assert_called_once_with(directory)
|
||||||
mock_file.assert_called_once_with(entity_file, 'w')
|
mock_file.assert_called_once_with(entity_file, "w")
|
||||||
|
|
||||||
# Get the file handle and check the written content
|
# Get the file handle and check the written content
|
||||||
handle = mock_file()
|
handle = mock_file()
|
||||||
handle.write.assert_called()
|
handle.write.assert_called()
|
||||||
written_content = ''.join(call.args[0] for call in handle.write.call_args_list)
|
written_content = "".join(call.args[0] for call in handle.write.call_args_list)
|
||||||
assert written_content == json.dumps(data, indent=4)
|
assert written_content == json.dumps(data, indent=4)
|
||||||
|
|
||||||
mock_logging_info.assert_called_once_with(f"Saving {entity} data to {entity_file}")
|
mock_logging_info.assert_called_once_with(
|
||||||
|
f"Saving {entity} data to {entity_file}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_save_entity_data_to_raw_existing_directory():
|
def test_save_entity_data_to_raw_existing_directory():
|
||||||
entity = 'entity1'
|
entity = "entity1"
|
||||||
data = {"key": "value"}
|
data = {"key": "value"}
|
||||||
current_time = '20230101123000'
|
current_time = "20230101123000"
|
||||||
directory = os.path.join(mock_config['raw_data_path'], entity)
|
directory = os.path.join(mock_config["raw_data_path"], entity)
|
||||||
entity_file = f'{directory}/{current_time}.json'
|
entity_file = f"{directory}/{current_time}.json"
|
||||||
|
|
||||||
with patch('os.path.exists', return_value=True), \
|
|
||||||
patch('os.makedirs') as mock_makedirs, \
|
|
||||||
patch('builtins.open', mock_open()) as mock_file, \
|
|
||||||
patch('time.strftime', return_value=current_time), \
|
|
||||||
patch('logging.info') as mock_logging_info:
|
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("os.path.exists", return_value=True),
|
||||||
|
patch("os.makedirs") as mock_makedirs,
|
||||||
|
patch("builtins.open", mock_open()) as mock_file,
|
||||||
|
patch("time.strftime", return_value=current_time),
|
||||||
|
patch("logging.info") as mock_logging_info,
|
||||||
|
):
|
||||||
ingest_instance = Ingest(mock_config)
|
ingest_instance = Ingest(mock_config)
|
||||||
ingest_instance.save_entity_data_to_raw(entity, data)
|
ingest_instance.save_entity_data_to_raw(entity, data)
|
||||||
|
|
||||||
mock_makedirs.assert_not_called()
|
mock_makedirs.assert_not_called()
|
||||||
mock_file.assert_called_once_with(entity_file, 'w')
|
mock_file.assert_called_once_with(entity_file, "w")
|
||||||
|
|
||||||
# Get the file handle and check the written content
|
# Get the file handle and check the written content
|
||||||
handle = mock_file()
|
handle = mock_file()
|
||||||
handle.write.assert_called()
|
handle.write.assert_called()
|
||||||
written_content = ''.join(call.args[0] for call in handle.write.call_args_list)
|
written_content = "".join(call.args[0] for call in handle.write.call_args_list)
|
||||||
assert written_content == json.dumps(data, indent=4)
|
assert written_content == json.dumps(data, indent=4)
|
||||||
|
|
||||||
mock_logging_info.assert_called_once_with(f"Saving {entity} data to {entity_file}")
|
mock_logging_info.assert_called_once_with(
|
||||||
|
f"Saving {entity} data to {entity_file}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_save_entity_data_to_raw_error():
|
def test_save_entity_data_to_raw_error():
|
||||||
entity = 'entity1'
|
entity = "entity1"
|
||||||
data = {"key": "value"}
|
data = {"key": "value"}
|
||||||
current_time = '20230101123000'
|
current_time = "20230101123000"
|
||||||
directory = os.path.join(mock_config['raw_data_path'], entity)
|
directory = os.path.join(mock_config["raw_data_path"], entity)
|
||||||
entity_file = f'{directory}/{current_time}.json'
|
entity_file = f"{directory}/{current_time}.json"
|
||||||
|
|
||||||
with patch('os.path.exists', return_value=True), \
|
|
||||||
patch('builtins.open', mock_open()) as mock_file, \
|
|
||||||
patch('time.strftime', return_value=current_time), \
|
|
||||||
patch('logging.info') as mock_logging_info, \
|
|
||||||
patch('logging.error') as mock_logging_error:
|
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("os.path.exists", return_value=True),
|
||||||
|
patch("builtins.open", mock_open()) as mock_file,
|
||||||
|
patch("time.strftime", return_value=current_time),
|
||||||
|
patch("logging.info") as mock_logging_info,
|
||||||
|
patch("logging.error") as mock_logging_error,
|
||||||
|
):
|
||||||
mock_file.side_effect = Exception("Test error")
|
mock_file.side_effect = Exception("Test error")
|
||||||
|
|
||||||
ingest_instance = Ingest(mock_config)
|
ingest_instance = Ingest(mock_config)
|
||||||
@@ -115,39 +128,47 @@ def test_save_entity_data_to_raw_error():
|
|||||||
with pytest.raises(Exception, match="Test error"):
|
with pytest.raises(Exception, match="Test error"):
|
||||||
ingest_instance.save_entity_data_to_raw(entity, data)
|
ingest_instance.save_entity_data_to_raw(entity, data)
|
||||||
|
|
||||||
mock_logging_error.assert_called_once_with(f"Failed to save data for {entity} to {entity_file}")
|
mock_logging_error.assert_called_once_with(
|
||||||
|
f"Failed to save data for {entity} to {entity_file}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_update_server_knowledge_cache_file_exists():
|
def test_update_server_knowledge_cache_file_exists():
|
||||||
entity = 'entity1'
|
entity = "entity1"
|
||||||
server_knowledge = {"key": "value"}
|
server_knowledge = {"key": "value"}
|
||||||
existing_cache = {"entity2": {"key": "old_value"}}
|
existing_cache = {"entity2": {"key": "old_value"}}
|
||||||
updated_cache = {"entity2": {"key": "old_value"}, "entity1": {"key": "value"}}
|
updated_cache = {"entity2": {"key": "old_value"}, "entity1": {"key": "value"}}
|
||||||
|
|
||||||
with patch('builtins.open', mock_open(read_data=json.dumps(existing_cache))) as mock_file, \
|
with (
|
||||||
patch('os.path.exists', return_value=True), \
|
patch(
|
||||||
patch('logging.error') as mock_logging_error:
|
"builtins.open", mock_open(read_data=json.dumps(existing_cache))
|
||||||
|
) as mock_file,
|
||||||
|
patch("os.path.exists", return_value=True),
|
||||||
|
patch("logging.error") as mock_logging_error,
|
||||||
|
):
|
||||||
ingest_instance = Ingest(mock_config)
|
ingest_instance = Ingest(mock_config)
|
||||||
ingest_instance.update_server_knowledge_cache(entity, server_knowledge)
|
ingest_instance.update_server_knowledge_cache(entity, server_knowledge)
|
||||||
|
|
||||||
mock_file.assert_called_with(mock_config['knowledge_file'], 'w')
|
mock_file.assert_called_with(mock_config["knowledge_file"], "w")
|
||||||
handle = mock_file()
|
handle = mock_file()
|
||||||
handle.write.assert_called()
|
handle.write.assert_called()
|
||||||
written_content = ''.join(call.args[0] for call in handle.write.call_args_list)
|
written_content = "".join(call.args[0] for call in handle.write.call_args_list)
|
||||||
assert json.loads(written_content) == updated_cache
|
assert json.loads(written_content) == updated_cache
|
||||||
mock_logging_error.assert_not_called()
|
mock_logging_error.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
def test_update_server_knowledge_cache_file_not_exists():
|
def test_update_server_knowledge_cache_file_not_exists():
|
||||||
entity = 'entity1'
|
entity = "entity1"
|
||||||
server_knowledge = {"key": "value"}
|
server_knowledge = {"key": "value"}
|
||||||
updated_cache = {"entity1": {"key": "value"}}
|
updated_cache = {"entity1": {"key": "value"}}
|
||||||
|
|
||||||
with patch('builtins.open', mock_open()) as mock_file, \
|
with (
|
||||||
patch('os.path.exists', return_value=False), \
|
patch("builtins.open", mock_open()) as mock_file,
|
||||||
patch('os.makedirs') as mock_makedirs, \
|
patch("os.path.exists", return_value=False),
|
||||||
patch('logging.info') as mock_logging_info, \
|
patch("os.makedirs") as mock_makedirs,
|
||||||
patch('logging.error') as mock_logging_error:
|
patch("logging.info") as mock_logging_info,
|
||||||
|
patch("logging.error") as mock_logging_error,
|
||||||
|
):
|
||||||
# Ensure the side_effect list has enough elements to cover all calls to open
|
# Ensure the side_effect list has enough elements to cover all calls to open
|
||||||
mock_file.side_effect = [FileNotFoundError(), mock_open().return_value]
|
mock_file.side_effect = [FileNotFoundError(), mock_open().return_value]
|
||||||
|
|
||||||
@@ -156,17 +177,25 @@ def test_update_server_knowledge_cache_file_not_exists():
|
|||||||
with pytest.raises(FileNotFoundError):
|
with pytest.raises(FileNotFoundError):
|
||||||
ingest_instance.update_server_knowledge_cache(entity, server_knowledge)
|
ingest_instance.update_server_knowledge_cache(entity, server_knowledge)
|
||||||
|
|
||||||
mock_makedirs.assert_called_once_with(os.path.dirname(mock_config['knowledge_file']), exist_ok=True)
|
mock_makedirs.assert_called_once_with(
|
||||||
mock_file.assert_called_with(mock_config['knowledge_file'], 'w')
|
os.path.dirname(mock_config["knowledge_file"]), exist_ok=True
|
||||||
mock_logging_error.assert_called_once_with(f"Failed to update knowledge cache for {entity} in {mock_config['knowledge_file']}")
|
)
|
||||||
|
mock_file.assert_called_with(mock_config["knowledge_file"], "w")
|
||||||
|
mock_logging_error.assert_called_once_with(
|
||||||
|
f"Failed to update knowledge cache for {entity} in {
|
||||||
|
mock_config['knowledge_file']
|
||||||
|
}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_update_server_knowledge_cache_write_error():
|
def test_update_server_knowledge_cache_write_error():
|
||||||
entity = 'entity1'
|
entity = "entity1"
|
||||||
server_knowledge = {"key": "value"}
|
server_knowledge = {"key": "value"}
|
||||||
|
|
||||||
with patch('builtins.open', mock_open()) as mock_file, \
|
with (
|
||||||
patch('logging.error') as mock_logging_error:
|
patch("builtins.open", mock_open()) as mock_file,
|
||||||
|
patch("logging.error") as mock_logging_error,
|
||||||
|
):
|
||||||
mock_file.side_effect = Exception("Test error")
|
mock_file.side_effect = Exception("Test error")
|
||||||
|
|
||||||
ingest_instance = Ingest(mock_config)
|
ingest_instance = Ingest(mock_config)
|
||||||
@@ -174,35 +203,43 @@ def test_update_server_knowledge_cache_write_error():
|
|||||||
with pytest.raises(Exception, match="Test error"):
|
with pytest.raises(Exception, match="Test error"):
|
||||||
ingest_instance.update_server_knowledge_cache(entity, server_knowledge)
|
ingest_instance.update_server_knowledge_cache(entity, server_knowledge)
|
||||||
|
|
||||||
mock_logging_error.assert_called_once_with(f"Failed to update knowledge cache for {entity} in {mock_config['knowledge_file']}")
|
mock_logging_error.assert_called_once_with(
|
||||||
|
f"Failed to update knowledge cache for {entity} in {
|
||||||
|
mock_config['knowledge_file']
|
||||||
|
}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_check_rate_limit_above_threshold():
|
def test_check_rate_limit_above_threshold():
|
||||||
response = MagicMock()
|
response = MagicMock()
|
||||||
response.headers = {'X-Rate-Limit': '10/100'}
|
response.headers = {"X-Rate-Limit": "10/100"}
|
||||||
|
|
||||||
ingest_instance = Ingest(mock_config)
|
ingest_instance = Ingest(mock_config)
|
||||||
result = ingest_instance.check_rate_limit(response)
|
result = ingest_instance.check_rate_limit(response)
|
||||||
|
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
def test_check_rate_limit_below_threshold():
|
def test_check_rate_limit_below_threshold():
|
||||||
response = MagicMock()
|
response = MagicMock()
|
||||||
response.headers = {'X-Rate-Limit': '90/100'}
|
response.headers = {"X-Rate-Limit": "90/100"}
|
||||||
|
|
||||||
ingest_instance = Ingest(mock_config)
|
ingest_instance = Ingest(mock_config)
|
||||||
result = ingest_instance.check_rate_limit(response)
|
result = ingest_instance.check_rate_limit(response)
|
||||||
|
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
def test_check_rate_limit_exceeded():
|
def test_check_rate_limit_exceeded():
|
||||||
response = MagicMock()
|
response = MagicMock()
|
||||||
response.headers = {'X-Rate-Limit': '100/100'}
|
response.headers = {"X-Rate-Limit": "100/100"}
|
||||||
|
|
||||||
ingest_instance = Ingest(mock_config)
|
ingest_instance = Ingest(mock_config)
|
||||||
result = ingest_instance.check_rate_limit(response)
|
result = ingest_instance.check_rate_limit(response)
|
||||||
|
|
||||||
assert result is True
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
def test_check_rate_limit_header_missing():
|
def test_check_rate_limit_header_missing():
|
||||||
response = MagicMock()
|
response = MagicMock()
|
||||||
response.headers = {}
|
response.headers = {}
|
||||||
@@ -212,6 +249,7 @@ def test_check_rate_limit_header_missing():
|
|||||||
|
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
|
|
||||||
def test_handle_response_bad_request():
|
def test_handle_response_bad_request():
|
||||||
response = MagicMock()
|
response = MagicMock()
|
||||||
response.status_code = 400
|
response.status_code = 400
|
||||||
@@ -220,9 +258,10 @@ def test_handle_response_bad_request():
|
|||||||
|
|
||||||
with pytest.raises(SystemExit) as e:
|
with pytest.raises(SystemExit) as e:
|
||||||
ingest_instance.handle_response(response)
|
ingest_instance.handle_response(response)
|
||||||
assert e.type == SystemExit
|
assert e.type is SystemExit
|
||||||
assert e.value.code == ec.BAD_REQUEST
|
assert e.value.code == ec.BAD_REQUEST
|
||||||
|
|
||||||
|
|
||||||
def test_handle_response_unauthorized():
|
def test_handle_response_unauthorized():
|
||||||
response = MagicMock()
|
response = MagicMock()
|
||||||
response.status_code = 401
|
response.status_code = 401
|
||||||
@@ -231,9 +270,10 @@ def test_handle_response_unauthorized():
|
|||||||
|
|
||||||
with pytest.raises(SystemExit) as e:
|
with pytest.raises(SystemExit) as e:
|
||||||
ingest_instance.handle_response(response)
|
ingest_instance.handle_response(response)
|
||||||
assert e.type == SystemExit
|
assert e.type is SystemExit
|
||||||
assert e.value.code == ec.UNAUTHORIZED_API_TOKEN
|
assert e.value.code == ec.UNAUTHORIZED_API_TOKEN
|
||||||
|
|
||||||
|
|
||||||
def test_handle_response_forbidden():
|
def test_handle_response_forbidden():
|
||||||
response = MagicMock()
|
response = MagicMock()
|
||||||
response.status_code = 403
|
response.status_code = 403
|
||||||
@@ -242,9 +282,10 @@ def test_handle_response_forbidden():
|
|||||||
|
|
||||||
with pytest.raises(SystemExit) as e:
|
with pytest.raises(SystemExit) as e:
|
||||||
ingest_instance.handle_response(response)
|
ingest_instance.handle_response(response)
|
||||||
assert e.type == SystemExit
|
assert e.type is SystemExit
|
||||||
assert e.value.code == ec.FORBIDDEN
|
assert e.value.code == ec.FORBIDDEN
|
||||||
|
|
||||||
|
|
||||||
def test_handle_response_not_found():
|
def test_handle_response_not_found():
|
||||||
response = MagicMock()
|
response = MagicMock()
|
||||||
response.status_code = 404
|
response.status_code = 404
|
||||||
@@ -253,9 +294,10 @@ def test_handle_response_not_found():
|
|||||||
|
|
||||||
with pytest.raises(SystemExit) as e:
|
with pytest.raises(SystemExit) as e:
|
||||||
ingest_instance.handle_response(response)
|
ingest_instance.handle_response(response)
|
||||||
assert e.type == SystemExit
|
assert e.type is SystemExit
|
||||||
assert e.value.code == ec.NOT_FOUND
|
assert e.value.code == ec.NOT_FOUND
|
||||||
|
|
||||||
|
|
||||||
def test_handle_response_conflict():
|
def test_handle_response_conflict():
|
||||||
response = MagicMock()
|
response = MagicMock()
|
||||||
response.status_code = 409
|
response.status_code = 409
|
||||||
@@ -264,9 +306,10 @@ def test_handle_response_conflict():
|
|||||||
|
|
||||||
with pytest.raises(SystemExit) as e:
|
with pytest.raises(SystemExit) as e:
|
||||||
ingest_instance.handle_response(response)
|
ingest_instance.handle_response(response)
|
||||||
assert e.type == SystemExit
|
assert e.type is SystemExit
|
||||||
assert e.value.code == ec.CONFLICT
|
assert e.value.code == ec.CONFLICT
|
||||||
|
|
||||||
|
|
||||||
def test_handle_response_too_many_requests():
|
def test_handle_response_too_many_requests():
|
||||||
response = MagicMock()
|
response = MagicMock()
|
||||||
response.status_code = 429
|
response.status_code = 429
|
||||||
@@ -276,6 +319,7 @@ def test_handle_response_too_many_requests():
|
|||||||
result = ingest_instance.handle_response(response)
|
result = ingest_instance.handle_response(response)
|
||||||
assert result is True
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
def test_handle_response_internal_server_error():
|
def test_handle_response_internal_server_error():
|
||||||
response = MagicMock()
|
response = MagicMock()
|
||||||
response.status_code = 500
|
response.status_code = 500
|
||||||
@@ -285,6 +329,7 @@ def test_handle_response_internal_server_error():
|
|||||||
result = ingest_instance.handle_response(response)
|
result = ingest_instance.handle_response(response)
|
||||||
assert result is True
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
def test_handle_response_service_unavailable():
|
def test_handle_response_service_unavailable():
|
||||||
response = MagicMock()
|
response = MagicMock()
|
||||||
response.status_code = 503
|
response.status_code = 503
|
||||||
@@ -294,6 +339,7 @@ def test_handle_response_service_unavailable():
|
|||||||
result = ingest_instance.handle_response(response)
|
result = ingest_instance.handle_response(response)
|
||||||
assert result is True
|
assert result is True
|
||||||
|
|
||||||
|
|
||||||
def test_handle_response_ok():
|
def test_handle_response_ok():
|
||||||
response = MagicMock()
|
response = MagicMock()
|
||||||
response.status_code = 200
|
response.status_code = 200
|
||||||
@@ -303,5 +349,7 @@ def test_handle_response_ok():
|
|||||||
result = ingest_instance.handle_response(response)
|
result = ingest_instance.handle_response(response)
|
||||||
assert result is False
|
assert result is False
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
pytest.main()
|
pytest.main()
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import pytest
|
|
||||||
from unittest.mock import patch, mock_open, MagicMock
|
from unittest.mock import patch, mock_open, MagicMock
|
||||||
import yaml
|
import yaml
|
||||||
import logging
|
import logging
|
||||||
import atexit
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from main import set_up_logging, load_config
|
from main import set_up_logging, load_config
|
||||||
import config.exit_codes as ec
|
import config.exit_codes as ec
|
||||||
|
|||||||
@@ -0,0 +1,546 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 2
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "blinker"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2025.4.26"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "3.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.2.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dash"
|
||||||
|
version = "3.0.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "flask" },
|
||||||
|
{ name = "importlib-metadata" },
|
||||||
|
{ name = "nest-asyncio" },
|
||||||
|
{ name = "plotly" },
|
||||||
|
{ name = "requests" },
|
||||||
|
{ name = "retrying" },
|
||||||
|
{ name = "setuptools" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "werkzeug" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/88/6d/90f113317d41266e20190185cf1b5121efbab79ff79b2ecdf8316a91be40/dash-3.0.4.tar.gz", hash = "sha256:4f9e62e9d8c5cd1b42dc6d6dcf211fe9498195f73ef0edb62a26e2a1b952a368", size = 7592060, upload-time = "2025-04-24T19:06:49.287Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/20/2e7ab37ea2ef1f8b2592a2615c8b3fb041ad51f32101061d8bc6465b8b40/dash-3.0.4-py3-none-any.whl", hash = "sha256:177f8c3d1fa45555b18f2f670808eba7803c72a6b1cd6fd172fd538aca18eb1d", size = 7935680, upload-time = "2025-04-24T19:06:41.751Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dash-bootstrap-components"
|
||||||
|
version = "2.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "dash" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/49/8d/0f641e7c7878ac65b4bb78a2c7cb707db036f82da13fd61948adec44d5aa/dash_bootstrap_components-2.0.3.tar.gz", hash = "sha256:5c161b04a6e7ed19a7d54e42f070c29fd6c385d5a7797e7a82999aa2fc15b1de", size = 115466, upload-time = "2025-05-22T22:30:18.02Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/f6/b4652aacfbc8d684c9ca8efc5178860a50b54abf82cd1960013c59f8258f/dash_bootstrap_components-2.0.3-py3-none-any.whl", hash = "sha256:82754d3d001ad5482b8a82b496c7bf98a1c68d2669d607a89dda7ec627304af5", size = 203706, upload-time = "2025-05-22T22:30:16.304Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-pipeline-for-ynab"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "dash" },
|
||||||
|
{ name = "dash-bootstrap-components" },
|
||||||
|
{ name = "pandas" },
|
||||||
|
{ name = "polars" },
|
||||||
|
{ name = "pyarrow" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "python-dotenv" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
{ name = "requests" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "dash", specifier = ">=3.0.4" },
|
||||||
|
{ name = "dash-bootstrap-components", specifier = ">=2.0.3" },
|
||||||
|
{ name = "pandas", specifier = ">=2.2.3" },
|
||||||
|
{ name = "polars", specifier = ">=1.30.0" },
|
||||||
|
{ name = "pyarrow", specifier = ">=20.0.0" },
|
||||||
|
{ name = "pytest", specifier = ">=8.3.5" },
|
||||||
|
{ name = "python-dotenv", specifier = ">=1.1.0" },
|
||||||
|
{ name = "pyyaml", specifier = ">=6.0.2" },
|
||||||
|
{ name = "requests", specifier = ">=2.32.3" },
|
||||||
|
{ name = "ruff", specifier = ">=0.11.12" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flask"
|
||||||
|
version = "3.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "blinker" },
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "itsdangerous" },
|
||||||
|
{ name = "jinja2" },
|
||||||
|
{ name = "werkzeug" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/41/e1/d104c83026f8d35dfd2c261df7d64738341067526406b40190bc063e829a/flask-3.0.3.tar.gz", hash = "sha256:ceb27b0af3823ea2737928a4d99d125a06175b8512c445cbd9a9ce200ef76842", size = 676315, upload-time = "2024-04-07T19:26:11.035Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/80/ffe1da13ad9300f87c93af113edd0638c75138c42a0994becfacac078c06/flask-3.0.3-py3-none-any.whl", hash = "sha256:34e815dfaa43340d1d15a5c3a02b8476004037eb4840b34910c6e21679d288f3", size = 101735, upload-time = "2024-04-07T19:26:08.569Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.10"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "importlib-metadata"
|
||||||
|
version = "8.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "zipp" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itsdangerous"
|
||||||
|
version = "2.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jinja2"
|
||||||
|
version = "3.1.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markupsafe" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markupsafe"
|
||||||
|
version = "3.0.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "narwhals"
|
||||||
|
version = "1.41.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/32/fc/7b9a3689911662be59889b1b0b40e17d5dba6f98080994d86ca1f3154d41/narwhals-1.41.0.tar.gz", hash = "sha256:0ab2e5a1757a19b071e37ca74b53b0b5426789321d68939738337dfddea629b5", size = 488446, upload-time = "2025-05-26T12:46:07.43Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/e0/ade8619846645461c012498f02b93a659e50f07d9d9a6ffefdf5ea2c02a0/narwhals-1.41.0-py3-none-any.whl", hash = "sha256:d958336b40952e4c4b7aeef259a7074851da0800cf902186a58f2faeff97be02", size = 357968, upload-time = "2025-05-26T12:46:05.207Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nest-asyncio"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numpy"
|
||||||
|
version = "2.2.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "25.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pandas"
|
||||||
|
version = "2.2.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "numpy" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "pytz" },
|
||||||
|
{ name = "tzdata" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9c/d6/9f8431bacc2e19dca897724cd097b1bb224a6ad5433784a44b587c7c13af/pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667", size = 4399213, upload-time = "2024-09-20T13:10:04.827Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/22/3b8f4e0ed70644e85cfdcd57454686b9057c6c38d2f74fe4b8bc2527214a/pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015", size = 12477643, upload-time = "2024-09-20T13:09:25.522Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/93/b3f5d1838500e22c8d793625da672f3eec046b1a99257666c94446969282/pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28", size = 11281573, upload-time = "2024-09-20T13:09:28.012Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/94/6c79b07f0e5aab1dcfa35a75f4817f5c4f677931d4234afcd75f0e6a66ca/pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0", size = 15196085, upload-time = "2024-09-20T19:02:10.451Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/31/aa8da88ca0eadbabd0a639788a6da13bb2ff6edbbb9f29aa786450a30a91/pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24", size = 12711809, upload-time = "2024-09-20T13:09:30.814Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/7c/c6dbdb0cb2a4344cacfb8de1c5808ca885b2e4dcfde8008266608f9372af/pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659", size = 16356316, upload-time = "2024-09-20T19:02:13.825Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/b7/8b757e7d92023b832869fa8881a992696a0bfe2e26f72c9ae9f255988d42/pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb", size = 14022055, upload-time = "2024-09-20T13:09:33.462Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/bc/4b18e2b8c002572c5a441a64826252ce5da2aa738855747247a971988043/pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d", size = 11481175, upload-time = "2024-09-20T13:09:35.871Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/a3/a5d88146815e972d40d19247b2c162e88213ef51c7c25993942c39dbf41d/pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468", size = 12615650, upload-time = "2024-09-20T13:09:38.685Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/8c/f0fd18f6140ddafc0c24122c8a964e48294acc579d47def376fef12bcb4a/pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18", size = 11290177, upload-time = "2024-09-20T13:09:41.141Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/f9/e995754eab9c0f14c6777401f7eece0943840b7a9fc932221c19d1abee9f/pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2", size = 14651526, upload-time = "2024-09-20T19:02:16.905Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/b0/98d6ae2e1abac4f35230aa756005e8654649d305df9a28b16b9ae4353bff/pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4", size = 11871013, upload-time = "2024-09-20T13:09:44.39Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/57/0f72a10f9db6a4628744c8e8f0df4e6e21de01212c7c981d31e50ffc8328/pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d", size = 15711620, upload-time = "2024-09-20T19:02:20.639Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436, upload-time = "2024-09-20T13:09:48.112Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "plotly"
|
||||||
|
version = "6.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "narwhals" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ae/77/431447616eda6a432dc3ce541b3f808ecb8803ea3d4ab2573b67f8eb4208/plotly-6.1.2.tar.gz", hash = "sha256:4fdaa228926ba3e3a213f4d1713287e69dcad1a7e66cf2025bd7d7026d5014b4", size = 7662971, upload-time = "2025-05-27T20:21:52.56Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/6f/759d5da0517547a5d38aabf05d04d9f8adf83391d2c7fc33f904417d3ba2/plotly-6.1.2-py3-none-any.whl", hash = "sha256:f1548a8ed9158d59e03d7fed548c7db5549f3130d9ae19293c8638c202648f6d", size = 16265530, upload-time = "2025-05-27T20:21:46.6Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "polars"
|
||||||
|
version = "1.30.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/82/b6/8dbdf626c0705a57f052708c9fc0860ffc2aa97955930d5faaf6a66fcfd3/polars-1.30.0.tar.gz", hash = "sha256:dfe94ae84a5efd9ba74e616e3e125b24ca155494a931890a8f17480737c4db45", size = 4668318, upload-time = "2025-05-21T13:33:24.175Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/48/e9b2cb379abcc9f7aff2e701098fcdb9fe6d85dc4ad4cec7b35d39c70951/polars-1.30.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:4c33bc97c29b7112f0e689a2f8a33143973a3ff466c70b25c7fd1880225de6dd", size = 35704342, upload-time = "2025-05-21T13:32:22.996Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/ca/f545f61282f75eea4dfde4db2944963dcd59abd50c20e33a1c894da44dad/polars-1.30.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:e3d05914c364b8e39a5b10dcf97e84d76e516b3b1693880bf189a93aab3ca00d", size = 32459857, upload-time = "2025-05-21T13:32:27.728Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/20/e018cd87d7cb6f8684355f31f4e193222455a6e8f7b942f4a2934f5969c7/polars-1.30.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a52af3862082b868c1febeae650af8ae8a2105d2cb28f0449179a7b44f54ccf", size = 36267243, upload-time = "2025-05-21T13:32:31.796Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/e7/b88b973021be07b13d91b9301cc14392c994225ef5107a32a8ffd3fd6424/polars-1.30.0-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:ffb3ef133454275d4254442257c5f71dd6e393ce365c97997dadeb6fa9d6d4b5", size = 33416871, upload-time = "2025-05-21T13:32:35.077Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/7c/d46d4381adeac537b8520b653dc30cb8b7edbf59883d71fbb989e9005de1/polars-1.30.0-cp39-abi3-win_amd64.whl", hash = "sha256:c26b633a9bd530c5fc09d317fca3bb3e16c772bd7df7549a9d8ec1934773cc5d", size = 36363630, upload-time = "2025-05-21T13:32:38.286Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/b5/5056d0c12aadb57390d0627492bef8b1abf3549474abb9ae0fd4e2bfa885/polars-1.30.0-cp39-abi3-win_arm64.whl", hash = "sha256:476f1bde65bc7b4d9f80af370645c2981b5798d67c151055e58534e89e96f2a8", size = 32643590, upload-time = "2025-05-21T13:32:42.107Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyarrow"
|
||||||
|
version = "20.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a2/ee/a7810cb9f3d6e9238e61d312076a9859bf3668fd21c69744de9532383912/pyarrow-20.0.0.tar.gz", hash = "sha256:febc4a913592573c8d5805091a6c2b5064c8bd6e002131f01061797d91c783c1", size = 1125187, upload-time = "2025-04-27T12:34:23.264Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/aa/daa413b81446d20d4dad2944110dcf4cf4f4179ef7f685dd5a6d7570dc8e/pyarrow-20.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a15532e77b94c61efadde86d10957950392999503b3616b2ffcef7621a002893", size = 30798501, upload-time = "2025-04-27T12:30:48.351Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ff/75/2303d1caa410925de902d32ac215dc80a7ce7dd8dfe95358c165f2adf107/pyarrow-20.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:dd43f58037443af715f34f1322c782ec463a3c8a94a85fdb2d987ceb5658e061", size = 32277895, upload-time = "2025-04-27T12:30:55.238Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/41/fe18c7c0b38b20811b73d1bdd54b1fccba0dab0e51d2048878042d84afa8/pyarrow-20.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa0d288143a8585806e3cc7c39566407aab646fb9ece164609dac1cfff45f6ae", size = 41327322, upload-time = "2025-04-27T12:31:05.587Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/ab/7dbf3d11db67c72dbf36ae63dcbc9f30b866c153b3a22ef728523943eee6/pyarrow-20.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6953f0114f8d6f3d905d98e987d0924dabce59c3cda380bdfaa25a6201563b4", size = 42411441, upload-time = "2025-04-27T12:31:15.675Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/90/c3/0c7da7b6dac863af75b64e2f827e4742161128c350bfe7955b426484e226/pyarrow-20.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:991f85b48a8a5e839b2128590ce07611fae48a904cae6cab1f089c5955b57eb5", size = 40677027, upload-time = "2025-04-27T12:31:24.631Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/27/43a47fa0ff9053ab5203bb3faeec435d43c0d8bfa40179bfd076cdbd4e1c/pyarrow-20.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:97c8dc984ed09cb07d618d57d8d4b67a5100a30c3818c2fb0b04599f0da2de7b", size = 42281473, upload-time = "2025-04-27T12:31:31.311Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/0b/d56c63b078876da81bbb9ba695a596eabee9b085555ed12bf6eb3b7cab0e/pyarrow-20.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9b71daf534f4745818f96c214dbc1e6124d7daf059167330b610fc69b6f3d3e3", size = 42893897, upload-time = "2025-04-27T12:31:39.406Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/ac/7d4bd020ba9145f354012838692d48300c1b8fe5634bfda886abcada67ed/pyarrow-20.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8b88758f9303fa5a83d6c90e176714b2fd3852e776fc2d7e42a22dd6c2fb368", size = 44543847, upload-time = "2025-04-27T12:31:45.997Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/07/290f4abf9ca702c5df7b47739c1b2c83588641ddfa2cc75e34a301d42e55/pyarrow-20.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:30b3051b7975801c1e1d387e17c588d8ab05ced9b1e14eec57915f79869b5031", size = 25653219, upload-time = "2025-04-27T12:31:54.11Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/df/720bb17704b10bd69dde086e1400b8eefb8f58df3f8ac9cff6c425bf57f1/pyarrow-20.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:ca151afa4f9b7bc45bcc791eb9a89e90a9eb2772767d0b1e5389609c7d03db63", size = 30853957, upload-time = "2025-04-27T12:31:59.215Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/72/0d5f875efc31baef742ba55a00a25213a19ea64d7176e0fe001c5d8b6e9a/pyarrow-20.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:4680f01ecd86e0dd63e39eb5cd59ef9ff24a9d166db328679e36c108dc993d4c", size = 32247972, upload-time = "2025-04-27T12:32:05.369Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/bc/e48b4fa544d2eea72f7844180eb77f83f2030b84c8dad860f199f94307ed/pyarrow-20.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f4c8534e2ff059765647aa69b75d6543f9fef59e2cd4c6d18015192565d2b70", size = 41256434, upload-time = "2025-04-27T12:32:11.814Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/01/974043a29874aa2cf4f87fb07fd108828fc7362300265a2a64a94965e35b/pyarrow-20.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e1f8a47f4b4ae4c69c4d702cfbdfe4d41e18e5c7ef6f1bb1c50918c1e81c57b", size = 42353648, upload-time = "2025-04-27T12:32:20.766Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/95/cc0d3634cde9ca69b0e51cbe830d8915ea32dda2157560dda27ff3b3337b/pyarrow-20.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:a1f60dc14658efaa927f8214734f6a01a806d7690be4b3232ba526836d216122", size = 40619853, upload-time = "2025-04-27T12:32:28.1Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/c2/3ad40e07e96a3e74e7ed7cc8285aadfa84eb848a798c98ec0ad009eb6bcc/pyarrow-20.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:204a846dca751428991346976b914d6d2a82ae5b8316a6ed99789ebf976551e6", size = 42241743, upload-time = "2025-04-27T12:32:35.792Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/cb/65fa110b483339add6a9bc7b6373614166b14e20375d4daa73483755f830/pyarrow-20.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f3b117b922af5e4c6b9a9115825726cac7d8b1421c37c2b5e24fbacc8930612c", size = 42839441, upload-time = "2025-04-27T12:32:46.64Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/7b/f30b1954589243207d7a0fbc9997401044bf9a033eec78f6cb50da3f304a/pyarrow-20.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e724a3fd23ae5b9c010e7be857f4405ed5e679db5c93e66204db1a69f733936a", size = 44503279, upload-time = "2025-04-27T12:32:56.503Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/40/ad395740cd641869a13bcf60851296c89624662575621968dcfafabaa7f6/pyarrow-20.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:82f1ee5133bd8f49d31be1299dc07f585136679666b502540db854968576faf9", size = 25944982, upload-time = "2025-04-27T12:33:04.72Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "8.3.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "python-dotenv"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920, upload-time = "2025-03-25T10:14:56.835Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256, upload-time = "2025-03-25T10:14:55.034Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytz"
|
||||||
|
version = "2025.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests"
|
||||||
|
version = "2.32.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "charset-normalizer" },
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "retrying"
|
||||||
|
version = "1.3.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ce/70/15ce8551d65b324e18c5aa6ef6998880f21ead51ebe5ed743c0950d7d9dd/retrying-1.3.4.tar.gz", hash = "sha256:345da8c5765bd982b1d1915deb9102fd3d1f7ad16bd84a9700b85f64d24e8f3e", size = 10929, upload-time = "2022-11-25T09:57:49.43Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/04/9e36f28be4c0532c0e9207ff9dc01fb13a2b0eb036476a213b0000837d0e/retrying-1.3.4-py3-none-any.whl", hash = "sha256:8cc4d43cb8e1125e0ff3344e9de678fefd85db3b750b81b2240dc0183af37b35", size = 11602, upload-time = "2022-11-25T09:57:47.494Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff"
|
||||||
|
version = "0.11.12"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/15/0a/92416b159ec00cdf11e5882a9d80d29bf84bba3dbebc51c4898bfbca1da6/ruff-0.11.12.tar.gz", hash = "sha256:43cf7f69c7d7c7d7513b9d59c5d8cafd704e05944f978614aa9faff6ac202603", size = 4202289, upload-time = "2025-05-29T13:31:40.037Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/cc/53eb79f012d15e136d40a8e8fc519ba8f55a057f60b29c2df34efd47c6e3/ruff-0.11.12-py3-none-linux_armv6l.whl", hash = "sha256:c7680aa2f0d4c4f43353d1e72123955c7a2159b8646cd43402de6d4a3a25d7cc", size = 10285597, upload-time = "2025-05-29T13:30:57.539Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/d7/73386e9fb0232b015a23f62fea7503f96e29c29e6c45461d4a73bac74df9/ruff-0.11.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:2cad64843da9f134565c20bcc430642de897b8ea02e2e79e6e02a76b8dcad7c3", size = 11053154, upload-time = "2025-05-29T13:31:00.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/eb/3eae144c5114e92deb65a0cb2c72326c8469e14991e9bc3ec0349da1331c/ruff-0.11.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9b6886b524a1c659cee1758140138455d3c029783d1b9e643f3624a5ee0cb0aa", size = 10403048, upload-time = "2025-05-29T13:31:03.413Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/64/20c54b20e58b1058db6689e94731f2a22e9f7abab74e1a758dfba058b6ca/ruff-0.11.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc3a3690aad6e86c1958d3ec3c38c4594b6ecec75c1f531e84160bd827b2012", size = 10597062, upload-time = "2025-05-29T13:31:05.539Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/3a/79fa6a9a39422a400564ca7233a689a151f1039110f0bbbabcb38106883a/ruff-0.11.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f97fdbc2549f456c65b3b0048560d44ddd540db1f27c778a938371424b49fe4a", size = 10155152, upload-time = "2025-05-29T13:31:07.986Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/a4/22c2c97b2340aa968af3a39bc38045e78d36abd4ed3fa2bde91c31e712e3/ruff-0.11.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74adf84960236961090e2d1348c1a67d940fd12e811a33fb3d107df61eef8fc7", size = 11723067, upload-time = "2025-05-29T13:31:10.57Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/cf/3e452fbd9597bcd8058856ecd42b22751749d07935793a1856d988154151/ruff-0.11.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b56697e5b8bcf1d61293ccfe63873aba08fdbcbbba839fc046ec5926bdb25a3a", size = 12460807, upload-time = "2025-05-29T13:31:12.88Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/ec/8f170381a15e1eb7d93cb4feef8d17334d5a1eb33fee273aee5d1f8241a3/ruff-0.11.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d47afa45e7b0eaf5e5969c6b39cbd108be83910b5c74626247e366fd7a36a13", size = 12063261, upload-time = "2025-05-29T13:31:15.236Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0d/bf/57208f8c0a8153a14652a85f4116c0002148e83770d7a41f2e90b52d2b4e/ruff-0.11.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:692bf9603fe1bf949de8b09a2da896f05c01ed7a187f4a386cdba6760e7f61be", size = 11329601, upload-time = "2025-05-29T13:31:18.68Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/56/edf942f7fdac5888094d9ffa303f12096f1a93eb46570bcf5f14c0c70880/ruff-0.11.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08033320e979df3b20dba567c62f69c45e01df708b0f9c83912d7abd3e0801cd", size = 11522186, upload-time = "2025-05-29T13:31:21.216Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/63/79ffef65246911ed7e2290aeece48739d9603b3a35f9529fec0fc6c26400/ruff-0.11.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:929b7706584f5bfd61d67d5070f399057d07c70585fa8c4491d78ada452d3bef", size = 10449032, upload-time = "2025-05-29T13:31:23.417Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/19/8c9d4d8a1c2a3f5a1ea45a64b42593d50e28b8e038f1aafd65d6b43647f3/ruff-0.11.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7de4a73205dc5756b8e09ee3ed67c38312dce1aa28972b93150f5751199981b5", size = 10129370, upload-time = "2025-05-29T13:31:25.777Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/0f/2d15533eaa18f460530a857e1778900cd867ded67f16c85723569d54e410/ruff-0.11.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2635c2a90ac1b8ca9e93b70af59dfd1dd2026a40e2d6eebaa3efb0465dd9cf02", size = 11123529, upload-time = "2025-05-29T13:31:28.396Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/e2/4c2ac669534bdded835356813f48ea33cfb3a947dc47f270038364587088/ruff-0.11.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d05d6a78a89166f03f03a198ecc9d18779076ad0eec476819467acb401028c0c", size = 11577642, upload-time = "2025-05-29T13:31:30.647Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/9b/c9ddf7f924d5617a1c94a93ba595f4b24cb5bc50e98b94433ab3f7ad27e5/ruff-0.11.12-py3-none-win32.whl", hash = "sha256:f5a07f49767c4be4772d161bfc049c1f242db0cfe1bd976e0f0886732a4765d6", size = 10475511, upload-time = "2025-05-29T13:31:32.917Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/d6/74fb6d3470c1aada019ffff33c0f9210af746cca0a4de19a1f10ce54968a/ruff-0.11.12-py3-none-win_amd64.whl", hash = "sha256:5a4d9f8030d8c3a45df201d7fb3ed38d0219bccd7955268e863ee4a115fa0832", size = 11523573, upload-time = "2025-05-29T13:31:35.782Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/42/d58086ec20f52d2b0140752ae54b355ea2be2ed46f914231136dd1effcc7/ruff-0.11.12-py3-none-win_arm64.whl", hash = "sha256:65194e37853158d368e333ba282217941029a28ea90913c67e558c611d04daa5", size = 10697770, upload-time = "2025-05-29T13:31:38.009Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "setuptools"
|
||||||
|
version = "80.9.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "typing-extensions"
|
||||||
|
version = "4.13.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2025.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "werkzeug"
|
||||||
|
version = "3.0.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markupsafe" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d4/f9/0ba83eaa0df9b9e9d1efeb2ea351d0677c37d41ee5d0f91e98423c7281c9/werkzeug-3.0.6.tar.gz", hash = "sha256:a8dd59d4de28ca70471a34cba79bed5f7ef2e036a76b3ab0835474246eb41f8d", size = 805170, upload-time = "2024-10-25T18:52:31.688Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/69/05837f91dfe42109203ffa3e488214ff86a6d68b2ed6c167da6cdc42349b/werkzeug-3.0.6-py3-none-any.whl", hash = "sha256:1bc0c2310d2fbb07b1dd1105eba2f7af72f322e1e455f2f93c993bee8c8a5f17", size = 227979, upload-time = "2024-10-25T18:52:30.129Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zipp"
|
||||||
|
version = "3.22.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/12/b6/7b3d16792fdf94f146bed92be90b4eb4563569eca91513c8609aebf0c167/zipp-3.22.0.tar.gz", hash = "sha256:dd2f28c3ce4bc67507bfd3781d21b7bb2be31103b51a4553ad7d90b84e57ace5", size = 25257, upload-time = "2025-05-26T14:46:32.217Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/da/f64669af4cae46f17b90798a827519ce3737d31dbafad65d391e49643dc4/zipp-3.22.0-py3-none-any.whl", hash = "sha256:fe208f65f2aca48b81f9e6fd8cf7b8b32c26375266b009b413d45306b6148343", size = 9796, upload-time = "2025-05-26T14:46:30.775Z" },
|
||||||
|
]
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import polars as pl
|
||||||
|
import plotly.express as px
|
||||||
|
import pandas as pd
|
||||||
|
import logging
|
||||||
|
# import datetime
|
||||||
|
|
||||||
|
|
||||||
|
class data_components:
|
||||||
|
accounts = pl.read_parquet("data/warehouse/accounts.parquet")
|
||||||
|
categories = pl.read_parquet("data/warehouse/categories.parquet")
|
||||||
|
dates = pl.read_parquet("data/warehouse/dates.parquet")
|
||||||
|
payees = pl.read_parquet("data/warehouse/payees.parquet")
|
||||||
|
scheduled_transactions = pl.read_parquet(
|
||||||
|
"data/warehouse/scheduled_transactions.parquet"
|
||||||
|
)
|
||||||
|
transactions = pl.read_parquet("data/warehouse/transactions.parquet")
|
||||||
|
|
||||||
|
master_transactions = (
|
||||||
|
transactions.join(
|
||||||
|
categories,
|
||||||
|
left_on="category_id",
|
||||||
|
right_on="category_id",
|
||||||
|
suffix="_category",
|
||||||
|
)
|
||||||
|
.join(accounts, left_on="account_id", right_on="account_id", suffix="_account")
|
||||||
|
.join(payees, left_on="payee_id", right_on="payee_id", suffix="_payee")
|
||||||
|
.join(dates, left_on="transaction_date", right_on="date_id", suffix="_date")
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
logging.info("Initializing data components")
|
||||||
|
pass
|
||||||
|
|
||||||
|
def update_dates(start_date, end_date):
|
||||||
|
logging.info("Updating dates")
|
||||||
|
logging.debug(f"start_date: {start_date}, end_date: {end_date}")
|
||||||
|
logging.debug(data_components.master_transactions.columns)
|
||||||
|
try:
|
||||||
|
master_data = data_components.master_transactions.filter(
|
||||||
|
pl.col("date").is_between(start_date, end_date)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error updating dates: {e}")
|
||||||
|
raise e
|
||||||
|
return master_data
|
||||||
|
|
||||||
|
def update_data(master_data, callback=0):
|
||||||
|
# Create aggregations
|
||||||
|
spend_per_day = master_data.sql("""
|
||||||
|
SELECT
|
||||||
|
date,
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day,
|
||||||
|
ABS(SUM(transaction_amount)) as total
|
||||||
|
FROM self
|
||||||
|
WHERE category_name != 'Inflow: Ready to Assign'
|
||||||
|
GROUP BY date, year, month, day
|
||||||
|
ORDER BY date DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
spend_per_category = master_data.sql("""
|
||||||
|
SELECT
|
||||||
|
category_name,
|
||||||
|
ABS(SUM(transaction_amount)) as total
|
||||||
|
FROM self
|
||||||
|
WHERE category_name != 'Inflow: Ready to Assign'
|
||||||
|
GROUP BY category_name
|
||||||
|
ORDER BY total DESC
|
||||||
|
""")
|
||||||
|
|
||||||
|
spend_per_payee = master_data.sql("""
|
||||||
|
SELECT
|
||||||
|
payee_name,
|
||||||
|
ABS(SUM(transaction_amount)) as total
|
||||||
|
FROM self
|
||||||
|
WHERE payee_name != 'Starting Balance'
|
||||||
|
AND transaction_amount < 0
|
||||||
|
GROUP BY payee_name
|
||||||
|
ORDER BY total DESC
|
||||||
|
""")
|
||||||
|
total_spend = master_data.sql("""
|
||||||
|
SELECT ABS(SUM(transaction_amount)) AS total
|
||||||
|
FROM self
|
||||||
|
WHERE payee_name != 'Starting Balance'
|
||||||
|
AND transaction_amount < 0
|
||||||
|
""").item()
|
||||||
|
|
||||||
|
# Convert DataFrame to list of dictionaries
|
||||||
|
spend_per_day_data = spend_per_day.to_dicts()
|
||||||
|
spend_per_category_data = spend_per_category.to_dicts()
|
||||||
|
spend_per_payee_data = spend_per_payee.to_dicts()
|
||||||
|
|
||||||
|
# Convert list of dictionaries to Pandas DataFrame
|
||||||
|
spend_per_day_df = pd.DataFrame(spend_per_day_data)
|
||||||
|
spend_per_category_df = pd.DataFrame(spend_per_category_data)
|
||||||
|
spend_per_payee_df = pd.DataFrame(spend_per_payee_data)
|
||||||
|
|
||||||
|
spend_per_day_line = px.line(spend_per_day_df, x="date", y="total")
|
||||||
|
spend_per_day_line.update_layout(
|
||||||
|
plot_bgcolor="black", paper_bgcolor="black", font_color="white"
|
||||||
|
)
|
||||||
|
|
||||||
|
spend_per_category_bar = px.bar(
|
||||||
|
spend_per_category_df, x="category_name", y="total"
|
||||||
|
)
|
||||||
|
spend_per_category_bar.update_layout(
|
||||||
|
plot_bgcolor="black", paper_bgcolor="black", font_color="white"
|
||||||
|
)
|
||||||
|
|
||||||
|
spend_per_payee_bar = px.bar(spend_per_payee_df, x="payee_name", y="total")
|
||||||
|
spend_per_payee_bar.update_layout(
|
||||||
|
plot_bgcolor="black", paper_bgcolor="black", font_color="white"
|
||||||
|
)
|
||||||
|
|
||||||
|
total_spend_line = f"### £{total_spend:,.2f}"
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"spend_per_day_line": spend_per_day_line,
|
||||||
|
"spend_per_category_bar": spend_per_category_bar,
|
||||||
|
"spend_per_payee_bar": spend_per_payee_bar,
|
||||||
|
"total_spend": total_spend_line,
|
||||||
|
}
|
||||||
|
|
||||||
|
if callback == 0:
|
||||||
|
return data
|
||||||
|
else:
|
||||||
|
return (
|
||||||
|
spend_per_day_line,
|
||||||
|
spend_per_category_bar,
|
||||||
|
spend_per_payee_bar,
|
||||||
|
total_spend,
|
||||||
|
)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
'''Module to create a Dash app that displays visualizations of YNAB data.'''
|
||||||
|
import dash
|
||||||
|
from dash.dependencies import Input, Output
|
||||||
|
import dash_bootstrap_components as dbc
|
||||||
|
from visuals.layout import create_layout
|
||||||
|
from visuals.components import data_components
|
||||||
|
from datetime import date, timedelta
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
today = date.today()
|
||||||
|
one_year_ago = today - timedelta(days=365)
|
||||||
|
|
||||||
|
master_data = data_components.update_dates(start_date=one_year_ago, end_date=today)
|
||||||
|
data = data_components.update_data(master_data)
|
||||||
|
|
||||||
|
|
||||||
|
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.DARKLY])
|
||||||
|
|
||||||
|
@app.callback(
|
||||||
|
Output("spend_per_day","figure"),
|
||||||
|
Output("spend_per_category","figure"),
|
||||||
|
Output("spend_per_payee","figure"),
|
||||||
|
Output("total_spend","children"),
|
||||||
|
|
||||||
|
Input('date-picker-range', 'start_date'),
|
||||||
|
Input('date-picker-range', 'end_date')
|
||||||
|
)
|
||||||
|
def update_date_range(start_date,end_date):
|
||||||
|
actual_start_date = datetime.date.fromisoformat(start_date)
|
||||||
|
actual_end_date = datetime.date.fromisoformat(end_date)
|
||||||
|
master_data = data_components.update_dates(actual_start_date,actual_end_date)
|
||||||
|
return data_components.update_data(master_data,callback=1)
|
||||||
|
|
||||||
|
app.layout = create_layout(data)
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
from dash import html, dcc
|
||||||
|
from datetime import date
|
||||||
|
import dash_bootstrap_components as dbc
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
today = date.today()
|
||||||
|
one_year_ago = today - timedelta(days=365)
|
||||||
|
|
||||||
|
|
||||||
|
def create_layout(data):
|
||||||
|
main_body = create_main_body(data)
|
||||||
|
topbar = create_topbar()
|
||||||
|
|
||||||
|
return [html.Div(topbar + main_body)]
|
||||||
|
|
||||||
|
|
||||||
|
def create_topbar():
|
||||||
|
return [
|
||||||
|
dbc.Container(
|
||||||
|
[
|
||||||
|
dbc.Row(
|
||||||
|
dbc.Col(
|
||||||
|
html.Div(
|
||||||
|
html.H1("Data Pipeline For YNAB, Preview Visualisations"),
|
||||||
|
className="text-center text-light",
|
||||||
|
),
|
||||||
|
width=12,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
dbc.Row(
|
||||||
|
[
|
||||||
|
dbc.Col(
|
||||||
|
dcc.DatePickerRange(
|
||||||
|
first_day_of_week=1,
|
||||||
|
display_format="YYYY-MM-DD",
|
||||||
|
id="date-picker-range",
|
||||||
|
start_date=one_year_ago,
|
||||||
|
end_date=today,
|
||||||
|
),
|
||||||
|
width=4,
|
||||||
|
),
|
||||||
|
dbc.Col(
|
||||||
|
html.Button(
|
||||||
|
"Change Date Range", id="date-range-confirm-button"
|
||||||
|
),
|
||||||
|
width=2,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def create_main_body(data):
|
||||||
|
return [
|
||||||
|
dbc.Container(
|
||||||
|
[
|
||||||
|
dbc.Row(
|
||||||
|
[
|
||||||
|
dbc.Col(
|
||||||
|
dbc.Card(
|
||||||
|
dbc.CardBody(
|
||||||
|
[
|
||||||
|
html.H4(
|
||||||
|
"Spend Per Day", className="card-title"
|
||||||
|
),
|
||||||
|
dcc.Graph(
|
||||||
|
figure=data["spend_per_day_line"],
|
||||||
|
id="spend_per_day",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
className="mb-4",
|
||||||
|
),
|
||||||
|
width=12,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
dbc.Row(
|
||||||
|
[
|
||||||
|
dbc.Col(
|
||||||
|
dbc.Card(
|
||||||
|
dbc.CardBody(
|
||||||
|
[
|
||||||
|
html.H4(
|
||||||
|
"Spend Per Category", className="card-title"
|
||||||
|
),
|
||||||
|
dcc.Graph(
|
||||||
|
figure=data["spend_per_category_bar"],
|
||||||
|
id="spend_per_category",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
className="mb-4",
|
||||||
|
),
|
||||||
|
width=5,
|
||||||
|
),
|
||||||
|
dbc.Col(
|
||||||
|
dbc.Card(
|
||||||
|
dbc.CardBody(
|
||||||
|
[
|
||||||
|
dcc.Markdown("## Total Spend:"),
|
||||||
|
dcc.Markdown(
|
||||||
|
data["total_spend"], id="total_spend"
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
className="text-center text-light",
|
||||||
|
),
|
||||||
|
width=2,
|
||||||
|
),
|
||||||
|
dbc.Col(
|
||||||
|
dbc.Card(
|
||||||
|
dbc.CardBody(
|
||||||
|
[
|
||||||
|
html.H4(
|
||||||
|
"Spend Per Payee", className="card-title"
|
||||||
|
),
|
||||||
|
dcc.Graph(
|
||||||
|
figure=data["spend_per_payee_bar"],
|
||||||
|
id="spend_per_payee",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
className="mb-4",
|
||||||
|
),
|
||||||
|
width=5,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
],
|
||||||
|
fluid=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user