From 73b997b7bae9562f04433187dceb6186bc6a7248 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Sat, 28 Dec 2024 23:54:54 +0100 Subject: [PATCH 1/9] Fix issue with data sources parameters passing --- README.md | 79 +++---- investing_algorithm_framework/__init__.py | 11 +- investing_algorithm_framework/app/app.py | 16 +- .../domain/__init__.py | 6 +- .../models/backtesting/backtest_report.py | 22 +- .../domain/models/base_model.py | 6 + .../domain/models/position/position.py | 3 + .../domain/models/time_unit.py | 4 +- .../domain/models/trade/trade.py | 21 ++ .../domain/utils/__init__.py | 6 +- .../domain/utils/backtesting.py | 192 ++++++++++++++++-- .../indicators/__init__.py | 7 +- .../indicators/trend.py | 10 +- .../indicators/utils.py | 144 +++++++++++-- .../models/market_data_sources/ccxt.py | 53 +++-- .../market_service/ccxt_market_service.py | 6 +- tests/indicators/test_utils.py | 120 ++++++++++- .../test_ccxt_ohlcv_market_data_source.py | 62 +++++- 18 files changed, 652 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index 63d038ad..36890816 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ The Investing Algorithm Framework is a Python framework that enables swift and e Features: -* Indicators module: A collection of indicators and utility functions that can be used in your trading strategies. +* Indicators module: A collection of indicators and utility functions that can be used in your trading strategies. * Order execution and tracking * Broker and exchange connections through [ccxt](https://github.com/ccxt/ccxt) * Backtesting and performance analysis reports [example](./examples/backtest_example) @@ -41,8 +41,8 @@ from investing_algorithm_framework import create_app, PortfolioConfiguration, \ RESOURCE_DIRECTORY, TimeUnit, CCXTOHLCVMarketDataSource, Algorithm, \ CCXTTickerMarketDataSource, MarketCredential, SYMBOLS -# Define the symbols you want to trade for optimization, otherwise the -# algorithm will check if you have orders and balances on all available +# Define the symbols you want to trade for optimization, otherwise the +# algorithm will check if you have orders and balances on all available # symbols on the market symbols = ["BTC/EUR"] @@ -52,6 +52,13 @@ config = { SYMBOLS: symbols } +state_manager = AzureBlobStorageStateManager( + account_name="", + account_key="", + container_name="", + blob_name="", +) + # Define market data sources # OHLCV data for candles bitvavo_btc_eur_ohlcv_2h = CCXTOHLCVMarketDataSource( @@ -67,7 +74,11 @@ bitvavo_btc_eur_ticker = CCXTTickerMarketDataSource( market="BITVAVO", symbol="BTC/EUR", ) -app = create_app(config=config) +app = create_app( + config=config, + sync_portfolio=True, + state_manager=state_manager +) algorithm = Algorithm() app.add_market_credential(MarketCredential( market="bitvavo", @@ -85,16 +96,16 @@ app.add_algorithm(algorithm) @algorithm.strategy( # Run every two hours - time_unit=TimeUnit.HOUR, - interval=2, + time_unit=TimeUnit.HOUR, + interval=2, # Specify market data sources that need to be passed to the strategy market_data_sources=[bitvavo_btc_eur_ticker, bitvavo_btc_eur_ohlcv_2h] ) def perform_strategy(algorithm: Algorithm, market_data: dict): # By default, ohlcv data is passed as polars df in the form of - # {"": } https://pola.rs/, + # {"": } https://pola.rs/, # call to_pandas() to convert to pandas - polars_df = market_data["BTC-ohlcv"] + polars_df = market_data["BTC-ohlcv"] print(f"I have access to {len(polars_df)} candles of ohlcv data") # Ticker data is passed as {"": } @@ -104,21 +115,21 @@ def perform_strategy(algorithm: Algorithm, market_data: dict): trades = algorithm.get_trades() open_trades = algorithm.get_open_trades() closed_trades = algorithm.get_closed_trades() - - # Create a buy oder + + # Create a buy oder algorithm.create_limit_order( target_symbol="BTC/EUR", order_side="buy", amount=0.01, price=ticker_data["ask"], ) - + # Close a trade algorithm.close_trade(trades[0].id) - + # Close a position algorithm.close_position(positions[0].get_symbol()) - + if __name__ == "__main__": app.run() ``` @@ -136,14 +147,14 @@ To run a single backtest you can use the example code that can be found [here](. You can use the ```pretty_print_backtest``` function to print a backtest report. For example if you run the [moving average example trading bot](./examples/crossover_moving_average_trading_bot) you will get the following backtesting report: - + ```bash :%%%#+- .=*#%%% Backtest report *%%%%%%%+------=*%%%%%%%- --------------------------- *%%%%%%%%%%%%%%%%%%%%%%%- Start date: 2023-08-24 00:00:00 .%%%%%%%%%%%%%%%%%%%%%%# End date: 2023-12-02 00:00:00 - #%%%####%%%%%%%%**#%%%+ Number of days: 100 + #%%%####%%%%%%%%**#%%%+ Number of days: 100 .:-+*%%%%- -+..#%%%+.+- +%%%#*=-: Number of runs: 1201 .:-=*%%%%. += .%%# -+.-%%%%=-:.. Number of orders: 40 .:=+#%%%%%*###%%%%#*+#%%%%%%*+-: Initial balance: 400.0 @@ -156,10 +167,10 @@ you will get the following backtesting report: .++- -%%%%%%%%%%%+= Percentage negative trades: 70.0% .++- .%%%%%%%%%%%%%+= Average trade size: 100.9692 EUR .++- *%%%%%%%%%%%%%*+: Average trade duration: 83.6 hours - .++- %%%%%%%%%%%%%%#+= - =++........:::%%%%%%%%%%%%%%*+- - .=++++++++++**#%%%%%%%%%%%%%++. - + .++- %%%%%%%%%%%%%%#+= + =++........:::%%%%%%%%%%%%%%*+- + .=++++++++++**#%%%%%%%%%%%%%++. + Price noise Positions overview @@ -220,8 +231,8 @@ Trades overview ### Backtest experiments -The framework also supports backtest experiments. Backtest experiments allows you to -compare multiple algorithms and evaluate their performance. Ideally, +The framework also supports backtest experiments. Backtest experiments allows you to +compare multiple algorithms and evaluate their performance. Ideally, you would do this by parameterizing your strategy and creating a factory function that creates the algorithm with the different parameters. You can find an example of this in the [backtest experiments example](./examples/backtest_experiment). @@ -237,14 +248,14 @@ from investing_algorithm_framework import PortfolioConfiguration, \ app = create_app() app.add_market_credential( MarketCredential( - market="", + market="", api_key="", secret_key="", ) ) app.add_portfolio_configuration( PortfolioConfiguration( - market="", + market="", initial_balance=400, track_from="01/01/2022", trading_symbol="EUR" @@ -267,27 +278,27 @@ pip install investing-algorithm-framework ## Disclaimer -If you use this framework for your investments, do not risk money -which you are afraid to lose, until you have clear understanding how +If you use this framework for your investments, do not risk money +which you are afraid to lose, until you have clear understanding how the framework works. We can't stress this enough: -BEFORE YOU START USING MONEY WITH THE FRAMEWORK, MAKE SURE THAT YOU TESTED -YOUR COMPONENTS THOROUGHLY. USE THE SOFTWARE AT YOUR OWN RISK. +BEFORE YOU START USING MONEY WITH THE FRAMEWORK, MAKE SURE THAT YOU TESTED +YOUR COMPONENTS THOROUGHLY. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR INVESTMENT RESULTS. -Also, make sure that you read the source code of any plugin you use or +Also, make sure that you read the source code of any plugin you use or implementation of an algorithm made with this framework. ## Documentation -All the documentation can be found online +All the documentation can be found online at the [documentation webstie](https://investing-algorithm-framework.com) -In most cases, you'll probably never have to change code on this repo directly -if you are building your algorithm/bot. But if you do, check out the +In most cases, you'll probably never have to change code on this repo directly +if you are building your algorithm/bot. But if you do, check out the contributing page at the website. -If you'd like to chat with investing-algorithm-framework users +If you'd like to chat with investing-algorithm-framework users and developers, [join us on Slack](https://inv-algo-framework.slack.com) or [join us on reddit](https://www.reddit.com/r/InvestingBots/) ## Acknowledgements @@ -302,12 +313,12 @@ first. If it hasn't been reported, please [create a new issue](https://github.co ### Contributing -The investing algorithm framework is a community driven project. +The investing algorithm framework is a community driven project. We welcome you to participate, contribute and together help build the future trading bots developed in python. Feel like the framework is missing a feature? We welcome your pull requests! If you want to contribute to the project roadmap, please take a look at the [project board](https://github.com/coding-kitties/investing-algorithm-framework/projects?query=is%3Aopen). -You can pick up a task by assigning yourself to it. +You can pick up a task by assigning yourself to it. **Note** before starting any major new feature work, *please open an issue describing what you are planning to do*. This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. diff --git a/investing_algorithm_framework/__init__.py b/investing_algorithm_framework/__init__.py index 0f2ab539..1ec300a6 100644 --- a/investing_algorithm_framework/__init__.py +++ b/investing_algorithm_framework/__init__.py @@ -11,12 +11,17 @@ pretty_print_backtest_reports_evaluation, load_backtest_reports, \ RESERVED_BALANCES, APP_MODE, AppMode, DATETIME_FORMAT, \ load_backtest_report, BacktestDateRange, convert_polars_to_pandas, \ - DateRange + DateRange, get_backtest_report from investing_algorithm_framework.infrastructure import \ CCXTOrderBookMarketDataSource, CCXTOHLCVMarketDataSource, \ CCXTTickerMarketDataSource, CSVOHLCVMarketDataSource, \ CSVTickerMarketDataSource from .create_app import create_app +from investing_algorithm_framework.indicators import get_rsi, get_peaks, \ + is_uptrend, is_downtrend, is_crossover, is_crossunder, is_above, \ + is_below, has_crossed_upward, get_sma, get_up_and_downtrends, \ + get_rsi, get_ema, get_adx, has_crossed_downward, get_willr, \ + is_divergence __all__ = [ "Algorithm", @@ -83,6 +88,6 @@ "get_adx", "has_crossed_downward", "get_willr", - "is_bearish_divergence", - "is_bullish_divergence", + "is_divergence", + "get_backtest_report" ] diff --git a/investing_algorithm_framework/app/app.py b/investing_algorithm_framework/app/app.py index 4f65d07c..889f4dc2 100644 --- a/investing_algorithm_framework/app/app.py +++ b/investing_algorithm_framework/app/app.py @@ -91,7 +91,10 @@ def initialize(self, sync=False): Also, it initializes all required services for the algorithm. - :return: None + Args: + sync (bool): Whether to sync the portfolio with the exchange + Returns: + None """ if self.algorithm is None: raise OperationalException("No algorithm registered") @@ -162,8 +165,8 @@ def initialize(self, sync=False): .create_portfolio_from_configuration( portfolio_configuration ) - self.sync(portfolio) - synced_portfolios.append(portfolio) + # self.sync(portfolio) + # synced_portfolios.append(portfolio) if sync: portfolios = portfolio_service.get_all() @@ -494,16 +497,15 @@ def run( separate thread. Args: - payload: The payload to handle if the app is running in + payload (dict): The payload to handle if the app is running in stateless mode - number_of_iterations: The number of iterations to run the + number_of_iterations (int): The number of iterations to run the algorithm for - sync: Whether to sync the portfolio with the exchange + sync (bool): Whether to sync the portfolio with the exchange Returns: None """ - # Run all on_initialize hooks for hook in self._on_after_initialize_hooks: hook.on_run(self, self.algorithm) diff --git a/investing_algorithm_framework/domain/__init__.py b/investing_algorithm_framework/domain/__init__.py index 68b294b0..5257bff8 100644 --- a/investing_algorithm_framework/domain/__init__.py +++ b/investing_algorithm_framework/domain/__init__.py @@ -29,7 +29,8 @@ add_column_headers_to_csv, get_total_amount_of_rows, \ load_backtest_report, convert_polars_to_pandas, \ csv_to_list, StoppableThread, pretty_print_backtest_reports_evaluation, \ - pretty_print_backtest, load_csv_into_dict, load_backtest_reports + pretty_print_backtest, load_csv_into_dict, load_backtest_reports, \ + get_backtest_report from .metrics import get_price_efficiency_ratio __all__ = [ @@ -117,5 +118,6 @@ "load_backtest_report", "get_price_efficiency_ratio", "convert_polars_to_pandas", - "DateRange" + "DateRange", + "get_backtest_report" ] diff --git a/investing_algorithm_framework/domain/models/backtesting/backtest_report.py b/investing_algorithm_framework/domain/models/backtesting/backtest_report.py index 58f1b47c..15c8f695 100644 --- a/investing_algorithm_framework/domain/models/backtesting/backtest_report.py +++ b/investing_algorithm_framework/domain/models/backtesting/backtest_report.py @@ -10,6 +10,9 @@ .backtesting.backtest_date_range import BacktestDateRange from investing_algorithm_framework.domain.models.base_model import BaseModel from investing_algorithm_framework.domain.models.time_unit import TimeUnit +from investing_algorithm_framework.domain.models.position import Position +from investing_algorithm_framework.domain.models.trade import Trade +from investing_algorithm_framework.domain.models.order import Order logger = getLogger(__name__) @@ -451,7 +454,7 @@ def from_dict(data): data["backtest_end_date"], DATETIME_FORMAT) ) - return BacktestReport( + report = BacktestReport( name=data["name"], strategy_identifiers=data["strategy_identifiers"], number_of_runs=data["number_of_runs"], @@ -477,6 +480,23 @@ def from_dict(data): average_trade_size=float(data["average_trade_size"]), ) + positions = data["positions"] + + if positions is not None: + report.positions = [Position.from_dict(position) for position in positions] + + trades = data["trades"] + + if trades is not None: + report.trades = [Trade.from_dict(trade) for trade in trades] + + orders = data["orders"] + + if orders is not None: + report.orders = [Order.from_dict(order) for order in orders] + + return report + def get_trades(self, symbol=None): """ Function to get trades. If a symbol is provided, it will diff --git a/investing_algorithm_framework/domain/models/base_model.py b/investing_algorithm_framework/domain/models/base_model.py index cea570bc..ea273254 100644 --- a/investing_algorithm_framework/domain/models/base_model.py +++ b/investing_algorithm_framework/domain/models/base_model.py @@ -23,3 +23,9 @@ def update(self, data): if value is not None: setattr(self, attr, value) + + @staticmethod + def from_dict(data): + instance = BaseModel() + instance.update(data) + return instance diff --git a/investing_algorithm_framework/domain/models/position/position.py b/investing_algorithm_framework/domain/models/position/position.py index 371c9b58..c9a6481c 100644 --- a/investing_algorithm_framework/domain/models/position/position.py +++ b/investing_algorithm_framework/domain/models/position/position.py @@ -2,6 +2,9 @@ class Position(BaseModel): + """ + This class represents a position in a portfolio. + """ def __init__( self, diff --git a/investing_algorithm_framework/domain/models/time_unit.py b/investing_algorithm_framework/domain/models/time_unit.py index 5ba35b4b..8bf5b42b 100644 --- a/investing_algorithm_framework/domain/models/time_unit.py +++ b/investing_algorithm_framework/domain/models/time_unit.py @@ -19,7 +19,9 @@ def from_string(value: str): return entry raise ValueError( - f"Could not convert value {value} to time unit" + f"Could not convert value {value} to time unit," + + " please make sure that the value is either of type string or" + + f"TimeUnit. Its current type is {type(value)}" ) def equals(self, other): diff --git a/investing_algorithm_framework/domain/models/trade/trade.py b/investing_algorithm_framework/domain/models/trade/trade.py index 33170507..44fd2225 100644 --- a/investing_algorithm_framework/domain/models/trade/trade.py +++ b/investing_algorithm_framework/domain/models/trade/trade.py @@ -232,6 +232,8 @@ def is_manual_stop_loss_trigger( def to_dict(self): return { + "buy_order_id": self.buy_order_id, + "sell_order_id": self.sell_order_id, "target_symbol": self.target_symbol, "trading_symbol": self.trading_symbol, "status": self.status, @@ -247,6 +249,25 @@ def to_dict(self): "absolute_change": self.absolute_change, } + @staticmethod + def from_dict(data): + return Trade( + buy_order_id=data["buy_order_id"] if "buy_order_id" in data else None, + sell_order_id=data["sell_order_id"] if "sell_order_id" in data else None, + target_symbol=data["target_symbol"], + trading_symbol=data["trading_symbol"], + amount=data["amount"], + open_price=data["open_price"], + opened_at=datetime.strptime( + data["opened_at"], DATETIME_FORMAT + ), + closed_price=data["closed_price"], + closed_at=datetime.strptime( + data["closed_at"], DATETIME_FORMAT + ) if data["closed_at"] else None, + current_price=data["current_price"], + ) + def __repr__(self): return self.repr( target_symbol=self.target_symbol, diff --git a/investing_algorithm_framework/domain/utils/__init__.py b/investing_algorithm_framework/domain/utils/__init__.py index 77b76673..0459913d 100644 --- a/investing_algorithm_framework/domain/utils/__init__.py +++ b/investing_algorithm_framework/domain/utils/__init__.py @@ -1,5 +1,6 @@ from .backtesting import pretty_print_backtest, load_backtest_report, \ - pretty_print_backtest_reports_evaluation, load_backtest_reports + pretty_print_backtest_reports_evaluation, load_backtest_reports, \ + get_backtest_report from .csv import get_total_amount_of_rows, append_dict_as_row_to_csv, \ add_column_headers_to_csv, csv_to_list, load_csv_into_dict from .random import random_string @@ -20,5 +21,6 @@ 'load_csv_into_dict', 'load_backtest_report', 'load_backtest_reports', - 'convert_polars_to_pandas' + 'convert_polars_to_pandas', + 'get_backtest_report' ] diff --git a/investing_algorithm_framework/domain/utils/backtesting.py b/investing_algorithm_framework/domain/utils/backtesting.py index bc6c67e0..57a379aa 100644 --- a/investing_algorithm_framework/domain/utils/backtesting.py +++ b/investing_algorithm_framework/domain/utils/backtesting.py @@ -1,4 +1,5 @@ import os +import re import json from datetime import datetime from typing import List, Tuple @@ -11,13 +12,19 @@ OperationalException from investing_algorithm_framework.domain.models.backtesting import \ BacktestReportsEvaluation, BacktestReport +from investing_algorithm_framework.domain.constants import \ + DATETIME_FORMAT_BACKTESTING COLOR_RED = '\033[91m' COLOR_PURPLE = '\033[95m' COLOR_RESET = '\033[0m' COLOR_GREEN = '\033[92m' COLOR_YELLOW = '\033[93m' - +BACKTEST_REPORT_FILE_NAME_PATTERN = ( + r"^report_\w+_backtest-start-date_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}_" + r"backtest-end-date_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}_" + r"created-at_\d{4}-\d{2}-\d{2}:\d{2}:\d{2}\.json$" +) def is_positive(number) -> bool: """ @@ -261,23 +268,23 @@ def pretty_print_backtest_reports_evaluation( :%%%#+- .=*#%%% {COLOR_GREEN}Backtest reports evaluation{COLOR_RESET} *%%%%%%%+------=*%%%%%%%- {COLOR_GREEN}---------------------------{COLOR_RESET} *%%%%%%%%%%%%%%%%%%%%%%%- {COLOR_YELLOW}Number of reports:{COLOR_RESET} {COLOR_GREEN}{number_of_backtest_reports} backtest reports{COLOR_RESET} - .%%%%%%%%%%%%%%%%%%%%%%# {COLOR_YELLOW}Largest overall profit:{COLOR_RESET}{COLOR_GREEN}{COLOR_RESET}{COLOR_GREEN} (Algorithm {most_profitable.name}) {most_profitable.total_net_gain:.{precision}f} {most_profitable.trading_symbol} {most_profitable.total_net_gain_percentage:.{precision}f}% ({most_profitable.backtest_date_range.name} {most_profitable.backtest_date_range.start_date} - {most_profitable.backtest_date_range.end_date}){COLOR_RESET} + .%%%%%%%%%%%%%%%%%%%%%%# {COLOR_YELLOW}Largest overall profit:{COLOR_RESET}{COLOR_GREEN}{COLOR_RESET}{COLOR_GREEN} (Algorithm {most_profitable.name}) {most_profitable.total_net_gain:.{precision}f} {most_profitable.trading_symbol} {most_profitable.total_net_gain_percentage:.{precision}f}% ({most_profitable.backtest_date_range.name} {most_profitable.backtest_date_range.start_date} - {most_profitable.backtest_date_range.end_date}){COLOR_RESET} #%%%####%%%%%%%%**#%%%+ {COLOR_YELLOW}Largest overall growth:{COLOR_RESET}{COLOR_GREEN} (Algorithm {most_profitable.name}) {most_growth.growth:.{precision}f} {most_growth.trading_symbol} {most_growth.growth_rate:.{precision}f}% ({most_growth.backtest_date_range.name} {most_growth.backtest_date_range.start_date} - {most_growth.backtest_date_range.end_date}){COLOR_RESET} - .:-+*%%%%- {COLOR_PURPLE}-+..#{COLOR_RESET}%%%+.{COLOR_PURPLE}+- +{COLOR_RESET}%%%#*=-: - .:-=*%%%%. {COLOR_PURPLE}+={COLOR_RESET} .%%# {COLOR_PURPLE}-+.-{COLOR_RESET}%%%%=-:.. - .:=+#%%%%%*###%%%%#*+#%%%%%%*+-: - +%%%%%%%%%%%%%%%%%%%= - :++ .=#%%%%%%%%%%%%%*- - :++: :+%%%%%%#-. - :++: .%%%%%#= - :++: .#%%%%%#*= - :++- :%%%%%%%%%+= - .++- -%%%%%%%%%%%+= - .++- .%%%%%%%%%%%%%+= - .++- *%%%%%%%%%%%%%*+: - .++- %%%%%%%%%%%%%%#+= - =++........:::%%%%%%%%%%%%%%*+- - .=++++++++++**#%%%%%%%%%%%%%++. + .:-+*%%%%- {COLOR_PURPLE}-+..#{COLOR_RESET}%%%+.{COLOR_PURPLE}+- +{COLOR_RESET}%%%#*=-: + .:-=*%%%%. {COLOR_PURPLE}+={COLOR_RESET} .%%# {COLOR_PURPLE}-+.-{COLOR_RESET}%%%%=-:.. + .:=+#%%%%%*###%%%%#*+#%%%%%%*+-: + +%%%%%%%%%%%%%%%%%%%= + :++ .=#%%%%%%%%%%%%%*- + :++: :+%%%%%%#-. + :++: .%%%%%#= + :++: .#%%%%%#*= + :++- :%%%%%%%%%+= + .++- -%%%%%%%%%%%+= + .++- .%%%%%%%%%%%%%+= + .++- *%%%%%%%%%%%%%*+: + .++- %%%%%%%%%%%%%%#+= + =++........:::%%%%%%%%%%%%%%*+- + .=++++++++++**#%%%%%%%%%%%%%++. """ if len(backtest_reports_evaluation.backtest_reports) == 0: @@ -326,7 +333,7 @@ def pretty_print_backtest( *%%%%%%%+------=*%%%%%%%- {COLOR_GREEN}---------------------------{COLOR_RESET} *%%%%%%%%%%%%%%%%%%%%%%%- {COLOR_YELLOW}Start date:{COLOR_RESET}{COLOR_GREEN} {backtest_report.backtest_start_date}{COLOR_RESET} .%%%%%%%%%%%%%%%%%%%%%%# {COLOR_YELLOW}End date:{COLOR_RESET}{COLOR_GREEN} {backtest_report.backtest_end_date}{COLOR_RESET} - #%%%####%%%%%%%%**#%%%+ {COLOR_YELLOW}Number of days:{COLOR_RESET}{COLOR_GREEN}{COLOR_RESET}{COLOR_GREEN} {backtest_report.number_of_days}{COLOR_RESET} + #%%%####%%%%%%%%**#%%%+ {COLOR_YELLOW}Number of days:{COLOR_RESET}{COLOR_GREEN}{COLOR_RESET}{COLOR_GREEN} {backtest_report.number_of_days}{COLOR_RESET} .:-+*%%%%- {COLOR_PURPLE}-+..#{COLOR_RESET}%%%+.{COLOR_PURPLE}+- +{COLOR_RESET}%%%#*=-: {COLOR_YELLOW}Number of runs:{COLOR_RESET}{COLOR_GREEN} {backtest_report.number_of_runs}{COLOR_RESET} .:-=*%%%%. {COLOR_PURPLE}+={COLOR_RESET} .%%# {COLOR_PURPLE}-+.-{COLOR_RESET}%%%%=-:.. {COLOR_YELLOW}Number of orders:{COLOR_RESET}{COLOR_GREEN} {backtest_report.number_of_orders}{COLOR_RESET} .:=+#%%%%%*###%%%%#*+#%%%%%%*+-: {COLOR_YELLOW}Initial balance:{COLOR_RESET}{COLOR_GREEN} {backtest_report.initial_unallocated}{COLOR_RESET} @@ -339,9 +346,9 @@ def pretty_print_backtest( .++- -%%%%%%%%%%%+= {COLOR_YELLOW}Percentage negative trades:{COLOR_RESET}{COLOR_GREEN} {backtest_report.percentage_negative_trades}%{COLOR_RESET} .++- .%%%%%%%%%%%%%+= {COLOR_YELLOW}Average trade size:{COLOR_RESET}{COLOR_GREEN} {backtest_report.average_trade_size:.{precision}f} {backtest_report.trading_symbol}{COLOR_RESET} .++- *%%%%%%%%%%%%%*+: {COLOR_YELLOW}Average trade duration:{COLOR_RESET}{COLOR_GREEN} {backtest_report.average_trade_duration} hours{COLOR_RESET} - .++- %%%%%%%%%%%%%%#+= - =++........:::%%%%%%%%%%%%%%*+- - .=++++++++++**#%%%%%%%%%%%%%++. + .++- %%%%%%%%%%%%%%#+= + =++........:::%%%%%%%%%%%%%%*+- + .=++++++++++**#%%%%%%%%%%%%%++. """ print(ascii_art) @@ -470,3 +477,146 @@ def load_backtest_reports(folder_path: str) -> List[BacktestReport]: backtest_reports.append(report) return backtest_reports + + +def get_backtest_report( + directory: str, + algorithm_name: str, + backtest_date_range: BacktestDateRange=None +) -> BacktestReport: + """ + Function to get a report based on the algorithm name and + backtest date range if it exists. + + Args: + algorithm_name (str): The name of the algorithm + backtest_date_range (BacktestDateRange): The backtest date range + directory (str): The output directory + + Returns: + BacktestReport: The backtest report if it exists, otherwise None + """ + + # Loop through all files in the output directory + for root, _, files in os.walk(directory): + for file in files: + # Check if the file contains the algorithm name + # and backtest date range + if is_backtest_report(os.path.join(root, file)): + # Read the file + with open(os.path.join(root, file), "r") as json_file: + + name = \ + get_algorithm_name_from_backtest_report_file( + os.path.join(root, file) + ) + + if name == algorithm_name: + + if backtest_date_range is None: + # Parse the JSON file + report = json.load(json_file) + # Convert the JSON file to a + # BacktestReport object + return BacktestReport.from_dict(report) + + backtest_start_date = \ + get_start_date_from_backtest_report_file( + os.path.join(root, file) + ) + print(backtest_start_date) + backtest_end_date = \ + get_end_date_from_backtest_report_file( + os.path.join(root, file) + ) + + if backtest_start_date == \ + backtest_date_range.start_date \ + and backtest_end_date == \ + backtest_date_range.end_date: + # Parse the JSON file + report = json.load(json_file) + # Convert the JSON file to a + # BacktestReport object + return BacktestReport.from_dict(report) + + return None + + +def get_start_date_from_backtest_report_file(path: str) -> datetime: + """ + Function to get the backtest start date from a backtest report file. + + Parameters: + path (str): The path to the backtest report file + + Returns: + datetime: The backtest start date + """ + + # Get the backtest start date from the file name + backtest_start_date = os.path.basename(path).split("_")[3] + # Parse the backtest start date + return datetime.strptime( + backtest_start_date, DATETIME_FORMAT_BACKTESTING + ) + + +def get_end_date_from_backtest_report_file(path: str) -> datetime: + """ + Function to get the backtest end date from a backtest report file. + + Parameters: + path (str): The path to the backtest report file + + Returns: + datetime: The backtest end date + """ + + # Get the backtest end date from the file name + backtest_end_date = os.path.basename(path).split("_")[5] + # Parse the backtest end date + return datetime.strptime( + backtest_end_date, DATETIME_FORMAT_BACKTESTING + ) + + +def get_algorithm_name_from_backtest_report_file(path: str) -> str: + """ + Function to get the algorithm name from a backtest report file. + + Parameters: + path (str): The path to the backtest report file + + Returns: + str: The algorithm name + """ + # Get the word between "report_" and "_backtest_start_date" + # it can contain _ + # Get the algorithm name from the file name + algorithm_name = os.path.basename(path).split("_")[1] + return algorithm_name + + +def is_backtest_report(path: str) -> bool: + """ + Function to check if a file is a backtest report file. + + Args: + path (str): The path to the file + + Returns: + bool: True if the file is a backtest report file, otherwise False + """ + + # Check if the file is a JSON file + if path.endswith(".json"): + + # Check if the file name matches the backtest + # report file name pattern + if re.match( + BACKTEST_REPORT_FILE_NAME_PATTERN, os.path.basename(path) + ): + return True + + return False diff --git a/investing_algorithm_framework/indicators/__init__.py b/investing_algorithm_framework/indicators/__init__.py index 9d7d3b7c..247c0ba1 100644 --- a/investing_algorithm_framework/indicators/__init__.py +++ b/investing_algorithm_framework/indicators/__init__.py @@ -5,7 +5,8 @@ is_below, has_crossed_upward, has_crossed_downward, \ has_any_higher_then_threshold, has_any_lower_then_threshold, \ get_slope, has_slope_above_threshold, has_slope_below_threshold, \ - has_values_above_threshold, has_values_below_threshold + has_values_above_threshold, has_values_below_threshold, \ + get_values_above_threshold, get_values_below_threshold from .momentum import get_willr __all__ = [ @@ -33,5 +34,7 @@ "has_slope_above_threshold", "has_slope_below_threshold", "has_values_above_threshold", - "has_values_below_threshold" + "has_values_below_threshold", + "get_values_above_threshold", + "get_values_below_threshold" ] diff --git a/investing_algorithm_framework/indicators/trend.py b/investing_algorithm_framework/indicators/trend.py index e2c6f50f..a4d10fe4 100644 --- a/investing_algorithm_framework/indicators/trend.py +++ b/investing_algorithm_framework/indicators/trend.py @@ -77,7 +77,15 @@ def is_downtrend( slow_column="SMA_200" ) -> bool: """ - Check if the price data is in a downturn. + Function to check if the price data is in a downturn. + + Args: + data (Union[pd.DataFrame, pd.Series]): The input pandas DataFrame or Series. + fast_column (str): The key for the fast moving average (default: SMA_50). + slow_column (str): The key for the slow moving average (default: SMA_200). + + Returns: + bool: Boolean indicating if the price data is in a downturn. """ if not isinstance(data, pd.Series) and not isinstance(data, pd.DataFrame): diff --git a/investing_algorithm_framework/indicators/utils.py b/investing_algorithm_framework/indicators/utils.py index 535844a1..89c58de7 100644 --- a/investing_algorithm_framework/indicators/utils.py +++ b/investing_algorithm_framework/indicators/utils.py @@ -408,7 +408,13 @@ def has_slope_below_threshold( def has_values_below_threshold( - df, column, threshold, number_of_data_points, proportion=100 + df, + column, + threshold, + number_of_data_points, + proportion=100, + window_size=None, + strict=True ) -> bool: """ Detect if the last N data points in a column are below a certain threshold. @@ -419,26 +425,57 @@ def has_values_below_threshold( - threshold: float, the threshold for "low" values - number_of_data_points: int, the number of recent data points to analyze - proportion: float, the required proportion of values below the threshold + - window_size: int, the number of data points to consider for the threshold + - strict: bool, whether to check for a strict comparison Returns: - bool: True if the last N data points are below the threshold, False otherwise """ - # Get the last `window_size` data points - recent_data = df[column].iloc[-number_of_data_points:] + if window_size is not None and window_size < number_of_data_points: + difference = number_of_data_points - window_size + count = 0 + else: + difference = 1 + window_size = number_of_data_points + count = 0 + index = -(window_size) proportion = proportion / 100 - # Calculate the proportion of values below the threshold - below_threshold = recent_data < threshold - proportion_below = below_threshold.mean() + # Loop over sliding windows that shrink from the beginning + while count <= difference: + + if count == 0: + selected_window = df[column].iloc[index:] + else: + selected_window = df[column].iloc[index:-count] - # Determine if this window qualifies as a low period - return proportion_below >= proportion + count += 1 + index -= 1 + + # Calculate the proportion of values below the threshold + if strict: + below_threshold = selected_window < threshold + else: + below_threshold = selected_window <= threshold + + proportion_below = below_threshold.mean() + + if proportion_below >= proportion: + return True + + return False def has_values_above_threshold( - df, column, threshold, number_of_data_points, proportion=100 + df, + column, + threshold, + number_of_data_points, + proportion=100, + window_size=None, + strict=True ) -> bool: """ Detect if the last N data points in a column are above a certain threshold. @@ -449,19 +486,94 @@ def has_values_above_threshold( - threshold: float, the threshold for values - number_of_data_points: int, the number of recent data points to analyze - proportion: float, the required proportion of values below the threshold + - window_size: int, the number of data points to consider for the threshold + - strict: bool, whether to check for a strict comparison Returns: - bool: True if the last N data points are above the threshold, False otherwise """ - # Get the last `window_size` data points - recent_data = df[column].iloc[-number_of_data_points:] + if window_size is not None and window_size < number_of_data_points: + difference = number_of_data_points - window_size + count = 0 + else: + difference = 1 + window_size = number_of_data_points + count = 1 + index = -(window_size) proportion = proportion / 100 - # Calculate the proportion of values below the threshold - above_threshold = recent_data < threshold - proportion_below = above_threshold.mean() + # Loop over sliding windows that shrink from the beginning + while count <= difference: + + if count == 0: + selected_window = df[column].iloc[index:] + else: + selected_window = df[column].iloc[index:-count] + + count += 1 + index -= 1 + + # Calculate the proportion of values below the threshold + if strict: + above_threshold = selected_window > threshold + else: + above_threshold = selected_window >= threshold + + proportion_above = above_threshold.mean() + + if proportion_above >= proportion: + return True + + return False + + +def get_values_above_threshold( + df, column, threshold, number_of_data_points +) -> int: + """ + Return a list of values above the threshold. + + Parameters: + - df: pandas DataFrame + - column: str, the column containing the values to analyze + - threshold: float, the threshold for values + - number_of_data_points: int, the number of recent data points to analyze + - window_size: int, the number of + + Returns: + - list: a list of values above the threshold + """ + # Get the last `number_of_data_points` data points + recent_data = df[column].iloc[-number_of_data_points:] + + # Filter for values above the threshold + above_threshold = recent_data[recent_data > threshold] + + # Return the filtered values as a list + return above_threshold.tolist() + +def get_values_below_threshold( + df, column, threshold, number_of_data_points +) -> int: + """ + Return a list of values below the threshold. + + Parameters: + - df: pandas DataFrame + - column: str, the column containing the values to analyze + - threshold: float, the threshold for values + - number_of_data_points: int, the number of recent data points to analyze + + Returns: + - list: a list of values below the threshold + """ + # Get the last `number_of_data_points` data points + recent_data = df[column].iloc[-number_of_data_points:] + + # Filter for values below the threshold + below_threshold = recent_data[recent_data < threshold] - # Determine if this window qualifies as a low period - return proportion_below >= proportion + # Return the filtered values as a list + return below_threshold.tolist() diff --git a/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py b/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py index f41ace93..1dc0c50b 100644 --- a/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +++ b/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py @@ -1,7 +1,6 @@ -import datetime import logging import os -from datetime import timedelta +from datetime import timedelta, datetime, timezone from dateutil.parser import parse import polars from dateutil import parser @@ -101,6 +100,7 @@ def prepare_data( self.backtest_data_index_date = backtest_data_start_date\ .replace(microsecond=0) self.backtest_data_end_date = backtest_end_date.replace(microsecond=0) + # Creating the backtest data directory and file self.backtest_data_directory = os.path.join( config.get(RESOURCE_DIRECTORY), @@ -442,7 +442,7 @@ def get_data(self, **kwargs): Implementation of get_data for CCXTOHLCVMarketDataSource. This implementation uses the CCXTMarketService to get the OHLCV data. - Parameters: + Args: window_size: int (optional) - the total amount of candle sticks that need to be returned start_date: datetime (optional) - the start date of the data. The @@ -470,19 +470,27 @@ def get_data(self, **kwargs): if "window_size" in kwargs: self.window_size = kwargs["window_size"] + start_date = None + end_date = None + if "start_date" in kwargs: start_date = kwargs["start_date"] - if not isinstance(start_date, datetime.datetime): + if not isinstance(start_date, datetime): raise OperationalException( "start_date should be a datetime object" ) - else: - raise OperationalException( - "start_date should be set for CCXTOHLCVMarketDataSource" - ) - if "end_date" not in kwargs: + if "end_date" in kwargs: + end_date = kwargs["end_date"] + + if not isinstance(end_date, datetime): + raise OperationalException( + "end_date should be a datetime object" + ) + + # Calculate the start and end dates + if start_date is None or end_date is None: if self.window_size is None: raise OperationalException( @@ -491,21 +499,22 @@ def get_data(self, **kwargs): "parameter for CCXTOHLCVMarketDataSource" ) - end_date = self.create_end_date( - start_date, self.time_frame, self.window_size - ) - else: - end_date = kwargs["end_date"] + if start_date is None: - if not isinstance(end_date, datetime.datetime): - raise OperationalException( - "end_date should be a datetime object" - ) + if end_date is None: + end_date = datetime.now(tz=timezone.utc) - if not isinstance(start_date, datetime.datetime): - raise OperationalException( - "start_date should be a datetime object" - ) + start_date = self.create_start_date( + end_date=end_date, + time_frame=self.time_frame, + window_size=self.window_size + ) + else: + end_date = self.create_end_date( + start_date=start_date, + time_frame=self.time_frame, + window_size=self.window_size + ) if "storage_path" in kwargs: storage_path = kwargs["storage_path"] diff --git a/investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py b/investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py index 73c0a874..56e47311 100644 --- a/investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py +++ b/investing_algorithm_framework/infrastructure/services/market_service/ccxt_market_service.py @@ -161,7 +161,11 @@ def get_order(self, order, market): def get_orders(self, symbol, market, since: datetime = None): market_credential = self.get_market_credential(market) exchange = self.initialize_exchange(market, market_credential) - datetime_format = self.config["DATETIME_FORMAT"] + + if self.config is not None and "DATETIME_FORMAT" in self.config: + datetime_format = self.config["DATETIME_FORMAT"] + else: + datetime_format = DATETIME_FORMAT if not exchange.has['fetchOrders']: raise OperationalException( diff --git a/tests/indicators/test_utils.py b/tests/indicators/test_utils.py index 1fe6d8f3..2669d0d2 100644 --- a/tests/indicators/test_utils.py +++ b/tests/indicators/test_utils.py @@ -1,7 +1,7 @@ from unittest import TestCase from investing_algorithm_framework.indicators import is_crossover, \ - has_any_higher_then_threshold, \ - get_slope, has_slope_above_threshold + has_any_higher_then_threshold, has_values_above_threshold, \ + get_slope, has_slope_above_threshold, has_values_below_threshold import pandas as pd class TestUtils(TestCase): @@ -98,3 +98,119 @@ def test_has_slope_above_threshold(self): df, column="RSI", threshold=2, number_of_data_points=2, window_size=2 ) ) + + def test_has_values_above_threshold(self): + df = pd.DataFrame({ + "RSI": [2, 5, 6, 5, 10, 9, 7, 7, 9, 7, 5], + "DateTime": pd.date_range("2021-01-01", periods=11, freq="D") + }) + self.assertTrue( + has_values_above_threshold( + df, + column="RSI", + threshold=1, + number_of_data_points=5, + proportion=100 + ) + ) + self.assertFalse( + has_values_above_threshold( + df, + column="RSI", + threshold=7, + number_of_data_points=5, + proportion=100 + ) + ) + self.assertTrue( + has_values_above_threshold( + df, + column="RSI", + threshold=6, + number_of_data_points=6, + proportion=100, + window_size=5 + ) + ) + self.assertTrue( + has_values_above_threshold( + df, + column="RSI", + threshold=9, + number_of_data_points=10, + proportion=20, + window_size=5 + ) + ) + self.assertFalse( + has_values_above_threshold( + df, + column="RSI", + threshold=9, + number_of_data_points=10, + proportion=40, + window_size=5 + ) + ) + + def test_has_values_below_threshold(self): + df = pd.DataFrame({ + "RSI": [2, 5, 6, 5, 10, 8, 7, 7, 5, 7, 9], + "DateTime": pd.date_range("2021-01-01", periods=11, freq="D") + }) + self.assertTrue( + has_values_below_threshold( + df, + column="RSI", + threshold=8, + number_of_data_points=5, + proportion=80 + ) + ) + self.assertFalse( + has_values_below_threshold( + df, + column="RSI", + threshold=8, + number_of_data_points=5, + proportion=90 + ) + ) + self.assertTrue( + has_values_below_threshold( + df, + column="RSI", + threshold=10, + number_of_data_points=5, + proportion=100 + ) + ) + self.assertFalse( + has_values_below_threshold( + df, + column="RSI", + threshold=4, + number_of_data_points=5, + proportion=100 + ) + ) + self.assertFalse( + has_values_below_threshold( + df, + column="RSI", + threshold=8, + number_of_data_points=6, + proportion=100, + window_size=5 + ) + ) + self.assertTrue( + has_values_below_threshold( + df, + column="RSI", + threshold=9, + number_of_data_points=6, + proportion=100, + window_size=5 + ) + ) diff --git a/tests/infrastructure/market_data_sources/test_ccxt_ohlcv_market_data_source.py b/tests/infrastructure/market_data_sources/test_ccxt_ohlcv_market_data_source.py index 46d6d749..87fa4af5 100644 --- a/tests/infrastructure/market_data_sources/test_ccxt_ohlcv_market_data_source.py +++ b/tests/infrastructure/market_data_sources/test_ccxt_ohlcv_market_data_source.py @@ -1,5 +1,6 @@ import os -from unittest import TestCase +from datetime import datetime +from unittest import TestCase, mock from investing_algorithm_framework.infrastructure import \ CCXTOHLCVMarketDataSource @@ -63,3 +64,62 @@ def test_get_timeframe(self): symbol="BTC/EUR", ) self.assertEqual("15m", ccxt_ohlcv_market_data_source.get_time_frame()) + + @mock.patch('investing_algorithm_framework.infrastructure.services.market_service.ccxt_market_service.CCXTMarketService.get_ohlcv') + def test_get_data_with_only_window_size(self, mock): + data_source = CCXTOHLCVMarketDataSource( + identifier="BTC/EUR", + window_size=200, + time_frame="15m", + market="BITVAVO", + symbol="BTC/EUR", + ) + mock.return_value = {'key': 'value'} + data = data_source.get_data() + self.assertIsNotNone(data) + self.assertEqual(data_source.window_size, 200) + + @mock.patch('investing_algorithm_framework.infrastructure.services.market_service.ccxt_market_service.CCXTMarketService.get_ohlcv') + def test_get_data_with_only_start_date_and_end_date(self, mock): + data_source = CCXTOHLCVMarketDataSource( + identifier="BTC/EUR", + time_frame="15m", + market="BITVAVO", + symbol="BTC/EUR", + ) + mock.return_value = {'key': 'value'} + data = data_source.get_data( + start_date=datetime(2021, 1, 1), + end_date=datetime(2021, 1, 2) + ) + self.assertIsNotNone(data) + + @mock.patch('investing_algorithm_framework.infrastructure.services.market_service.ccxt_market_service.CCXTMarketService.get_ohlcv') + def test_get_data_with_only_start_date_and_window_size(self, mock): + data_source = CCXTOHLCVMarketDataSource( + identifier="BTC/EUR", + time_frame="15m", + market="BITVAVO", + symbol="BTC/EUR", + ) + mock.return_value = {'key': 'value'} + data = data_source.get_data( + start_date=datetime(2021, 1, 1), + window_size=200 + ) + self.assertIsNotNone(data) + + @mock.patch('investing_algorithm_framework.infrastructure.services.market_service.ccxt_market_service.CCXTMarketService.get_ohlcv') + def test_get_data_with_only_end_date_and_window_size(self, mock): + data_source = CCXTOHLCVMarketDataSource( + identifier="BTC/EUR", + time_frame="15m", + market="BITVAVO", + symbol="BTC/EUR", + ) + mock.return_value = {'key': 'value'} + data = data_source.get_data( + start_date=datetime(2021, 1, 1), + window_size=200 + ) + self.assertIsNotNone(data) From d6baab6426eb1dea6fc106c3501e22258205b4d5 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Sun, 29 Dec 2024 00:00:12 +0100 Subject: [PATCH 2/9] Fix flake8 issues --- investing_algorithm_framework/__init__.py | 3 +-- .../domain/models/backtesting/backtest_report.py | 4 +++- .../domain/models/trade/trade.py | 6 ++++-- investing_algorithm_framework/indicators/trend.py | 9 ++++++--- investing_algorithm_framework/indicators/utils.py | 1 + 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/investing_algorithm_framework/__init__.py b/investing_algorithm_framework/__init__.py index 1ec300a6..2fa42e83 100644 --- a/investing_algorithm_framework/__init__.py +++ b/investing_algorithm_framework/__init__.py @@ -20,8 +20,7 @@ from investing_algorithm_framework.indicators import get_rsi, get_peaks, \ is_uptrend, is_downtrend, is_crossover, is_crossunder, is_above, \ is_below, has_crossed_upward, get_sma, get_up_and_downtrends, \ - get_rsi, get_ema, get_adx, has_crossed_downward, get_willr, \ - is_divergence + get_ema, get_adx, has_crossed_downward, get_willr, is_divergence __all__ = [ "Algorithm", diff --git a/investing_algorithm_framework/domain/models/backtesting/backtest_report.py b/investing_algorithm_framework/domain/models/backtesting/backtest_report.py index 15c8f695..cc4a36cb 100644 --- a/investing_algorithm_framework/domain/models/backtesting/backtest_report.py +++ b/investing_algorithm_framework/domain/models/backtesting/backtest_report.py @@ -483,7 +483,9 @@ def from_dict(data): positions = data["positions"] if positions is not None: - report.positions = [Position.from_dict(position) for position in positions] + report.positions = [ + Position.from_dict(position) for position in positions + ] trades = data["trades"] diff --git a/investing_algorithm_framework/domain/models/trade/trade.py b/investing_algorithm_framework/domain/models/trade/trade.py index 44fd2225..582558e2 100644 --- a/investing_algorithm_framework/domain/models/trade/trade.py +++ b/investing_algorithm_framework/domain/models/trade/trade.py @@ -252,8 +252,10 @@ def to_dict(self): @staticmethod def from_dict(data): return Trade( - buy_order_id=data["buy_order_id"] if "buy_order_id" in data else None, - sell_order_id=data["sell_order_id"] if "sell_order_id" in data else None, + buy_order_id=data["buy_order_id"] if "buy_order_id" + in data else None, + sell_order_id=data["sell_order_id"] if "sell_order_id" + in data else None, target_symbol=data["target_symbol"], trading_symbol=data["trading_symbol"], amount=data["amount"], diff --git a/investing_algorithm_framework/indicators/trend.py b/investing_algorithm_framework/indicators/trend.py index a4d10fe4..580fdd16 100644 --- a/investing_algorithm_framework/indicators/trend.py +++ b/investing_algorithm_framework/indicators/trend.py @@ -80,9 +80,12 @@ def is_downtrend( Function to check if the price data is in a downturn. Args: - data (Union[pd.DataFrame, pd.Series]): The input pandas DataFrame or Series. - fast_column (str): The key for the fast moving average (default: SMA_50). - slow_column (str): The key for the slow moving average (default: SMA_200). + data (Union[pd.DataFrame, pd.Series]): The input pandas + DataFrame or Series. + fast_column (str): The key for the fast moving + average (default: SMA_50). + slow_column (str): The key for the slow moving + average (default: SMA_200). Returns: bool: Boolean indicating if the price data is in a downturn. diff --git a/investing_algorithm_framework/indicators/utils.py b/investing_algorithm_framework/indicators/utils.py index 89c58de7..98f68a5e 100644 --- a/investing_algorithm_framework/indicators/utils.py +++ b/investing_algorithm_framework/indicators/utils.py @@ -554,6 +554,7 @@ def get_values_above_threshold( # Return the filtered values as a list return above_threshold.tolist() + def get_values_below_threshold( df, column, threshold, number_of_data_points ) -> int: From f7c5a123db7583046689afd1ea54ea5b389113eb Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Fri, 10 Jan 2025 18:55:53 +0100 Subject: [PATCH 3/9] Add azure storage state handler support --- .gitignore | 2 + README.md | 53 +- examples/app.py | 49 ++ examples/bitvavo_trading_bot/bitvavo.py | 29 +- .../deployments/azure_function/.funcignore | 1 + .../deployments/azure_function/.gitignore | 48 ++ examples/deployments/azure_function/README.md | 76 ++ .../deployments/azure_function/__init__.py | 0 .../azure_function/function_app.py | 87 +++ examples/deployments/azure_function/host.json | 15 + .../azure_function/requirements.txt | 2 + .../example_strategies/macd_wr/macd_wr.ipynb | 184 +---- investing_algorithm_framework/__init__.py | 11 +- .../app/algorithm.py | 65 +- investing_algorithm_framework/app/app.py | 421 +++++----- .../app/web/__init__.py | 3 +- .../app/web/create_app.py | 6 +- .../{deployment => cli}/__init__.py | 0 .../cli/create_azure_function_app_skeleton.py | 118 +++ .../cli/deploy_to_azure_function.py | 651 ++++++++++++++++ .../azure_function_framework_app.py.template | 49 ++ .../azure_function_function_app.py.template | 90 +++ .../azure_function_host.json.template | 15 + ...zure_function_local.settings.json.template | 8 + .../azure_function_requirements.txt.template | 2 + investing_algorithm_framework/create_app.py | 29 +- .../deployment/azure/__init__.py | 3 - .../deployment/azure/azure_functions.py | 102 --- .../domain/__init__.py | 6 +- .../domain/config.py | 123 +-- .../domain/models/market/market_credential.py | 55 +- .../domain/models/order/order.py | 5 +- .../domain/models/portfolio/portfolio.py | 40 +- .../domain/utils/backtesting.py | 1 - .../infrastructure/__init__.py | 4 +- .../infrastructure/database/sql_alchemy.py | 17 +- .../models/market_data_sources/ccxt.py | 17 +- .../models/portfolio/__init__.py | 2 +- .../{portfolio.py => sql_portfolio.py} | 11 +- .../repositories/order_repository.py | 1 + .../infrastructure/repositories/repository.py | 12 + .../infrastructure/services/__init__.py | 4 +- .../infrastructure/services/azure/__init__.py | 5 + .../services/azure/state_handler.py | 142 ++++ .../services/backtesting/backtest_service.py | 1 + .../services/configuration_service.py | 70 +- .../services/market_credential_service.py | 8 + .../market_data_source_service.py | 13 +- .../order_service/order_backtest_service.py | 10 +- .../services/order_service/order_service.py | 16 +- .../portfolios/backtest_portfolio_service.py | 1 + .../portfolio_configuration_service.py | 3 + .../services/portfolios/portfolio_service.py | 52 +- .../portfolios/portfolio_sync_service.py | 282 +++---- .../services/repository_service.py | 3 + .../services/strategy_orchestrator_service.py | 4 +- poetry.lock | 234 +++++- pyproject.toml | 12 +- .../test_create_market_sell_order.py | 2 +- tests/app/algorithm/test_get_data.py | 66 -- .../app/algorithm/test_get_pending_orders.py | 11 +- tests/app/algorithm/test_get_portfolio.py | 2 +- .../algorithm/test_get_unfilled_buy_value.py | 21 +- .../algorithm/test_get_unfilled_sell_value.py | 33 +- .../app/algorithm/test_has_open_buy_orders.py | 105 +-- tests/app/algorithm/test_run_strategy.py | 17 +- tests/app/backtesting/test_backtest_report.py | 96 +-- tests/app/backtesting/test_run_backtest.py | 18 +- tests/app/backtesting/test_run_backtests.py | 12 +- tests/app/test_add_config.py | 48 +- tests/app/test_add_portfolio_configuration.py | 52 +- tests/app/test_app_initialize.py | 211 +++-- tests/app/test_app_stateless.py | 99 --- tests/app/test_backtesting.py | 72 +- tests/app/test_config.py | 47 +- tests/app/test_start.py | 163 ++-- .../test_start_with_new_external_orders.py | 132 ++++ .../test_start_with_new_external_positions.py | 132 ++++ .../test_list_positions.py | 3 - tests/app/web/schemas/test_order_schema.py | 25 - tests/domain/test_load_backtest_reports.py | 1 - tests/infrastructure/models/test_order.py | 57 +- tests/infrastructure/models/test_position.py | 89 ++- tests/resources/test_base.py | 139 +++- .../test_market_data_source_service.py | 60 +- tests/services/test_order_backtest_service.py | 66 +- tests/services/test_portfolio_sync_service.py | 725 +----------------- tests/test_create_app.py | 24 +- 88 files changed, 3279 insertions(+), 2522 deletions(-) create mode 100644 examples/app.py create mode 100644 examples/deployments/azure_function/.funcignore create mode 100644 examples/deployments/azure_function/.gitignore create mode 100644 examples/deployments/azure_function/README.md rename tests/app/test_tasks.py => examples/deployments/azure_function/__init__.py (100%) create mode 100644 examples/deployments/azure_function/function_app.py create mode 100644 examples/deployments/azure_function/host.json create mode 100644 examples/deployments/azure_function/requirements.txt rename investing_algorithm_framework/{deployment => cli}/__init__.py (100%) create mode 100644 investing_algorithm_framework/cli/create_azure_function_app_skeleton.py create mode 100644 investing_algorithm_framework/cli/deploy_to_azure_function.py create mode 100644 investing_algorithm_framework/cli/templates/azure_function_framework_app.py.template create mode 100644 investing_algorithm_framework/cli/templates/azure_function_function_app.py.template create mode 100644 investing_algorithm_framework/cli/templates/azure_function_host.json.template create mode 100644 investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template create mode 100644 investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template delete mode 100644 investing_algorithm_framework/deployment/azure/__init__.py delete mode 100644 investing_algorithm_framework/deployment/azure/azure_functions.py rename investing_algorithm_framework/infrastructure/models/portfolio/{portfolio.py => sql_portfolio.py} (89%) create mode 100644 investing_algorithm_framework/infrastructure/services/azure/__init__.py create mode 100644 investing_algorithm_framework/infrastructure/services/azure/state_handler.py delete mode 100644 tests/app/algorithm/test_get_data.py delete mode 100644 tests/app/test_app_stateless.py create mode 100644 tests/app/test_start_with_new_external_orders.py create mode 100644 tests/app/test_start_with_new_external_positions.py delete mode 100644 tests/app/web/schemas/test_order_schema.py diff --git a/.gitignore b/.gitignore index f80c3c6e..2bb2cda5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,8 +2,10 @@ ### Python template # Byte-compiled / optimized / DLL files __pycache__/ +*__pycache__/ *.py[cod] *$py.class +.DS_Store # C extensions *.so diff --git a/README.md b/README.md index 36890816..4c6513e8 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ [![GitHub stars](https://img.shields.io/github/stars/coding-kitties/investing-algorithm-framework.svg?style=social&label=Star&maxAge=1)](https://github.com/SeaQL/sea-orm/stargazers/) If you like what we do, consider starring, sharing and contributing! ###### Sponsors +

Finterion @@ -36,30 +37,13 @@ Features: The following algorithm connects to binance and buys BTC every 5 seconds. It also exposes an REST API that allows you to interact with the algorithm. ```python -import pathlib +import logging from investing_algorithm_framework import create_app, PortfolioConfiguration, \ - RESOURCE_DIRECTORY, TimeUnit, CCXTOHLCVMarketDataSource, Algorithm, \ - CCXTTickerMarketDataSource, MarketCredential, SYMBOLS - -# Define the symbols you want to trade for optimization, otherwise the -# algorithm will check if you have orders and balances on all available -# symbols on the market -symbols = ["BTC/EUR"] - -# Define resource directory and the symbols you want to trade -config = { - RESOURCE_DIRECTORY: pathlib.Path(__file__).parent.resolve(), - SYMBOLS: symbols -} - -state_manager = AzureBlobStorageStateManager( - account_name="", - account_key="", - container_name="", - blob_name="", -) + TimeUnit, CCXTOHLCVMarketDataSource, Algorithm, \ + CCXTTickerMarketDataSource, MarketCredential, DEFAULT_LOGGING_CONFIG + +logging.config.dictConfig(DEFAULT_LOGGING_CONFIG) -# Define market data sources # OHLCV data for candles bitvavo_btc_eur_ohlcv_2h = CCXTOHLCVMarketDataSource( identifier="BTC-ohlcv", @@ -74,17 +58,10 @@ bitvavo_btc_eur_ticker = CCXTTickerMarketDataSource( market="BITVAVO", symbol="BTC/EUR", ) -app = create_app( - config=config, - sync_portfolio=True, - state_manager=state_manager -) +app = create_app() algorithm = Algorithm() -app.add_market_credential(MarketCredential( - market="bitvavo", - api_key="", - secret_key="", -)) +# Bitvavo market credentials are read from .env file +app.add_market_credential(MarketCredential(market="bitvavo")) app.add_portfolio_configuration( PortfolioConfiguration( market="bitvavo", @@ -94,21 +71,18 @@ app.add_portfolio_configuration( ) app.add_algorithm(algorithm) +# Run every two hours and register the data sources @algorithm.strategy( - # Run every two hours time_unit=TimeUnit.HOUR, interval=2, - # Specify market data sources that need to be passed to the strategy market_data_sources=[bitvavo_btc_eur_ticker, bitvavo_btc_eur_ohlcv_2h] ) def perform_strategy(algorithm: Algorithm, market_data: dict): - # By default, ohlcv data is passed as polars df in the form of - # {"": } https://pola.rs/, - # call to_pandas() to convert to pandas + # Access the data sources with the indentifier polars_df = market_data["BTC-ohlcv"] - print(f"I have access to {len(polars_df)} candles of ohlcv data") - # Ticker data is passed as {"": } + # Convert the polars dataframe to a pandas dataframe + pandas_df = polars_df.to_pandas() ticker_data = market_data["BTC-ticker"] unallocated_balance = algorithm.get_unallocated() positions = algorithm.get_positions() @@ -257,7 +231,6 @@ app.add_portfolio_configuration( PortfolioConfiguration( market="", initial_balance=400, - track_from="01/01/2022", trading_symbol="EUR" ) ) diff --git a/examples/app.py b/examples/app.py new file mode 100644 index 00000000..ee2d1cbd --- /dev/null +++ b/examples/app.py @@ -0,0 +1,49 @@ +from dotenv import load_dotenv + +from investing_algorithm_framework import create_app, PortfolioConfiguration, \ + TimeUnit, CCXTOHLCVMarketDataSource, Algorithm, \ + CCXTTickerMarketDataSource, MarketCredential, AzureBlobStorageStateHandler + +load_dotenv() + +# Define market data sources +# OHLCV data for candles +bitvavo_btc_eur_ohlcv_2h = CCXTOHLCVMarketDataSource( + identifier="BTC-ohlcv", + market="BITVAVO", + symbol="BTC/EUR", + time_frame="2h", + window_size=200 +) +# Ticker data for orders, trades and positions +bitvavo_btc_eur_ticker = CCXTTickerMarketDataSource( + identifier="BTC-ticker", + market="BITVAVO", + symbol="BTC/EUR", +) +app = create_app(state_handler=AzureBlobStorageStateHandler()) +app.add_market_data_source(bitvavo_btc_eur_ohlcv_2h) +algorithm = Algorithm() +app.add_market_credential(MarketCredential(market="bitvavo")) +app.add_portfolio_configuration( + PortfolioConfiguration( + market="bitvavo", + trading_symbol="EUR", + initial_balance=20 + ) +) +app.add_algorithm(algorithm) + +@algorithm.strategy( + # Run every two hours + time_unit=TimeUnit.HOUR, + interval=2, + # Specify market data sources that need to be passed to the strategy + market_data_sources=[bitvavo_btc_eur_ticker, "BTC-ohlcv"] +) +def perform_strategy(algorithm: Algorithm, market_data: dict): + # By default, ohlcv data is passed as polars df in the form of + # {"": } https://pola.rs/, + # call to_pandas() to convert to pandas + polars_df = market_data["BTC-ohlcv"] + print(f"I have access to {len(polars_df)} candles of ohlcv data") diff --git a/examples/bitvavo_trading_bot/bitvavo.py b/examples/bitvavo_trading_bot/bitvavo.py index 46bd11a2..a3a69852 100644 --- a/examples/bitvavo_trading_bot/bitvavo.py +++ b/examples/bitvavo_trading_bot/bitvavo.py @@ -1,31 +1,28 @@ -import os - from investing_algorithm_framework import MarketCredential, TimeUnit, \ CCXTOHLCVMarketDataSource, CCXTTickerMarketDataSource, TradingStrategy, \ - create_app, PortfolioConfiguration, Algorithm, SYMBOLS, RESOURCE_DIRECTORY + create_app, PortfolioConfiguration, Algorithm """ Bitvavo trading bot example with market data sources of bitvavo. -Bitvavo does not requires you to have an API key and secret key to access -their market data. If you just want to backtest your strategy, +Bitvavo does not requires you to have an API key and secret key to access +their market data. If you just want to backtest your strategy, you don't need to add a market credential. If your running your strategy live, -you need to add a market credential to the app, that accesses your +you need to add a market credential to the app, that accesses your account on bitvavo. """ - # Define your market credential for bitvavo bitvavo_market_credential = MarketCredential( - api_key="", - secret_key="", market="bitvavo", + api_key="your_api_key", + secret_key="your_secret_key" ) # Define your market data sources for coinbase bitvavo_btc_eur_ohlcv_2h = CCXTOHLCVMarketDataSource( identifier="BTC/EUR-ohlcv", market="bitvavo", symbol="BTC/EUR", - timeframe="2h", + time_frame="2h", window_size=200 ) bitvavo_btc_eur_ticker = CCXTTickerMarketDataSource( @@ -36,26 +33,20 @@ class BitvavoTradingStrategy(TradingStrategy): - time_unit = TimeUnit.HOUR - interval = 2 + time_unit = TimeUnit.SECOND + interval = 10 market_data_sources = [bitvavo_btc_eur_ohlcv_2h, bitvavo_btc_eur_ticker] def apply_strategy(self, algorithm, market_data): print(market_data["BTC/EUR-ohlcv"]) print(market_data["BTC/EUR-ticker"]) - -config = { - SYMBOLS: ["BTC/EUR"], - RESOURCE_DIRECTORY: os.path.join(os.path.dirname(__file__), "resources") -} - # Create an algorithm and link your trading strategy to it algorithm = Algorithm() algorithm.add_strategy(BitvavoTradingStrategy) # Create an app and add the market data sources and market credentials to it -app = create_app(config=config) +app = create_app() app.add_market_credential(bitvavo_market_credential) app.add_market_data_source(bitvavo_btc_eur_ohlcv_2h) app.add_market_data_source(bitvavo_btc_eur_ticker) diff --git a/examples/deployments/azure_function/.funcignore b/examples/deployments/azure_function/.funcignore new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/deployments/azure_function/.funcignore @@ -0,0 +1 @@ + diff --git a/examples/deployments/azure_function/.gitignore b/examples/deployments/azure_function/.gitignore new file mode 100644 index 00000000..f15ac3fc --- /dev/null +++ b/examples/deployments/azure_function/.gitignore @@ -0,0 +1,48 @@ +bin +obj +csx +.vs +edge +Publish + +*.user +*.suo +*.cscfg +*.Cache +project.lock.json + +/packages +/TestResults + +/tools/NuGet.exe +/App_Data +/secrets +/data +.secrets +appsettings.json +local.settings.json + +node_modules +dist + +# Local python packages +.python_packages/ + +# Python Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# Azurite artifacts +__blobstorage__ +__queuestorage__ +__azurite_db*__.json \ No newline at end of file diff --git a/examples/deployments/azure_function/README.md b/examples/deployments/azure_function/README.md new file mode 100644 index 00000000..a790b331 --- /dev/null +++ b/examples/deployments/azure_function/README.md @@ -0,0 +1,76 @@ +# Investing Algorithm Framework App Deployment to Azure Functions + +This article demonstrates how to deploy your trading bot to Azure Functions. +We will deploy an example bot that uses the investing algorithm framework to +azure functions. In order to do that we will do the following: + +1. Create a new app using the framework with the azure blob storage state handler. +2. Use the framework provided ci tools to create a azure functions ready application. +3. Use the framework provided ci tools to deploy the application to azure functions. + +## Prerequisites + +For this example, you need to have the following: + +- An Azure account with an active subscription. [Create an account](https://azure.microsoft.com/en-us/free/) +- The Azure CLI installed. [Install the Azure CLI](https://docs.microsoft.com/en-us/cli/azure/install-azure-cli) +- The Azure Functions Core Tools installed. [Install the Azure Functions Core Tools](https://docs.microsoft.com/en-us/azure/azure-functions/functions-run-local) +- The investing algorithm framework installed. [Install the framework]() or simply run `pip install investing_algorithm_framework` + +### Creating a new app + +First run the following command to create a new app azure functions ready app: + +```bash +create_azure_function_trading_bot_app +``` + +This command will create a new app with the following structure: + +```yaml +. +├── function_app.py +├── host.json +├── local.settings.json +└── requirements.txt +``` + +The function_app.py while import the app from the app.py file in the root of the project and run it as an azure function. It is therefore important that you have a file named `app.py` in the root of your project that defines the application in a variable named `app`. + +Additionaly, because Azure Functions are stateless, you need to use a state storage solution. In this example, we will use Azure Blob Storage state handler provided by the framework. This state handler is specifically designed to work with Azure Functions and Azure Blob Storage. + +The reason why we need a state storage solution is that Azure Functions are stateless. This means that each function execution is independent of the previous one. This is a problem for trading bots because they need to keep track of their state between executions (portfolios, order, positions and trades). In order to solve this problem, we need to use a state storage solution that can store the bot's databases between executions. + +Combining all of this, the `app.py` file should look like this: + +> When you are using the cli command 'deploy_trading_bot_to_azure_function' (which we will use later) you don't need to provide any connection strings. The command will take care of provisioning all +> resourses and configuration of all required parameters for the state handler. + +```python +from investing_algorithm_framework import AzureBlobStorageStateHandler, create_app + +app = create_app(state_handler=AzureBlobStorageStateHandler) + +# Write here your code where your register your portfolio configurations, strategies and data providers +.... +``` + +## Deployment to Azure + +To deploy your trading bot to Azure Functions, you need to run the following command: + +```bash +deploy_trading_bot_to_azure_function +``` + +This command will do the following: + +- Create a new resource group in your Azure account. +- Create a new storage account in the resource group. +- Create a new blob container in the storage account. +- Create a new function app in the resource group. +- Deploy the trading bot to the function app. +- Configure the function app to use the blob container as the state handler. +- Print the URL of the function app. + +After running the command, you will see the URL of the function app. You can use this URL to access your trading bot. Now your trading bot is running on Azure Functions! diff --git a/tests/app/test_tasks.py b/examples/deployments/azure_function/__init__.py similarity index 100% rename from tests/app/test_tasks.py rename to examples/deployments/azure_function/__init__.py diff --git a/examples/deployments/azure_function/function_app.py b/examples/deployments/azure_function/function_app.py new file mode 100644 index 00000000..a4c8905d --- /dev/null +++ b/examples/deployments/azure_function/function_app.py @@ -0,0 +1,87 @@ +import logging + +import azure.functions as func +# from investing_algorithm_framework import StatelessAction +# from app import app as investing_algorithm_framework_app + + +import logging +import logging.config + +LOGGING_CONFIG = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'default': { + 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s', + }, + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'default', + }, + 'file': { + 'class': 'logging.FileHandler', + 'formatter': 'default', + 'filename': 'app_logs.log', + }, + }, + 'loggers': { # Make sure to add a 'loggers' section + 'investing_algorithm_framework': { # Define your logger here + 'level': 'INFO', # Set the desired level + 'handlers': ['console', 'file'], # Use these handlers + 'propagate': False, # Prevent logs from propagating to the root logger (optional) + }, + }, + 'root': { # Optional: Root logger configuration + 'level': 'WARNING', # Root logger defaults to WARNING + 'handlers': ['console', 'file'], + }, +} + +logging.config.dictConfig(LOGGING_CONFIG) +app = func.FunctionApp() + +# Change your interval here, e.ge. "0 */1 * * * *" for every minute +# or "0 0 */1 * * *" for every hour or "0 */5 * * * *" for every 5 minutes +# @func.timer_trigger( +# schedule="0 */5 * * * *", +# arg_name="myTimer", +# run_on_startup=False, +# use_monitor=False +# ) +# def app(myTimer: func.TimerRequest) -> None: + +# if myTimer.past_due: +# logging.info('The timer is past due!') + +# logging.info('Python timer trigger function ran at %s', myTimer.next) +# investing_algorithm_framework_app.run( +# payload={"ACTION": StatelessAction.RUN_STRATEGY.value} +# ) + +@app.route(route="test", auth_level=func.AuthLevel.ANONYMOUS) +def test(req: func.HttpRequest) -> func.HttpResponse: + logging.info('Python HTTP trigger function processed a request.') + + name = req.params.get('name') + # investing_algorithm_framework_app.run( + # payload={"ACTION": StatelessAction.RUN_STRATEGY.value} + # ) + + if not name: + try: + req_body = req.get_json() + except ValueError: + pass + else: + name = req_body.get('name') + + if name: + return func.HttpResponse(f"Hello, {name}. This HTTP triggered function executed successfully.") + else: + return func.HttpResponse( + "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.", + status_code=200 + ) diff --git a/examples/deployments/azure_function/host.json b/examples/deployments/azure_function/host.json new file mode 100644 index 00000000..9df91361 --- /dev/null +++ b/examples/deployments/azure_function/host.json @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} \ No newline at end of file diff --git a/examples/deployments/azure_function/requirements.txt b/examples/deployments/azure_function/requirements.txt new file mode 100644 index 00000000..f41fb1bf --- /dev/null +++ b/examples/deployments/azure_function/requirements.txt @@ -0,0 +1,2 @@ +investing-algorithm-framework +azure-functions==1.21.3 diff --git a/examples/example_strategies/macd_wr/macd_wr.ipynb b/examples/example_strategies/macd_wr/macd_wr.ipynb index 33b6f036..5158c3a2 100644 --- a/examples/example_strategies/macd_wr/macd_wr.ipynb +++ b/examples/example_strategies/macd_wr/macd_wr.ipynb @@ -33,166 +33,6 @@ "* Evaluate the backtest reports to determine the best configuration" ] }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": { - "metadata": {} - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: investing_algorithm_framework in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (3.7.3)\n", - "Requirement already satisfied: Flask<3.0.0,>=2.3.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (2.3.3)\n", - "Requirement already satisfied: Flask-Cors<5.0.0,>=3.0.9 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (4.0.1)\n", - "Requirement already satisfied: Flask-Migrate<3.0.0,>=2.6.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (2.7.0)\n", - "Requirement already satisfied: MarkupSafe<3.0.0,>=2.1.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (2.1.5)\n", - "Requirement already satisfied: SQLAlchemy<3.0.0,>=2.0.18 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (2.0.31)\n", - "Requirement already satisfied: ccxt<5.0.0,>=4.2.48 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (4.3.64)\n", - "Requirement already satisfied: dependency-injector<5.0.0,>=4.40.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (4.41.0)\n", - "Requirement already satisfied: jupyter<2.0.0,>=1.0.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (1.0.0)\n", - "Requirement already satisfied: marshmallow<4.0.0,>=3.5.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (3.21.3)\n", - "Requirement already satisfied: plotly<6.0.0,>=5.22.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (5.22.0)\n", - "Requirement already satisfied: polars<0.21.0,>=0.20.10 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from polars[numpy,pandas]<0.21.0,>=0.20.10->investing_algorithm_framework) (0.20.31)\n", - "Requirement already satisfied: python-dateutil<3.0.0,>=2.8.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (2.9.0.post0)\n", - "Requirement already satisfied: schedule<2.0.0,>=1.1.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (1.2.2)\n", - "Requirement already satisfied: tabulate<0.10.0,>=0.9.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (0.9.0)\n", - "Requirement already satisfied: tqdm<5.0.0,>=4.66.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (4.66.4)\n", - "Requirement already satisfied: wrapt<2.0.0,>=1.16.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from investing_algorithm_framework) (1.16.0)\n", - "Requirement already satisfied: setuptools>=60.9.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (74.1.0)\n", - "Requirement already satisfied: certifi>=2018.1.18 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (2024.8.30)\n", - "Requirement already satisfied: requests>=2.18.4 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (2.32.3)\n", - "Requirement already satisfied: cryptography>=2.6.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (42.0.8)\n", - "Requirement already satisfied: typing-extensions>=4.4.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (4.12.2)\n", - "Requirement already satisfied: aiohttp>=3.8 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (3.9.5)\n", - "Requirement already satisfied: aiodns>=1.1.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (3.2.0)\n", - "Requirement already satisfied: yarl>=1.7.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (1.9.4)\n", - "Requirement already satisfied: six<=1.16.0,>=1.7.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from dependency-injector<5.0.0,>=4.40.0->investing_algorithm_framework) (1.16.0)\n", - "Requirement already satisfied: Werkzeug>=2.3.7 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from Flask<3.0.0,>=2.3.2->investing_algorithm_framework) (3.0.3)\n", - "Requirement already satisfied: Jinja2>=3.1.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from Flask<3.0.0,>=2.3.2->investing_algorithm_framework) (3.1.4)\n", - "Requirement already satisfied: itsdangerous>=2.1.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from Flask<3.0.0,>=2.3.2->investing_algorithm_framework) (2.2.0)\n", - "Requirement already satisfied: click>=8.1.3 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from Flask<3.0.0,>=2.3.2->investing_algorithm_framework) (8.1.7)\n", - "Requirement already satisfied: blinker>=1.6.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from Flask<3.0.0,>=2.3.2->investing_algorithm_framework) (1.8.2)\n", - "Requirement already satisfied: importlib-metadata>=3.6.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from Flask<3.0.0,>=2.3.2->investing_algorithm_framework) (8.4.0)\n", - "Requirement already satisfied: Flask-SQLAlchemy>=1.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from Flask-Migrate<3.0.0,>=2.6.0->investing_algorithm_framework) (3.1.1)\n", - "Requirement already satisfied: alembic>=0.7 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from Flask-Migrate<3.0.0,>=2.6.0->investing_algorithm_framework) (1.13.2)\n", - "Requirement already satisfied: notebook in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (7.2.2)\n", - "Requirement already satisfied: qtconsole in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (5.5.2)\n", - "Requirement already satisfied: jupyter-console in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (6.6.3)\n", - "Requirement already satisfied: nbconvert in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (7.16.4)\n", - "Requirement already satisfied: ipykernel in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (6.29.5)\n", - "Requirement already satisfied: ipywidgets in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (8.1.3)\n", - "Requirement already satisfied: packaging>=17.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from marshmallow<4.0.0,>=3.5.0->investing_algorithm_framework) (24.1)\n", - "Requirement already satisfied: tenacity>=6.2.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from plotly<6.0.0,>=5.22.0->investing_algorithm_framework) (8.5.0)\n", - "Requirement already satisfied: numpy>=1.16.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from polars[numpy,pandas]<0.21.0,>=0.20.10->investing_algorithm_framework) (1.24.4)\n", - "Requirement already satisfied: pyarrow>=7.0.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from polars[numpy,pandas]<0.21.0,>=0.20.10->investing_algorithm_framework) (17.0.0)\n", - "Requirement already satisfied: pandas in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from polars[numpy,pandas]<0.21.0,>=0.20.10->investing_algorithm_framework) (2.0.3)\n", - "Requirement already satisfied: pycares>=4.0.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from aiodns>=1.1.1->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (4.4.0)\n", - "Requirement already satisfied: aiosignal>=1.1.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from aiohttp>=3.8->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (1.3.1)\n", - "Requirement already satisfied: attrs>=17.3.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from aiohttp>=3.8->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (24.2.0)\n", - "Requirement already satisfied: frozenlist>=1.1.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from aiohttp>=3.8->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (1.4.1)\n", - "Requirement already satisfied: multidict<7.0,>=4.5 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from aiohttp>=3.8->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (6.0.5)\n", - "Requirement already satisfied: async-timeout<5.0,>=4.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from aiohttp>=3.8->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (4.0.3)\n", - "Requirement already satisfied: Mako in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from alembic>=0.7->Flask-Migrate<3.0.0,>=2.6.0->investing_algorithm_framework) (1.3.5)\n", - "Requirement already satisfied: cffi>=1.12 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from cryptography>=2.6.1->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (1.17.0)\n", - "Requirement already satisfied: zipp>=0.5 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from importlib-metadata>=3.6.0->Flask<3.0.0,>=2.3.2->investing_algorithm_framework) (3.20.1)\n", - "Requirement already satisfied: charset-normalizer<4,>=2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from requests>=2.18.4->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (3.3.2)\n", - "Requirement already satisfied: idna<4,>=2.5 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from requests>=2.18.4->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (3.8)\n", - "Requirement already satisfied: urllib3<3,>=1.21.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from requests>=2.18.4->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (2.2.2)\n", - "Requirement already satisfied: appnope in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.1.4)\n", - "Requirement already satisfied: comm>=0.1.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.2.2)\n", - "Requirement already satisfied: debugpy>=1.6.5 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.8.5)\n", - "Requirement already satisfied: ipython>=7.23.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (8.18.1)\n", - "Requirement already satisfied: jupyter-client>=6.1.12 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (8.6.2)\n", - "Requirement already satisfied: jupyter-core!=5.0.*,>=4.12 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (5.7.2)\n", - "Requirement already satisfied: matplotlib-inline>=0.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.1.7)\n", - "Requirement already satisfied: nest-asyncio in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.6.0)\n", - "Requirement already satisfied: psutil in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (6.0.0)\n", - "Requirement already satisfied: pyzmq>=24 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (26.2.0)\n", - "Requirement already satisfied: tornado>=6.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (6.4.1)\n", - "Requirement already satisfied: traitlets>=5.4.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (5.14.3)\n", - "Requirement already satisfied: widgetsnbextension~=4.0.11 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipywidgets->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (4.0.11)\n", - "Requirement already satisfied: jupyterlab-widgets~=3.0.11 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipywidgets->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (3.0.11)\n", - "Requirement already satisfied: prompt-toolkit>=3.0.30 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-console->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (3.0.47)\n", - "Requirement already satisfied: pygments in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-console->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.18.0)\n", - "Requirement already satisfied: beautifulsoup4 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (4.12.3)\n", - "Requirement already satisfied: bleach!=5.0.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (6.1.0)\n", - "Requirement already satisfied: defusedxml in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.7.1)\n", - "Requirement already satisfied: jupyterlab-pygments in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.3.0)\n", - "Requirement already satisfied: mistune<4,>=2.0.3 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (3.0.2)\n", - "Requirement already satisfied: nbclient>=0.5.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.10.0)\n", - "Requirement already satisfied: nbformat>=5.7 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (5.10.4)\n", - "Requirement already satisfied: pandocfilters>=1.4.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.5.1)\n", - "Requirement already satisfied: tinycss2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.3.0)\n", - "Requirement already satisfied: jupyter-server<3,>=2.4.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.14.2)\n", - "Requirement already satisfied: jupyterlab-server<3,>=2.27.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.27.3)\n", - "Requirement already satisfied: jupyterlab<4.3,>=4.2.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (4.2.5)\n", - "Requirement already satisfied: notebook-shim<0.3,>=0.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.2.4)\n", - "Requirement already satisfied: pytz>=2020.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from pandas->polars[numpy,pandas]<0.21.0,>=0.20.10->investing_algorithm_framework) (2024.1)\n", - "Requirement already satisfied: tzdata>=2022.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from pandas->polars[numpy,pandas]<0.21.0,>=0.20.10->investing_algorithm_framework) (2024.1)\n", - "Requirement already satisfied: qtpy>=2.4.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from qtconsole->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.4.1)\n", - "Requirement already satisfied: webencodings in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from bleach!=5.0.0->nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.5.1)\n", - "Requirement already satisfied: pycparser in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from cffi>=1.12->cryptography>=2.6.1->ccxt<5.0.0,>=4.2.48->investing_algorithm_framework) (2.22)\n", - "Requirement already satisfied: decorator in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (5.1.1)\n", - "Requirement already satisfied: jedi>=0.16 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.19.1)\n", - "Requirement already satisfied: stack-data in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.6.3)\n", - "Requirement already satisfied: exceptiongroup in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.2.2)\n", - "Requirement already satisfied: pexpect>4.3 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from ipython>=7.23.1->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (4.9.0)\n", - "Requirement already satisfied: platformdirs>=2.5 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-core!=5.0.*,>=4.12->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (4.2.2)\n", - "Requirement already satisfied: anyio>=3.1.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (4.4.0)\n", - "Requirement already satisfied: argon2-cffi>=21.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (23.1.0)\n", - "Requirement already satisfied: jupyter-events>=0.9.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.10.0)\n", - "Requirement already satisfied: jupyter-server-terminals>=0.4.4 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.5.3)\n", - "Requirement already satisfied: overrides>=5.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (7.7.0)\n", - "Requirement already satisfied: prometheus-client>=0.9 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.20.0)\n", - "Requirement already satisfied: send2trash>=1.8.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.8.3)\n", - "Requirement already satisfied: terminado>=0.8.3 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.18.1)\n", - "Requirement already satisfied: websocket-client>=1.7 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.8.0)\n", - "Requirement already satisfied: async-lru>=1.0.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyterlab<4.3,>=4.2.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.0.4)\n", - "Requirement already satisfied: httpx>=0.25.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyterlab<4.3,>=4.2.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.27.2)\n", - "Requirement already satisfied: jupyter-lsp>=2.0.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyterlab<4.3,>=4.2.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.2.5)\n", - "Requirement already satisfied: tomli>=1.2.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyterlab<4.3,>=4.2.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.0.1)\n", - "Requirement already satisfied: babel>=2.10 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyterlab-server<3,>=2.27.1->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.16.0)\n", - "Requirement already satisfied: json5>=0.9.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyterlab-server<3,>=2.27.1->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.9.25)\n", - "Requirement already satisfied: jsonschema>=4.18.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyterlab-server<3,>=2.27.1->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (4.23.0)\n", - "Requirement already satisfied: fastjsonschema>=2.15 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from nbformat>=5.7->nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.20.0)\n", - "Requirement already satisfied: wcwidth in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from prompt-toolkit>=3.0.30->jupyter-console->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.2.13)\n", - "Requirement already satisfied: soupsieve>1.2 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from beautifulsoup4->nbconvert->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.6)\n", - "Requirement already satisfied: sniffio>=1.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from anyio>=3.1.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.3.1)\n", - "Requirement already satisfied: argon2-cffi-bindings in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from argon2-cffi>=21.1->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (21.2.0)\n", - "Requirement already satisfied: httpcore==1.* in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from httpx>=0.25.0->jupyterlab<4.3,>=4.2.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.0.5)\n", - "Requirement already satisfied: h11<0.15,>=0.13 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from httpcore==1.*->httpx>=0.25.0->jupyterlab<4.3,>=4.2.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.14.0)\n", - "Requirement already satisfied: parso<0.9.0,>=0.8.3 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jedi>=0.16->ipython>=7.23.1->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.8.4)\n", - "Requirement already satisfied: jsonschema-specifications>=2023.03.6 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jsonschema>=4.18.0->jupyterlab-server<3,>=2.27.1->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2023.12.1)\n", - "Requirement already satisfied: referencing>=0.28.4 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jsonschema>=4.18.0->jupyterlab-server<3,>=2.27.1->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.35.1)\n", - "Requirement already satisfied: rpds-py>=0.7.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jsonschema>=4.18.0->jupyterlab-server<3,>=2.27.1->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.20.0)\n", - "Requirement already satisfied: python-json-logger>=2.0.4 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.0.7)\n", - "Requirement already satisfied: pyyaml>=5.3 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (6.0.2)\n", - "Requirement already satisfied: rfc3339-validator in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.1.4)\n", - "Requirement already satisfied: rfc3986-validator>=0.1.1 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.1.1)\n", - "Requirement already satisfied: ptyprocess>=0.5 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from pexpect>4.3->ipython>=7.23.1->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.7.0)\n", - "Requirement already satisfied: executing>=1.2.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from stack-data->ipython>=7.23.1->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.1.0)\n", - "Requirement already satisfied: asttokens>=2.1.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from stack-data->ipython>=7.23.1->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.4.1)\n", - "Requirement already satisfied: pure-eval in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from stack-data->ipython>=7.23.1->ipykernel->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (0.2.3)\n", - "Requirement already satisfied: fqdn in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.5.1)\n", - "Requirement already satisfied: isoduration in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (20.11.0)\n", - "Requirement already satisfied: jsonpointer>1.13 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (3.0.0)\n", - "Requirement already satisfied: uri-template in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.3.0)\n", - "Requirement already satisfied: webcolors>=24.6.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (24.8.0)\n", - "Requirement already satisfied: arrow>=0.15.0 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from isoduration->jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (1.3.0)\n", - "Requirement already satisfied: types-python-dateutil>=2.8.10 in /Users/marcvanduyn/Library/Caches/pypoetry/virtualenvs/investing-algorithm-framework-ygOLr3-a-py3.9/lib/python3.9/site-packages (from arrow>=0.15.0->isoduration->jsonschema[format-nongpl]>=4.18.0->jupyter-events>=0.9.0->jupyter-server<3,>=2.4.0->notebook->jupyter<2.0.0,>=1.0.0->investing_algorithm_framework) (2.9.0.20240821)\n", - "\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.3.1\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m24.3.1\u001b[0m\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n" - ] - } - ], - "source": [ - "!{sys.executable} -m pip install investing_algorithm_framework" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -227,14 +67,6 @@ " !{sys.executable} -m pip install investing_algorithm_framework\n", "\n", "try:\n", - " import investing_algorithm_framework.indicators\n", - " print(f\"investing_algorithm_framork indicators plugin is already installed.\")\n", - "except ImportError:\n", - " print(\"investing_algorithm_framork indicators plugin is not installed. Installing...\")\n", - " import sys\n", - " !{sys.executable} -m pip install investing_algorithm_framework[indicators]\n", - "\n", - "try:\n", " import ipywidgets\n", " print(f\"ipywidgets is already installed.\")\n", "except ImportError:\n", @@ -312,15 +144,18 @@ "outputs": [], "source": [ "from datetime import datetime, timedelta\n", + "from investing_algorithm_framework import DateRange\n", "\n", - "start_date = datetime(year=2021, month=1, day=1)\n", - "end_date = datetime(year=2024, month=6, day=1)\n", - "total_date_range = (datetime(year=2022, month=1, day=1), datetime(year=2024, month=6, day=1))\n", + "total_date_range = DateRange(\n", + " start_date=datetime(year=2021, month=1, day=1), \n", + " end_date=datetime(year=2024, month=6, day=1), \n", + " name=\"Total date range\"\n", + ")\n", "\n", "# Our start date for our data is different then our start_date for our backtest range. This is because we will be using indicators such as the 200 sma, \n", "# which need to have atleast 200 data points before the start date of our backtest range. If we don't do this,\n", "# we can't calculate indicators such as the 200 sma for our strategy.\n", - "start_date_data = start_date - timedelta(days=200)" + "start_date_data = total_date_range.start_date - timedelta(days=200)" ] }, { @@ -410,8 +245,7 @@ "import ipywidgets as widgets\n", "from IPython.display import display\n", "import plotly.graph_objects as go\n", - "from investing_algorithm_framework.indicators import get_up_and_downtrends, get_sma\n", - "from investing_algorithm_framework import DateRange\n", + "from investing_algorithm_framework.indicators import get_sma\n", "\n", "# Add sma 50 and sma 200\n", "total_data_pandas_df = get_sma(total_data_pandas_df, period=50, source_column_name=\"Close\", result_column_name=\"SMA_50\")\n", @@ -980,7 +814,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "investing-algorithm-framework-ygOLr3-a-py3.9", "language": "python", "name": "python3" }, diff --git a/investing_algorithm_framework/__init__.py b/investing_algorithm_framework/__init__.py index 2fa42e83..fd8fae81 100644 --- a/investing_algorithm_framework/__init__.py +++ b/investing_algorithm_framework/__init__.py @@ -3,7 +3,7 @@ StatelessAction, Task from investing_algorithm_framework.domain import ApiException, \ TradingDataType, TradingTimeFrame, OrderType, OperationalException, \ - OrderStatus, OrderSide, Config, TimeUnit, TimeInterval, Order, Portfolio, \ + OrderStatus, OrderSide, TimeUnit, TimeInterval, Order, Portfolio, \ Position, TimeFrame, BACKTESTING_INDEX_DATETIME, MarketCredential, \ PortfolioConfiguration, RESOURCE_DIRECTORY, pretty_print_backtest, \ Trade, OHLCVMarketDataSource, OrderBookMarketDataSource, SYMBOLS, \ @@ -11,11 +11,11 @@ pretty_print_backtest_reports_evaluation, load_backtest_reports, \ RESERVED_BALANCES, APP_MODE, AppMode, DATETIME_FORMAT, \ load_backtest_report, BacktestDateRange, convert_polars_to_pandas, \ - DateRange, get_backtest_report + DateRange, get_backtest_report, DEFAULT_LOGGING_CONFIG from investing_algorithm_framework.infrastructure import \ CCXTOrderBookMarketDataSource, CCXTOHLCVMarketDataSource, \ CCXTTickerMarketDataSource, CSVOHLCVMarketDataSource, \ - CSVTickerMarketDataSource + CSVTickerMarketDataSource, AzureBlobStorageStateHandler from .create_app import create_app from investing_algorithm_framework.indicators import get_rsi, get_peaks, \ is_uptrend, is_downtrend, is_crossover, is_crossunder, is_above, \ @@ -34,7 +34,6 @@ "OrderType", "OrderStatus", "OrderSide", - "Config", "PortfolioConfiguration", "TimeUnit", "TimeInterval", @@ -88,5 +87,7 @@ "has_crossed_downward", "get_willr", "is_divergence", - "get_backtest_report" + "get_backtest_report", + "AzureBlobStorageStateHandler", + "DEFAULT_LOGGING_CONFIG" ] diff --git a/investing_algorithm_framework/app/algorithm.py b/investing_algorithm_framework/app/algorithm.py index c19396bc..8cc3ec16 100644 --- a/investing_algorithm_framework/app/algorithm.py +++ b/investing_algorithm_framework/app/algorithm.py @@ -23,13 +23,13 @@ class Algorithm: class is responsible for managing the strategies and executing them in the correct order. - :param (optional) name: The name of the algorithm - :param (optional) description: The description of the algorithm - :param (optional) context: The context of the algorithm, - for backtest references - :param (optional) strategy: A single strategy to add to the algorithm - :param (optional) data_sources: The list of data sources to - add to the algorithm + Args: + name (str): The name of the algorithm + description (str): The description of the algorithm + context (dict): The context of the algorithm, for backtest + references + strategy: A single strategy to add to the algorithm + data_sources: The list of data sources to add to the algorithm """ def __init__( self, @@ -135,13 +135,24 @@ def initialize_services( self._strategies ) - def start(self, number_of_iterations=None, stateless=False): + def start(self, number_of_iterations: int = None): + """ + Function to start the algorithm. + This function will start the algorithm by scheduling all + jobs in the strategy orchestrator service. The jobs are not + run immediately, but are scheduled to run in the future by the + app. - if not stateless: - self.strategy_orchestrator_service.start( - algorithm=self, - number_of_iterations=number_of_iterations - ) + Args: + number_of_iterations (int): (Optional) The number of iterations to run the algorithm + + Returns: + None + """ + self.strategy_orchestrator_service.start( + algorithm=self, + number_of_iterations=number_of_iterations + ) @property def name(self): @@ -229,17 +240,20 @@ def create_order( and execute it if the execute parameter is set to True. If the validate parameter is set to True, the order will be validated - :param target_symbol: The symbol of the asset to trade - :param price: The price of the asset - :param order_type: The type of the order - :param order_side: The side of the order - :param amount: The amount of the asset to trade - :param market: The market to trade the asset - :param execute: If set to True, the order will be executed - :param validate: If set to True, the order will be validated - :param sync: If set to True, the created order will be synced - with the portfolio of the algorithm. - :return: The order created + Args: + target_symbol: The symbol of the asset to trade + price: The price of the asset + order_type: The type of the order + order_side: The side of the order + amount: The amount of the asset to trade + market: The market to trade the asset + execute: If set to True, the order will be executed + validate: If set to True, the order will be validated + sync: If set to True, the created order will be synced + with the portfolio of the algorithm. + + Returns: + The order created """ portfolio = self.portfolio_service.find({"market": market}) order_data = { @@ -345,9 +359,8 @@ def create_limit_order( "Percentage of portfolio is only supported for BUY orders." ) - percentage_of_portfolio = percentage_of_portfolio net_size = portfolio.get_net_size() - size = net_size * percentage_of_portfolio / 100 + size = net_size * (percentage_of_portfolio / 100) amount = size / price elif percentage_of_position is not None: diff --git a/investing_algorithm_framework/app/app.py b/investing_algorithm_framework/app/app.py index 889f4dc2..2604c883 100644 --- a/investing_algorithm_framework/app/app.py +++ b/investing_algorithm_framework/app/app.py @@ -1,10 +1,8 @@ import inspect import logging import os -import shutil import threading from abc import abstractmethod -from distutils.sysconfig import get_python_lib from time import sleep from typing import List, Optional @@ -16,7 +14,7 @@ from investing_algorithm_framework.app.web import create_flask_app from investing_algorithm_framework.domain import DATABASE_NAME, TimeUnit, \ DATABASE_DIRECTORY_PATH, RESOURCE_DIRECTORY, ENVIRONMENT, Environment, \ - SQLALCHEMY_DATABASE_URI, OperationalException, BACKTESTING_FLAG, \ + SQLALCHEMY_DATABASE_URI, OperationalException, \ BACKTESTING_START_DATE, BACKTESTING_END_DATE, BacktestReport, \ BACKTESTING_PENDING_ORDER_CHECK_INTERVAL, APP_MODE, MarketCredential, \ AppMode, BacktestDateRange @@ -41,10 +39,9 @@ def on_run(self, app, algorithm: Algorithm): class App: - def __init__(self, stateless=False, web=False): + def __init__(self, state_handler=None, web=False): self._flask_app: Optional[Flask] = None self.container = None - self._stateless = stateless self._web = web self._algorithm: Optional[Algorithm] = None self._started = False @@ -56,6 +53,7 @@ def __init__(self, stateless=False, web=False): Optional[MarketCredentialService] = None self._on_initialize_hooks = [] self._on_after_initialize_hooks = [] + self._state_handler = state_handler def add_algorithm(self, algorithm: Algorithm) -> None: """ @@ -64,9 +62,13 @@ def add_algorithm(self, algorithm: Algorithm) -> None: """ self._algorithm = algorithm - def set_config(self, config: dict) -> None: + def set_config(self, key, value) -> None: configuration_service = self.container.configuration_service() - configuration_service.initialize_from_dict(config) + configuration_service.add_value(key, value) + + def set_config_with_dict(self, dictionary) -> None: + configuration_service = self.container.configuration_service() + configuration_service.add_dict(dictionary) def initialize_services(self) -> None: self._configuration_service = self.container.configuration_service() @@ -83,7 +85,64 @@ def algorithm(self) -> Algorithm: def algorithm(self, algorithm: Algorithm) -> None: self._algorithm = algorithm - def initialize(self, sync=False): + def initialize_config(self): + """ + Function to initialize the configuration for the app. This method + should be called before running the algorithm. + """ + logger.info("Initializing configuration") + configuration_service = self.container.configuration_service() + config = configuration_service.get_config() + + # Check if the resource directory is set + if RESOURCE_DIRECTORY not in config \ + or config[RESOURCE_DIRECTORY] is None: + logger.info( + "Resource directory not set, setting" + + " to current working directory" + ) + path = os.path.join(os.getcwd(), "resources") + configuration_service.add_value(RESOURCE_DIRECTORY, path) + + config = configuration_service.get_config() + logger.info(f"Resource directory set to {config[RESOURCE_DIRECTORY]}") + + if DATABASE_NAME not in config or config[DATABASE_NAME] is None: + configuration_service.add_value( + DATABASE_NAME, "prod-database.sqlite3" + ) + + config = configuration_service.get_config() + + if DATABASE_DIRECTORY_PATH not in config \ + or config[DATABASE_DIRECTORY_PATH] is None: + resource_dir = config[RESOURCE_DIRECTORY] + configuration_service.add_value( + DATABASE_DIRECTORY_PATH, + os.path.join(resource_dir, "databases") + ) + + config = configuration_service.get_config() + + if SQLALCHEMY_DATABASE_URI not in config \ + or config[SQLALCHEMY_DATABASE_URI] is None: + path = "sqlite:///" + os.path.join( + configuration_service.config[DATABASE_DIRECTORY_PATH], + configuration_service.config[DATABASE_NAME] + ) + configuration_service.add_value(SQLALCHEMY_DATABASE_URI, path) + + config = configuration_service.get_config() + + if APP_MODE not in config or config[APP_MODE] is None: + if self._web: + configuration_service.add_value(APP_MODE, AppMode.WEB.value) + else: + configuration_service.add_value( + APP_MODE, AppMode.DEFAULT.value + ) + + def initialize(self): """ Method to initialize the app. This method should be called before running the algorithm. It initializes the services and the algorithm @@ -91,11 +150,11 @@ def initialize(self, sync=False): Also, it initializes all required services for the algorithm. - Args: - sync (bool): Whether to sync the portfolio with the exchange Returns: None """ + logger.info("Initializing app") + if self.algorithm is None: raise OperationalException("No algorithm registered") @@ -122,26 +181,31 @@ def initialize(self, sync=False): trade_service=self.container.trade_service(), ) - if APP_MODE not in self.config: - if self._stateless: - self.config[APP_MODE] = AppMode.STATELESS.value - elif self._web: - self.config[APP_MODE] = AppMode.WEB.value - else: - self.config[APP_MODE] = AppMode.DEFAULT.value + # Ensure that all resource directories exist + self._create_resources_if_not_exists() + + # Setup the database + setup_sqlalchemy(self) + create_all_tables() - if AppMode.WEB.from_value(self.config[APP_MODE]): + # Initialize all market credentials + market_credential_service = self.container.market_credential_service() + market_credential_service.initialize() + + # Initialize all market data sources from registered the strategies + market_data_source_service = \ + self.container.market_data_source_service() + + for strategy in self.algorithm.strategies: + + for market_data_source in strategy.market_data_sources: + market_data_source_service.add(market_data_source) + + if self._web: + self._configuration_service.add_value( + APP_MODE, AppMode.WEB.value + ) self._initialize_web() - setup_sqlalchemy(self) - create_all_tables() - elif AppMode.STATELESS.from_value(self.config[APP_MODE]): - self._initialize_stateless() - setup_sqlalchemy(self) - create_all_tables() - else: - self._initialize_standard() - setup_sqlalchemy(self) - create_all_tables() # Initialize all portfolios that are registered portfolio_configuration_service = self.container \ @@ -165,16 +229,13 @@ def initialize(self, sync=False): .create_portfolio_from_configuration( portfolio_configuration ) - # self.sync(portfolio) - # synced_portfolios.append(portfolio) - if sync: - portfolios = portfolio_service.get_all() + portfolios = portfolio_service.get_all() - for portfolio in portfolios: + for portfolio in portfolios: - if portfolio not in synced_portfolios: - self.sync(portfolio) + if portfolio not in synced_portfolios: + self.sync(portfolio) def sync(self, portfolio): """ @@ -182,38 +243,25 @@ def sync(self, portfolio): before running the algorithm. It syncs the portfolio with the exchange by syncing the unallocated balance, positions, orders, and trades. + + Args: + portfolio (Portfolio): The portfolio to sync + + Returns: + None """ + logger.info(f"Syncing portfolio {portfolio.identifier}") portfolio_sync_service = self.container.portfolio_sync_service() # Sync unallocated balance portfolio_sync_service.sync_unallocated(portfolio) - # Sync all positions from exchange with current - # position history - portfolio_sync_service.sync_positions(portfolio) - # Sync all orders from exchange with current order history portfolio_sync_service.sync_orders(portfolio) # Sync all trades from exchange with current trade history portfolio_sync_service.sync_trades(portfolio) - def _initialize_stateless(self): - """ - Initialize the app for stateless mode by setting the configuration - parameters for stateless mode and overriding the services with the - stateless services equivalents. - - In stateless mode, sqlalchemy is-setup with an in-memory database. - - Stateless has the following implications: - db: in-memory - web: False - app: Run with stateless action objects - algorithm: Run with stateless action objects - """ - configuration_service = self.container.configuration_service() - configuration_service.config[SQLALCHEMY_DATABASE_URI] = "sqlite://" def _initialize_standard(self): """ @@ -266,16 +314,21 @@ def _initialize_app_for_backtest( """ # Set all config vars for backtesting configuration_service = self.container.configuration_service() - configuration_service.config[BACKTESTING_FLAG] = True - configuration_service.config[BACKTESTING_START_DATE] = \ - backtest_date_range.start_date - configuration_service.config[BACKTESTING_END_DATE] = \ - backtest_date_range.end_date + configuration_service.add_value( + ENVIRONMENT, Environment.BACKTEST.value + ) + configuration_service.add_value( + BACKTESTING_START_DATE, backtest_date_range.start_date + ) + configuration_service.add_value( + BACKTESTING_END_DATE, backtest_date_range.end_date + ) if pending_order_check_interval is not None: - configuration_service.config[ - BACKTESTING_PENDING_ORDER_CHECK_INTERVAL - ] = pending_order_check_interval + configuration_service.add_value( + BACKTESTING_PENDING_ORDER_CHECK_INTERVAL, + pending_order_check_interval + ) # Create resource dir if not exits self._create_resource_directory_if_not_exists() @@ -296,10 +349,14 @@ def _create_backtest_database_if_not_exists(self): resource_dir = configuration_service.config[RESOURCE_DIRECTORY] # Create the database if not exists - configuration_service.config[DATABASE_DIRECTORY_PATH] = \ + configuration_service.add_value( + DATABASE_NAME, "backtest-database.sqlite3" + ) + configuration_service.add_value( + DATABASE_DIRECTORY_PATH, os.path.join(resource_dir, "databases") - configuration_service.config[DATABASE_NAME] = \ - "backtest-database.sqlite3" + ) + database_path = os.path.join( configuration_service.config[DATABASE_DIRECTORY_PATH], configuration_service.config[DATABASE_NAME] @@ -308,11 +365,15 @@ def _create_backtest_database_if_not_exists(self): if os.path.exists(database_path): os.remove(database_path) - configuration_service.config[SQLALCHEMY_DATABASE_URI] = \ + sql_alchemy_uri = \ "sqlite:///" + os.path.join( configuration_service.config[DATABASE_DIRECTORY_PATH], configuration_service.config[DATABASE_NAME] ) + + configuration_service.add_value( + SQLALCHEMY_DATABASE_URI, sql_alchemy_uri + ) self._create_database_if_not_exists() setup_sqlalchemy(self) create_all_tables() @@ -460,97 +521,100 @@ def _initialize_algorithm_for_backtest(self, algorithm): portfolio_configuration ) - def _initialize_management_commands(self): - - if not Environment.TEST.equals(self.config.get(ENVIRONMENT)): - # Copy the template manage.py file to the resource directory of the - # algorithm - management_commands_template = os.path.join( - get_python_lib(), - "investing_algorithm_framework/templates/manage.py" - ) - destination = os.path.join( - self.config.get(RESOURCE_DIRECTORY), "manage.py" - ) - - if not os.path.exists(destination): - shutil.copy(management_commands_template, destination) - def run( self, payload: dict = None, number_of_iterations: int = None, - sync=False ): """ Entry point to run the application. This method should be called to - start the algorithm. The method runs the algorithm for the specified - number of iterations and handles the payload if the app is running in - stateless mode. + start the algorithm. This method can be called in three modes: + + - Without any params: In this mode, the app runs until a keyboard + interrupt is received. This mode is useful when running the app in + a loop. + - With a payload: In this mode, the app runs only once with the + payload provided. This mode is useful when running the app in a + one-off mode, such as running the app from the command line or + on a schedule. Payload is a dictionary that contains the data to + handle for the algorithm. This data should look like this: + { + "action": "RUN_STRATEGY", + } + - With a number of iterations: In this mode, the app runs for the + number of iterations provided. This mode is useful when running the + app in a loop for a fixed number of iterations. + + This function first checks if there is an algorithm registered. If not, it raises an OperationalException. Then it initializes the algorithm with the services and the configuration. - First the app checks if there is an algorithm registered. If not, it - raises an OperationalException. Then it initializes the algorithm - with the services and the configuration. - - If the app is running in stateless mode, it handles the - payload. If the app is running in web mode, it starts the web app in a - separate thread. Args: - payload (dict): The payload to handle if the app is running in - stateless mode + payload (dict): The payload to handle for the algorithm number_of_iterations (int): The number of iterations to run the algorithm for - sync (bool): Whether to sync the portfolio with the exchange Returns: None """ - # Run all on_initialize hooks - for hook in self._on_after_initialize_hooks: - hook.on_run(self, self.algorithm) - - self.initialize(sync=sync) - - # Run all on_initialize hooks - for hook in self._on_initialize_hooks: - hook.on_run(self, self.algorithm) - - self.algorithm.start( - number_of_iterations=number_of_iterations, - stateless=self.stateless - ) - - if AppMode.STATELESS.equals(self.config[APP_MODE]): - logger.info("Running stateless") - action_handler = ActionHandler.of(payload) - return action_handler.handle( - payload=payload, algorithm=self.algorithm - ) - elif AppMode.WEB.equals(self.config[APP_MODE]): - logger.info("Running web") - flask_thread = threading.Thread( - name='Web App', - target=self._flask_app.run, - kwargs={"port": 8080} - ) - flask_thread.setDaemon(True) - flask_thread.start() - - number_of_iterations_since_last_orders_check = 1 - self.algorithm.check_pending_orders() - try: - while self.algorithm.running: - if number_of_iterations_since_last_orders_check == 30: - logger.info("Checking pending orders") - number_of_iterations_since_last_orders_check = 1 + self.initialize_config() + + # Load the state if a state handler is provided + if self._state_handler is not None: + logger.info("Detected state handler, loading state") + config = self.container.configuration_service().get_config() + self._state_handler.load(config[RESOURCE_DIRECTORY]) + + self.initialize() + logger.info("Initialization complete") + + # Run all on_initialize hooks + for hook in self._on_initialize_hooks: + hook.on_run(self, self.algorithm) + + configuration_service = self.container.configuration_service() + config = configuration_service.get_config() + + # Run in payload mode if payload is provided + if payload is not None: + logger.info("Running with payload") + action_handler = ActionHandler.of(payload) + response = action_handler.handle( + payload=payload, algorithm=self.algorithm + ) + return response + + if AppMode.WEB.equals(config[APP_MODE]): + logger.info("Running web") + flask_thread = threading.Thread( + name='Web App', + target=self._flask_app.run, + kwargs={"port": 8080} + ) + flask_thread.setDaemon(True) + flask_thread.start() + + self.algorithm.start(number_of_iterations=number_of_iterations) + number_of_iterations_since_last_orders_check = 1 + self.algorithm.check_pending_orders() - self.algorithm.run_jobs() - number_of_iterations_since_last_orders_check += 1 - sleep(1) - except KeyboardInterrupt: - exit(0) + try: + while self.algorithm.running: + if number_of_iterations_since_last_orders_check == 30: + logger.info("Checking pending orders") + number_of_iterations_since_last_orders_check = 1 + + self.algorithm.run_jobs() + number_of_iterations_since_last_orders_check += 1 + sleep(1) + except KeyboardInterrupt: + exit(0) + finally: + # Upload state if state handler is provided + if self._state_handler is not None: + logger.info("Detected state handler, saving state") + config = self.container.configuration_service().get_config() + self._state_handler.save(config[RESOURCE_DIRECTORY]) @property def started(self): @@ -580,10 +644,6 @@ def add_portfolio_configuration(self, portfolio_configuration): .portfolio_configuration_service() portfolio_configuration_service.add(portfolio_configuration) - @property - def stateless(self): - return self._stateless - @property def web(self): return self._web @@ -633,39 +693,26 @@ def _initialize_web(self): - Algorithm """ configuration_service = self.container.configuration_service() - resource_dir = configuration_service.config[RESOURCE_DIRECTORY] + self._flask_app = create_flask_app(configuration_service) - if resource_dir is None: - configuration_service.config[SQLALCHEMY_DATABASE_URI] = "sqlite://" - else: - resource_dir = self._create_resource_directory_if_not_exists() - configuration_service.config[DATABASE_DIRECTORY_PATH] = \ - os.path.join(resource_dir, "databases") - configuration_service.config[DATABASE_NAME] \ - = "prod-database.sqlite3" - configuration_service.config[SQLALCHEMY_DATABASE_URI] = \ - "sqlite:///" + os.path.join( - configuration_service.config[DATABASE_DIRECTORY_PATH], - configuration_service.config[DATABASE_NAME] - ) - self._create_database_if_not_exists() - - self._flask_app = create_flask_app(configuration_service.config) - - def _create_resource_directory_if_not_exists(self): - - if self._stateless: - return + def _create_resources_if_not_exists(self): + """ + Function to create the resources required by the app if they do not exist. This function will check if the resource directory exists and + check if the database directory exists. If they do not exist, it will create them. + Returns: + None + """ configuration_service = self.container.configuration_service() - resource_dir = configuration_service.config.get( - RESOURCE_DIRECTORY, None - ) + config = configuration_service.get_config() + resource_dir = config[RESOURCE_DIRECTORY] + database_dir = config[DATABASE_DIRECTORY_PATH] if resource_dir is None: raise OperationalException( - "Resource directory is not specified. " - "A resource directory is required for running a backtest." + "Resource directory is not specified in the config, please " + "specify the resource directory in the config with the key " + "RESOURCE_DIRECTORY" ) if not os.path.isdir(resource_dir): @@ -677,13 +724,16 @@ def _create_resource_directory_if_not_exists(self): "Could not create resource directory" ) - return resource_dir + if not os.path.isdir(database_dir): + try: + os.makedirs(database_dir) + except OSError as e: + logger.error(e) + raise OperationalException( + "Could not create database directory" + ) def _create_database_if_not_exists(self): - - if self._stateless: - return - configuration_service = self.container.configuration_service() database_dir = configuration_service.config \ .get(DATABASE_DIRECTORY_PATH, None) @@ -691,7 +741,8 @@ def _create_database_if_not_exists(self): if database_dir is None: return - database_name = configuration_service.config.get(DATABASE_NAME, None) + config = configuration_service.get_config() + database_name = config[DATABASE_NAME] if database_name is None: return @@ -750,9 +801,9 @@ def run_backtest( algorithm=self.algorithm ) backtest_service = self.container.backtest_service() - backtest_service.resource_directory = self.config.get( - RESOURCE_DIRECTORY - ) + configuration_service = self.container.configuration_service() + config = configuration_service.get_config() + backtest_service.resource_directory = config[RESOURCE_DIRECTORY] # Run the backtest with the backtest_service and collect the report report = backtest_service.run_backtest( @@ -763,8 +814,7 @@ def run_backtest( if output_directory is None: output_directory = os.path.join( - self.config.get(RESOURCE_DIRECTORY), - "backtest_reports" + config[RESOURCE_DIRECTORY], "backtest_reports" ) backtest_report_writer_service.write_report_to_json( @@ -847,9 +897,9 @@ def run_backtests( self._initialize_algorithm_for_backtest(algorithm) backtest_service = self.container.backtest_service() - backtest_service.resource_directory = self.config.get( + backtest_service.resource_directory = self.config[ RESOURCE_DIRECTORY - ) + ] # Run the backtest with the backtest_service # and collect the report @@ -866,8 +916,7 @@ def run_backtests( if output_directory is None: output_directory = os.path.join( - self.config.get(RESOURCE_DIRECTORY), - "backtest_reports" + self.config[RESOURCE_DIRECTORY], "backtest_reports" ) backtest_report_writer_service.write_report_to_json( diff --git a/investing_algorithm_framework/app/web/__init__.py b/investing_algorithm_framework/app/web/__init__.py index c3b5dfba..b5ed007f 100644 --- a/investing_algorithm_framework/app/web/__init__.py +++ b/investing_algorithm_framework/app/web/__init__.py @@ -1,4 +1,5 @@ from .create_app import create_flask_app from .run_strategies import run_strategies +from .schemas import OrderSerializer -__all__ = ["create_flask_app", "run_strategies"] +__all__ = ["create_flask_app", "run_strategies", 'OrderSerializer'] diff --git a/investing_algorithm_framework/app/web/create_app.py b/investing_algorithm_framework/app/web/create_app.py index e82badf4..9a1ec0af 100644 --- a/investing_algorithm_framework/app/web/create_app.py +++ b/investing_algorithm_framework/app/web/create_app.py @@ -5,10 +5,12 @@ from .error_handler import setup_error_handler -def create_flask_app(config): +def create_flask_app(configuration_service): app = Flask(__name__.split('.')[0]) - for key, value in config.items(): + flask_config = configuration_service.get_flask_config() + + for key, value in flask_config.items(): app.config[key] = value app = setup_cors(app) diff --git a/investing_algorithm_framework/deployment/__init__.py b/investing_algorithm_framework/cli/__init__.py similarity index 100% rename from investing_algorithm_framework/deployment/__init__.py rename to investing_algorithm_framework/cli/__init__.py diff --git a/investing_algorithm_framework/cli/create_azure_function_app_skeleton.py b/investing_algorithm_framework/cli/create_azure_function_app_skeleton.py new file mode 100644 index 00000000..a9868d83 --- /dev/null +++ b/investing_algorithm_framework/cli/create_azure_function_app_skeleton.py @@ -0,0 +1,118 @@ +import os +import click + + +def create_file_from_template(template_path, output_path): + """ + Creates a new file by replacing placeholders in a template file. + + Args: + template_path (str): The path to the template file. + output_path (str): The path to the output file. + replacements (dict): A dictionary of placeholder keys and their replacements. + + Returns: + None + """ + with open(template_path, "r") as file: + template = file.read() + + with open(output_path, "w") as file: + file.write(template) + +def create_azure_function_skeleton( + add_app_template, add_requirements_template +): + """ + Function to create an azure function app skeleton. + + Args: + create_app_skeleton (bool): Flag to create an app skeleton. + + Returns: + None + """ + + # Get current working directory + cwd = os.getcwd() + + # Get the path of this script (command.py) + current_script_path = os.path.abspath(__file__) + + # Construct the path to the template file + template_host_file_path = os.path.join( + os.path.dirname(current_script_path), + "templates", + "azure_function_host.json.template" + ) + template_settings_path = os.path.join( + os.path.dirname(current_script_path), + "templates", + "azure_function_local.settings.json.template" + ) + function_app_path = os.path.join( + os.path.dirname(current_script_path), + "templates", + "azure_function_function_app.py.template" + ) + + if add_app_template: + function_app_path = os.path.join( + os.path.dirname(current_script_path), + "templates", + "azure_function_framework_app.py.template" + ) + + if add_requirements_template: + requirements_path = os.path.join( + os.path.dirname(current_script_path), + "templates", + "azure_function_requirements.txt.template" + ) + + create_file_from_template( + template_host_file_path, + os.path.join(cwd, "host.json") + ) + create_file_from_template( + template_settings_path, + os.path.join(cwd, "local.settings.json") + ) + create_file_from_template( + function_app_path, + os.path.join(cwd, "function_app.py") + ) + create_file_from_template( + requirements_path, + os.path.join(cwd, "requirements.txt") + ) + print( + f"Function App trading bot skeleton creation completed" + ) + + +@click.command() +@click.option( + '--add-app-template', + is_flag=True, + help='Flag to create an framework app skeleton', + default=False +) +@click.option( + '--add-requirements-template', + is_flag=True, + help='Flag to create an framework app skeleton', + default=True +) +def cli(add_app_template, add_requirements_template): + """ + Command-line tool for creating an azure function enabled app skeleton. + + Args: + add_app_template (bool): Flag to create an app skeleton. + add_requirements_template (bool): Flag to create a requirements template. + + Returns: + None + """ + create_azure_function_skeleton(add_app_template, add_requirements_template) diff --git a/investing_algorithm_framework/cli/deploy_to_azure_function.py b/investing_algorithm_framework/cli/deploy_to_azure_function.py new file mode 100644 index 00000000..79bb820f --- /dev/null +++ b/investing_algorithm_framework/cli/deploy_to_azure_function.py @@ -0,0 +1,651 @@ +import os +import subprocess +import re +import click +import random +import string +import asyncio + +from azure.identity import DefaultAzureCredential +from azure.mgmt.resource import ResourceManagementClient +from azure.mgmt.storage import StorageManagementClient +from azure.mgmt.web import WebSiteManagementClient + +STORAGE_ACCOUNT_NAME_PREFIX = "iafstorageaccount" + + +def generate_unique_resource_name(base_name): + """ + Function to generate a unique resource name by appending a random suffix. + + Args: + base_name (str): The base name for the resource. + + Returns: + str: The unique resource name. + """ + unique_suffix = ''.join( + random.choices(string.ascii_lowercase + string.digits, k=6) + ) + return f"{base_name}{unique_suffix}".lower() + + +def ensure_azure_functools(): + """ + Function to ensure that the Azure Functions Core Tools are installed. + If not, it will prompt the user to install it. + """ + + try: + result = subprocess.run( + ["func", "--version"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + if result.returncode != 0: + raise FileNotFoundError("Azure Functions Core Tools not found.") + except FileNotFoundError: + print("Azure Functions Core Tools not found. Please install it.") + print("You can install it using the following command:") + print("npm install -g azure-functions-core-tools@4 --unsafe-perm true") + exit(1) + + +async def publish_function_app( + function_app_name, + storage_connection_string, + storage_container_name, + resource_group_name +): + """ + Function to publish the Function App using Azure Functions Core Tools. + + Args: + function_app_name (str): Name of the Function App to publish. + storage_connection_string (str): Azure Storage Connection String. + storage_container_name (str): Azure Storage Container Name. + resource_group_name (str): Resource Group Name. + + Returns: + None + """ + print(f"Publishing Function App {function_app_name}") + + try: + # Step 1: Publish the Azure Function App + process = await asyncio.create_subprocess_exec( + "func", "azure", "functionapp", "publish", function_app_name + ) + + # Wait for the subprocess to finish + _, stderr = await process.communicate() + + # Check the return code + if process.returncode != 0: + + if stderr is not None: + raise Exception( + f"Error publishing Function App: {stderr.decode().strip()}" + ) + else: + raise Exception("Error publishing Function App") + + print(f"Function App {function_app_name} published successfully.") + + # Step 2: Add app settings + add_settings_process = await asyncio.create_subprocess_exec( + "az", "functionapp", "config", "appsettings", "set", + "--name", function_app_name, + "--settings", + f"AZURE_STORAGE_CONNECTION_STRING={storage_connection_string}", + f"AZURE_STORAGE_CONTAINER_NAME={storage_container_name}", + f"--resource-group", resource_group_name + ) + _, stderr1 = await add_settings_process.communicate() + + if add_settings_process.returncode != 0: + + if stderr1 is not None: + raise Exception( + f"Error adding App settings: {stderr1.decode().strip()}" + ) + else: + raise Exception("Error adding App settings") + + print( + f"Added app settings to the Function App successfully" + ) + + # Step 3: Update the cors settings + cors_process = await asyncio.create_subprocess_exec( + "az", "functionapp", "cors", "add", + "--name", function_app_name, + "--allowed-origins", "*", + "--resource-group", resource_group_name + ) + + _, stderr1 = await add_settings_process.communicate() + + if cors_process.returncode != 0: + + if stderr1 is not None: + raise Exception( + f"Error adding cors settings: {stderr1.decode().strip()}" + ) + else: + raise Exception("Error adding cors settings") + + print("All app settings have been added successfully.") + print(f"Function App creation completed successfully.") + except Exception as e: + print(f"Error publishing Function App: {e}") + + +async def create_function_app( + resource_group_name, + deployment_name, + storage_account_name, + region +): + """ + Creates an Azure Function App in a Consumption Plan and deploys a Python Function. + + Args: + resource_group_name (str): Resource group name. + deployment_name (str): Name of the Function App to create. + storage_account_name (str): Name of the associated Storage Account. + region (str): Azure region (e.g., "eastus"). + + Returns: + dict: Details of the created or existing Function App. + """ + # Check if the Function App already exists + print(f"Checking if Function App '{deployment_name}' exists...") + + try: + # Check for the Function App + check_process = await asyncio.create_subprocess_exec( + "az", + "functionapp", + "show", + "--name", + deployment_name, + "--resource-group", + resource_group_name, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await check_process.communicate() + + if check_process.returncode == 0: + # The Function App exists, return details + print(f"Function App '{deployment_name}' already exists.") + return stdout.decode() + + # If the return code is non-zero, and the error indicates the Function App doesn't exist, proceed to create it + if "ResourceNotFound" in stderr.decode(): + print(f"Function App '{deployment_name}' does not exist. Proceeding to create it...") + else: + # If the error is something else, raise it + print(f"Error checking for Function App: {stderr.decode()}") + raise Exception(stderr.decode()) + + # Create the Function App + print(f"Creating Function App '{deployment_name}'...") + create_process = await asyncio.create_subprocess_exec( + "az", + "functionapp", + "create", + "--resource-group", + resource_group_name, + "--consumption-plan-location", + region, + "--runtime", + "python", + "--runtime-version", + "3.10", + "--functions-version", + "4", + "--name", + deployment_name, + "--os-type", + "linux", + "--storage-account", + storage_account_name + ) + + # Wait for the subprocess to finish + _, create_stderr = await create_process.communicate() + + # Check the return code for the create command + if create_process.returncode != 0: + print(f"Error creating Function App: {create_stderr.decode().strip()}") + raise Exception(f"Error creating Function App: {create_stderr.decode().strip()}") + + print(f"Function App '{deployment_name}' created successfully.") + return {"status": "created"} + + except Exception as e: + print(f"Error creating Function App: {e}") + raise e + + +def create_file_from_template(template_path, output_path): + """ + Creates a new file by replacing placeholders in a template file. + + Args: + template_path (str): The path to the template file. + output_path (str): The path to the output file. + replacements (dict): A dictionary of placeholder keys and their replacements. + + Returns: + None + """ + with open(template_path, "r") as file: + template = file.read() + + with open(output_path, "w") as file: + file.write(template) + + +def ensure_consumption_plan( + resource_group_name, + plan_name, + region, + subscription_id, + credential +): + """ + Ensures that an App Service Plan with the Consumption Plan exists. If not, creates it. + + Args: + resource_group_name (str): The name of the resource group. + plan_name (str): The name of the App Service Plan. + region (str): The Azure region for the resources. + subscription_id (str): The Azure subscription ID. + + Returns: + object: The App Service Plan object. + """ + web_client = WebSiteManagementClient(credential, subscription_id) + + try: + print( + f"Checking if App Service Plan '{plan_name}' exists in resource group '{resource_group_name}'..." + ) + plan = web_client.app_service_plans.get(resource_group_name, plan_name) + print(f"App Service Plan '{plan_name}' already exists.") + except Exception: # Plan does not exist + print( + f"App Service Plan '{plan_name}' not found. Creating it as a Consumption Plan..." + ) + plan = web_client.app_service_plans.begin_create_or_update( + resource_group_name, + plan_name, + { + "location": region, + "sku": {"name": "Y1", "tier": "Dynamic"}, + "kind": "functionapp", # Mark this as for Function Apps + "properties": {} + }, + ).result() + print(f"App Service Plan '{plan_name}' created successfully.") + return plan + +def ensure_storage_account( + storage_account_name, + resource_group_name, + region, + subscription_id, + credential, +): + """ + Checks if a storage account exists. If it doesn't, creates it. + + If no storage account name is provided, a unique name will be generated. + However, before we create a new storage account, we check if there a storage account exists with the prefix 'iafstorageaccount'. If it exists, we use that storage account. + + Args: + storage_account_name (str): The name of the storage account. + resource_group_name (str): The name of the resource group. + region (str): The Azure region for the resources. + subscription_id (str): The Azure subscription ID. + credential: Azure credentials object. + + Returns: + StorageAccount: The created storage account object. + """ + # Create Storage Management Client + storage_client = StorageManagementClient(credential, subscription_id) + + # Check if the storage account exists + try: + + # Check if provided storage account name has prefix 'iafstorageaccount' + if storage_account_name.startswith(STORAGE_ACCOUNT_NAME_PREFIX): + # List all storage accounts in the resource group + storage_accounts = storage_client\ + .storage_accounts.list_by_resource_group( + resource_group_name + ) + + for account in storage_accounts: + if account.name.startswith(STORAGE_ACCOUNT_NAME_PREFIX): + storage_account_name = account.name + break + + storage_client.storage_accounts.get_properties( + resource_group_name, + storage_account_name, + ) + print(f"Storage account '{storage_account_name}' already exists.") + account_key = storage_client.storage_accounts.list_keys( + resource_group_name, + storage_account_name, + ).keys[1].value + connection_string = f"DefaultEndpointsProtocol=https;AccountName={storage_account_name};AccountKey={account_key};EndpointSuffix=core.windows.net" + return connection_string, storage_account_name + except Exception as e: # If the storage account does not exist + print("Creating storage account ...") + + # Create storage account + storage_async_operation = storage_client.storage_accounts.begin_create( + resource_group_name, + storage_account_name, + { + "location": region, + "sku": {"name": "Standard_LRS"}, + "kind": "StorageV2", + }, + ) + storage_async_operation.result() + + if storage_async_operation.status() == "Succeeded": + print(f"Storage account '{storage_account_name}' created successfully.") + + account_key = storage_client.storage_accounts.list_keys( + resource_group_name, + storage_account_name, + ).keys[1].value + connection_string = f"DefaultEndpointsProtocol=https;AccountName={storage_account_name};AccountKey={account_key};EndpointSuffix=core.windows.net" + return connection_string, storage_account_name + + +def ensure_az_login(skip_check=False): + """ + Ensures the user is logged into Azure using `az login`. + If not logged in, it will prompt the user to log in. + + Raises: + Exception: An error occurred during the login process. + """ + + if skip_check: + return + + result = subprocess.run(["az", "login"], check=True) + + if result.returncode != 0: + raise Exception("An error occurred during 'az login'.") + + +def get_default_subscription_id(): + """ + Fetches the default subscription ID using Azure CLI. + + Returns: + str: The default subscription ID. + """ + print("Fetching default subscription ID...") + + # Check if an default subscription ID is set in the environment + if "AZURE_SUBSCRIPTION_ID" in os.environ: + return os.environ["AZURE_SUBSCRIPTION_ID"] + + try: + print( + "If you want to use a different subscription, please provide the" + " subscription ID with the '--subscription_id' option or" + " by setting the 'AZURE_SUBSCRIPTION_ID' environment variable." + ) + result = subprocess.run( + ["az", "account", "show", "--query", "id", "-o", "tsv"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + check=True, + ) + subscription_id = result.stdout.strip() + print(f"Default subscription ID: {subscription_id}") + return subscription_id + except subprocess.CalledProcessError as e: + print("Error fetching default subscription ID. Please log in with 'az login'.") + raise + +def ensure_resource_group( + resource_group_name, + region, + subscription_id, + create_if_not_exists +): + """ + Checks if a resource group exists. If it doesn't, creates it if `create_if_not_exists` is True. + + Args: + resource_group_name (str): The name of the resource group. + region (str): The Azure region for the resources. + subscription_id (str): The Azure subscription ID. + create_if_not_exists (bool): Flag to create the resource group if it does not exist. + + Returns: + None + """ + credential = DefaultAzureCredential() + resource_client = ResourceManagementClient(credential, subscription_id) + + print(f"Checking if resource group '{resource_group_name}' exists...") + try: + resource_group = resource_client.resource_groups.get(resource_group_name) + print(f"Resource group '{resource_group_name}' already exists.") + except Exception: # If the resource group does not exist + + try: + if create_if_not_exists: + print(f"Resource group '{resource_group_name}' not found. Creating it...") + resource_client.resource_groups.create_or_update( + resource_group_name, + {"location": region}, + ) + print(f"Resource group '{resource_group_name}' created successfully.") + else: + print(f"Resource group '{resource_group_name}' does not exist, and 'create_if_not_exists' is False.") + raise ValueError(f"Resource group '{resource_group_name}' does not exist.") + except Exception as e: + raise Exception(f"Error creating resource group: {e}") + + +def create_storage_and_function( + resource_group_name, + storage_account_name, + container_name, + deployment_name, + region, + subscription_id=None, + create_resource_group_if_not_exists=False, + skip_login=False +): + + # Make sure that the deployment name only contains lowercase letters, and + # uppercase letters + regex = r"^[a-zA-Z0-9]+$" + if not re.match(regex, deployment_name): + raise ValueError( + "--deployment_name can only contain " + + "letters (uppercase and lowercase)." + ) + # Get current working directory + cwd = os.getcwd() + + # Get the path of this script (command.py) + current_script_path = os.path.abspath(__file__) + + # Construct the path to the template file + template_host_file_path = os.path.join( + os.path.dirname(current_script_path), + "templates", + "azure_function_host.json.template" + ) + template_settings_path = os.path.join( + os.path.dirname(current_script_path), + "templates", + "azure_function_local.settings.json.template" + ) + + create_file_from_template( + template_host_file_path, os.path.join(cwd, "host.json") + ) + + create_file_from_template( + template_settings_path, os.path.join(cwd, "local.settings.json") + ) + + # Ensure the user is logged in + ensure_az_login(skip_check=skip_login) + + # ensure_azure_functools() + ensure_azure_functools() + + # Fetch default subscription ID if not provided + if not subscription_id: + subscription_id = get_default_subscription_id() + + # Authenticate using DefaultAzureCredential (requires environment variables or Azure CLI login) + credential = DefaultAzureCredential() + + # Check if the resource group exists + ensure_resource_group( + resource_group_name, + region, + subscription_id, + create_resource_group_if_not_exists + ) + + if storage_account_name is None: + storage_account_name = \ + generate_unique_resource_name(STORAGE_ACCOUNT_NAME_PREFIX) + + # Ensure storage account exists + storage_account_connection_string, storage_account_name = ensure_storage_account( + storage_account_name, + resource_group_name, + region, + subscription_id, + credential + ) + + # Create Function App + asyncio.run( + create_function_app( + resource_group_name=resource_group_name, + deployment_name=deployment_name, + region=region, + storage_account_name=storage_account_name + ) + ) + + # Publish Function App + asyncio.run( + publish_function_app( + function_app_name=deployment_name, + storage_connection_string=storage_account_connection_string, + storage_container_name=container_name, + resource_group_name=resource_group_name + ) + ) + + print( + f"Function App '{deployment_name}' deployment" + + "completed successfully." + ) + + +@click.command() +@click.option( + '--resource_group', + required=True, + help='The name of the resource group.', +) +@click.option( + '--subscription_id', + required=False, + help='The subscription ID. If not provided, the default will be used.' +) +@click.option( + '--storage_account_name', + required=False, + help='The name of the storage account.', +) +@click.option( + '--container_name', + required=False, + help='The name of the blob container.', + default='iafcontainer' +) +@click.option( + '--deployment_name', + required=True, + help='The name of the deployment. This will be used as the name of the Function App.' +) +@click.option( + '--region', + required=True, + help='The Azure region for the resources.' +) +@click.option( + '--create_resource_group_if_not_exists', + is_flag=True, + help='Flag to create the resource group if it does not exist.' +) +@click.option( + '--skip_login', + is_flag=True, + help='Flag to create the resource group if it does not exist.', + default=False +) +def cli( + resource_group, + subscription_id, + storage_account_name, + container_name, + deployment_name, + region, + create_resource_group_if_not_exists, + skip_login +): + """ + Command-line tool for creating an Azure storage account, blob container, and Function App. + + Args: + resource_group (str): The name of the resource group. + subscription_id (str): The Azure subscription ID. + storage_account_name (str): The name of the storage account. + container_name (str): The name of the blob container. + function_app (str): The name of the Azure Function App. + region (str): The Azure region for the resources. + create_resource_group_if_not_exists (bool): Flag to create the resource group if it does not exist. + + Returns: + None + """ + create_storage_and_function( + resource_group_name=resource_group, + storage_account_name=storage_account_name, + container_name=container_name, + deployment_name=deployment_name, + region=region, + subscription_id=subscription_id, + skip_login=skip_login, + create_resource_group_if_not_exists=create_resource_group_if_not_exists + ) diff --git a/investing_algorithm_framework/cli/templates/azure_function_framework_app.py.template b/investing_algorithm_framework/cli/templates/azure_function_framework_app.py.template new file mode 100644 index 00000000..ee2d1cbd --- /dev/null +++ b/investing_algorithm_framework/cli/templates/azure_function_framework_app.py.template @@ -0,0 +1,49 @@ +from dotenv import load_dotenv + +from investing_algorithm_framework import create_app, PortfolioConfiguration, \ + TimeUnit, CCXTOHLCVMarketDataSource, Algorithm, \ + CCXTTickerMarketDataSource, MarketCredential, AzureBlobStorageStateHandler + +load_dotenv() + +# Define market data sources +# OHLCV data for candles +bitvavo_btc_eur_ohlcv_2h = CCXTOHLCVMarketDataSource( + identifier="BTC-ohlcv", + market="BITVAVO", + symbol="BTC/EUR", + time_frame="2h", + window_size=200 +) +# Ticker data for orders, trades and positions +bitvavo_btc_eur_ticker = CCXTTickerMarketDataSource( + identifier="BTC-ticker", + market="BITVAVO", + symbol="BTC/EUR", +) +app = create_app(state_handler=AzureBlobStorageStateHandler()) +app.add_market_data_source(bitvavo_btc_eur_ohlcv_2h) +algorithm = Algorithm() +app.add_market_credential(MarketCredential(market="bitvavo")) +app.add_portfolio_configuration( + PortfolioConfiguration( + market="bitvavo", + trading_symbol="EUR", + initial_balance=20 + ) +) +app.add_algorithm(algorithm) + +@algorithm.strategy( + # Run every two hours + time_unit=TimeUnit.HOUR, + interval=2, + # Specify market data sources that need to be passed to the strategy + market_data_sources=[bitvavo_btc_eur_ticker, "BTC-ohlcv"] +) +def perform_strategy(algorithm: Algorithm, market_data: dict): + # By default, ohlcv data is passed as polars df in the form of + # {"": } https://pola.rs/, + # call to_pandas() to convert to pandas + polars_df = market_data["BTC-ohlcv"] + print(f"I have access to {len(polars_df)} candles of ohlcv data") diff --git a/investing_algorithm_framework/cli/templates/azure_function_function_app.py.template b/investing_algorithm_framework/cli/templates/azure_function_function_app.py.template new file mode 100644 index 00000000..7291ffa1 --- /dev/null +++ b/investing_algorithm_framework/cli/templates/azure_function_function_app.py.template @@ -0,0 +1,90 @@ +import logging + +import azure.functions as func +from investing_algorithm_framework import StatelessAction +from app import app as investing_algorithm_framework_app + + +import logging +import logging.config + +LOGGING_CONFIG = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'default': { + 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s', + }, + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'default', + }, + 'file': { + 'class': 'logging.FileHandler', + 'formatter': 'default', + 'filename': 'app_logs.log', + }, + }, + 'loggers': { # Make sure to add a 'loggers' section + 'investing_algorithm_framework': { # Define your logger here + 'level': 'INFO', # Set the desired level + 'handlers': ['console', 'file'], # Use these handlers + 'propagate': False, # Prevent logs from propagating to the root logger (optional) + }, + }, + 'root': { # Optional: Root logger configuration + 'level': 'WARNING', # Root logger defaults to WARNING + 'handlers': ['console', 'file'], + }, +} + +logging.config.dictConfig(LOGGING_CONFIG) +app: func.FunctionApp = func.FunctionApp() + +# Change your interval here, e.ge. "0 */1 * * * *" for every minute +# or "0 0 */1 * * *" for every hour or "0 */5 * * * *" for every 5 minutes +@func.timer_trigger( + schedule="0 */5 * * * *", + arg_name="myTimer", + run_on_startup=False, + use_monitor=False +) +@func.http_trigger( + route='start', methods=["GET"], auth_level=func.AuthLevel.FUNCTION +) +def app(myTimer: func.TimerRequest) -> None: + + if myTimer.past_due: + logging.info('The timer is past due!') + + logging.info('Python timer trigger function ran at %s', myTimer.next) + investing_algorithm_framework_app.run( + payload={"ACTION": StatelessAction.RUN_STRATEGY.value} + ) + +@app.route(route="test", auth_level=func.AuthLevel.ANONYMOUS) +def test(req: func.HttpRequest) -> func.HttpResponse: + logging.info('Python HTTP trigger function processed a request.') + + name = req.params.get('name') + investing_algorithm_framework_app.run( + payload={"ACTION": StatelessAction.RUN_STRATEGY.value} + ) + + if not name: + try: + req_body = req.get_json() + except ValueError: + pass + else: + name = req_body.get('name') + + if name: + return func.HttpResponse(f"Hello, {name}. This HTTP triggered function executed successfully.") + else: + return func.HttpResponse( + "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.", + status_code=200 + ) diff --git a/investing_algorithm_framework/cli/templates/azure_function_host.json.template b/investing_algorithm_framework/cli/templates/azure_function_host.json.template new file mode 100644 index 00000000..9df91361 --- /dev/null +++ b/investing_algorithm_framework/cli/templates/azure_function_host.json.template @@ -0,0 +1,15 @@ +{ + "version": "2.0", + "logging": { + "applicationInsights": { + "samplingSettings": { + "isEnabled": true, + "excludedTypes": "Request" + } + } + }, + "extensionBundle": { + "id": "Microsoft.Azure.Functions.ExtensionBundle", + "version": "[4.*, 5.0.0)" + } +} \ No newline at end of file diff --git a/investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template b/investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template new file mode 100644 index 00000000..67043d71 --- /dev/null +++ b/investing_algorithm_framework/cli/templates/azure_function_local.settings.json.template @@ -0,0 +1,8 @@ +{ + "IsEncrypted": false, + "Values": { + "FUNCTIONS_WORKER_RUNTIME": "python", + "AzureWebJobsFeatureFlags": "EnableWorkerIndexing", + "AzureWebJobsStorage": "" + } +} \ No newline at end of file diff --git a/investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template b/investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template new file mode 100644 index 00000000..ca3bf070 --- /dev/null +++ b/investing_algorithm_framework/cli/templates/azure_function_requirements.txt.template @@ -0,0 +1,2 @@ +investing-algorithm-framework +azure-functions==1.17.0 diff --git a/investing_algorithm_framework/create_app.py b/investing_algorithm_framework/create_app.py index 57155b7f..e2de4557 100644 --- a/investing_algorithm_framework/create_app.py +++ b/investing_algorithm_framework/create_app.py @@ -1,4 +1,6 @@ import logging +import os +from dotenv import load_dotenv from .app import App from .dependency_container import setup_dependency_container @@ -6,8 +8,26 @@ logger = logging.getLogger("investing_algorithm_framework") -def create_app(config=None, stateless=False, web=False) -> App: - app = App(web=web, stateless=stateless) +def create_app( + config: dict =None, + web=False, + state_handler=None +) -> App: + """ + Factory method to create an app instance. + + Args: + config (dict): Configuration dictionary + web (bool): Whether to create a web app + state_handler (StateHandler): State handler for the app + + Returns: + App: App instance + """ + # Load the environment variables + load_dotenv() + + app = App(web=web, state_handler=state_handler) app = setup_dependency_container( app, ["investing_algorithm_framework"], @@ -15,6 +35,9 @@ def create_app(config=None, stateless=False, web=False) -> App: ) # After the container is setup, initialize the services app.initialize_services() - app.set_config(config) + + if config is not None: + app.set_config_with_dict(config) + logger.info("Investing algoritm framework app created") return app diff --git a/investing_algorithm_framework/deployment/azure/__init__.py b/investing_algorithm_framework/deployment/azure/__init__.py deleted file mode 100644 index 32b8d704..00000000 --- a/investing_algorithm_framework/deployment/azure/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# from .azure_functions import deploy_to_azure_functions - -# __all__ = ['deploy_to_azure_functions'] diff --git a/investing_algorithm_framework/deployment/azure/azure_functions.py b/investing_algorithm_framework/deployment/azure/azure_functions.py deleted file mode 100644 index 6b12c011..00000000 --- a/investing_algorithm_framework/deployment/azure/azure_functions.py +++ /dev/null @@ -1,102 +0,0 @@ -# import os -# import json -# from azure.identity import DefaultAzureCredential -# from azure.mgmt.resource import ResourceManagementClient -# from azure.mgmt.storage import StorageManagementClient -# from azure.mgmt.web import WebSiteManagementClient -# import shutil - - -# def deploy_to_azure_functions(azure_credentials_json, azure_function_path): -# """ -# This function deploys a Python function app to Azure Functions. - -# Parameters: -# - azure_credentials_json (str): Path to the Azure credentials -# JSON file. -# - azure_function_path (str): Path to the Python function -# app directory. - -# Returns: -# None -# """ - -# # Load Azure credentials -# with open('azure_credentials.json') as f: -# credentials = json.load(f) - -# SUBSCRIPTION_ID = credentials['subscriptionId'] -# RESOURCE_GROUP_NAME = "myResourceGroup" -# LOCATION = "eastus" -# STORAGE_ACCOUNT_NAME = "mystorageaccount123" -# FUNCTION_APP_NAME = "my-python-function-app" - -# # Authenticate using DefaultAzureCredential -# credential = DefaultAzureCredential() - -# # Clients -# resource_client = ResourceManagementClient(credential, SUBSCRIPTION_ID) -# storage_client = StorageManagementClient(credential, SUBSCRIPTION_ID) -# web_client = WebSiteManagementClient(credential, SUBSCRIPTION_ID) - -# # Create Resource Group -# resource_client.resource_groups.create_or_update(RESOURCE_GROUP_NAME, -# {"location": LOCATION}) - -# # Create Storage Account -# storage_client.storage_accounts.begin_create( -# RESOURCE_GROUP_NAME, -# STORAGE_ACCOUNT_NAME, -# { -# "sku": {"name": "Standard_LRS"}, -# "kind": "StorageV2", -# "location": LOCATION -# } -# ).result() - -# # Create Function App (with a Consumption Plan) -# site_config = { -# "location": LOCATION, -# "server_farm_id": f"/subscriptions/{SUBSCRIPTION_ID}" + -# "/resourceGroups" + -# "/{RESOURCE_GROUP_NAME}/providers/Microsoft.Web/" + -# "serverfarms/{APP_SERVICE_PLAN_NAME}", -# "reserved": True, # This is necessary for Linux-based function apps -# "site_config": { -# "app_settings": [ -# { -# "name": "FUNCTIONS_WORKER_RUNTIME", "value": "python" -# }, -# { -# "name": "AzureWebJobsStorage", -# "value": "DefaultEndpointsProtocol=https;" + -# f"AccountName={STORAGE_ACCOUNT_NAME}" + -# ";AccountKey=account_key>", -# } -# ] -# }, -# "kind": "functionapp", -# } - -# web_client.web_apps.begin_create_or_update(RESOURCE_GROUP_NAME, -# FUNCTION_APP_NAME, -# site_config).result() - -# # Zip Function Code -# def zipdir(path, zipfile): -# for root, dirs, files in os.walk(path): -# for file in files: -# zipfile.write(os.path.join(root, file), -# os.path.relpath(os.path.join(root, file), path)) - -# shutil.make_archive('myfunctionapp', 'zip', 'myfunctionapp/') - -# # Deploy Function Code -# def deploy_function(): -# with open("myfunctionapp.zip", "rb") as z: -# web_client.web_apps.begin_create_zip_deployment( -# RESOURCE_GROUP_NAME, FUNCTION_APP_NAME, z).result() - -# deploy_function() - -# print(f"Function app '{FUNCTION_APP_NAME}' deployed to Azure.") diff --git a/investing_algorithm_framework/domain/__init__.py b/investing_algorithm_framework/domain/__init__.py index 5257bff8..76fcba7d 100644 --- a/investing_algorithm_framework/domain/__init__.py +++ b/investing_algorithm_framework/domain/__init__.py @@ -1,4 +1,4 @@ -from .config import Config, Environment +from .config import Environment, DEFAULT_LOGGING_CONFIG from .constants import ITEMIZE, ITEMIZED, PER_PAGE, PAGE, ENVIRONMENT, \ DATABASE_DIRECTORY_PATH, DATABASE_NAME, DEFAULT_PER_PAGE_VALUE, \ DEFAULT_PAGE_VALUE, SQLALCHEMY_DATABASE_URI, RESOURCE_DIRECTORY, \ @@ -34,7 +34,6 @@ from .metrics import get_price_efficiency_ratio __all__ = [ - 'Config', "OrderStatus", "OrderSide", "OrderType", @@ -119,5 +118,6 @@ "get_price_efficiency_ratio", "convert_polars_to_pandas", "DateRange", - "get_backtest_report" + "get_backtest_report", + "DEFAULT_LOGGING_CONFIG" ] diff --git a/investing_algorithm_framework/domain/config.py b/investing_algorithm_framework/domain/config.py index 3962d367..ac2ce7d8 100644 --- a/investing_algorithm_framework/domain/config.py +++ b/investing_algorithm_framework/domain/config.py @@ -1,8 +1,6 @@ import logging -import os from enum import Enum -from .constants import RESOURCE_DIRECTORY from .exceptions import OperationalException logger = logging.getLogger("investing_algorithm_framework") @@ -15,6 +13,7 @@ class Environment(Enum): DEV = 'DEV' PROD = 'PROD' TEST = 'TEST' + BACKTEST = 'BACKTEST' # Static factory method to convert # a string to environment @@ -52,92 +51,34 @@ def equals(self, other): return other == self.value -class Config(dict): - ENVIRONMENT = Environment.PROD.value - LOG_LEVEL = 'DEBUG' - APP_DIR = os.path.abspath(os.path.dirname(__file__)) - PROJECT_ROOT = os.path.abspath(os.path.join(APP_DIR, os.pardir)) - DEBUG_TB_INTERCEPT_REDIRECTS = False - SQLALCHEMY_TRACK_MODIFICATIONS = False - CACHE_TYPE = 'simple' # Can be "memcached", "redis", etc. - CORS_ORIGIN_WHITELIST = [ - 'http://0.0.0.0:4100', - 'http://localhost:4100', - 'http://0.0.0.0:8000', - 'http://localhost:8000', - 'http://0.0.0.0:4200', - 'http://localhost:4200', - 'http://0.0.0.0:4000', - 'http://localhost:4000', - ] - RESOURCE_DIRECTORY = os.getenv(RESOURCE_DIRECTORY) - SCHEDULER_API_ENABLED = True - CHECK_PENDING_ORDERS = True - SQLITE_ENABLED = True - SQLITE_INITIALIZED = False - BACKTEST_DATA_DIRECTORY_NAME = "backtest_data" - SYMBOLS = None - DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" - - def __init__(self, resource_directory=None): - super().__init__() - - if resource_directory is not None: - self.RESOURCE_DIRECTORY = resource_directory - - super().__init__(vars(self.__class__)) - - def __str__(self): - field_strings = [] - - for attribute_key in self: - - if attribute_key.isupper(): - field_strings.append( - f'{attribute_key}=' - f'{self[attribute_key]!r}' - ) - - return f"<{self.__class__.__name__}({','.join(field_strings)})>" - - def get(self, key: str, default=None): - """ - Mimics the dict get() functionality - """ - - try: - return self[key] - # Ignore exception - except Exception: - pass - - return default - - def set(self, key: str, value) -> None: - self[key] = value - - @staticmethod - def from_dict(dictionary): - config = Config() - - if dictionary is not None: - for attribute_key in dictionary: - - if attribute_key: - config.set(attribute_key, dictionary[attribute_key]) - config[attribute_key] = dictionary[attribute_key] - - return config - - -class TestConfig(Config): - ENVIRONMENT = Environment.TEST.value - TESTING = True - DATABASE_CONFIG = {'DATABASE_NAME': "test"} - LOG_LEVEL = "INFO" - - -class DevConfig(Config): - ENVIRONMENT = Environment.DEV.value - DATABASE_CONFIG = {'DATABASE_NAME': "dev"} - LOG_LEVEL = "INFO" +DEFAULT_LOGGING_CONFIG = { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'default': { + 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s', + }, + }, + 'handlers': { + 'console': { + 'class': 'logging.StreamHandler', + 'formatter': 'default', + }, + 'file': { + 'class': 'logging.FileHandler', + 'formatter': 'default', + 'filename': 'app_logs.log', + }, + }, + 'loggers': { # Make sure to add a 'loggers' section + 'investing_algorithm_framework': { # Define your logger here + 'level': 'INFO', # Set the desired level + 'handlers': ['console', 'file'], # Use these handlers + 'propagate': False, # Prevent logs from propagating to the root logger (optional) + }, + }, + 'root': { # Optional: Root logger configuration + 'level': 'WARNING', # Root logger defaults to WARNING + 'handlers': ['console', 'file'], + }, +} diff --git a/investing_algorithm_framework/domain/models/market/market_credential.py b/investing_algorithm_framework/domain/models/market/market_credential.py index 3ea70dfa..ebb9aa3d 100644 --- a/investing_algorithm_framework/domain/models/market/market_credential.py +++ b/investing_algorithm_framework/domain/models/market/market_credential.py @@ -1,9 +1,62 @@ +import os +import logging + +from investing_algorithm_framework.domain import OperationalException + +logger = logging.getLogger("investing_algorithm_framework") + + class MarketCredential: - def __init__(self, api_key: str, secret_key: str, market: str): + """ + Market credential model to store the api key and secret key for a market. + """ + def __init__( + self, market: str, api_key: str = None, secret_key: str = None + ): self._api_key = api_key self._secret_key = secret_key self._market = market + def initialize(self): + """ + Internal helper to initialize the market credential. + """ + logger.info(f"Initializing market credential for {self.market}") + + if self.api_key is None: + logger.info( + "Reading api key from environment variable" + f" {self.market.upper()}_API_KEY" + ) + + # Check if environment variable is set + environment_variable = f"{self.market.upper()}_API_KEY" + self._api_key = os.getenv(environment_variable) + + if self.api_key is None: + raise OperationalException( + "Market credential requires an api key, either" + " as an argument or as an environment variable" + f" named as {self._market.upper()}_API_KEY" + ) + + if self.secret_key is None: + logger.info( + "Reading secret key from environment variable" + f" {self.market.upper()}_SECRET_KEY" + ) + + # Check if environment variable is set + environment_variable = f"{self.market.upper()}_SECRET_KEY" + self._secret_key = os.getenv(environment_variable) + + if self.secret_key is None: + raise OperationalException( + "Market credential requires a secret key, either" + " as an argument or as an environment variable" + f" named as {self._market.upper()}_SECRET_KEY" + ) + def get_api_key(self): return self.api_key diff --git a/investing_algorithm_framework/domain/models/order/order.py b/investing_algorithm_framework/domain/models/order/order.py index e51d99eb..e76885f8 100644 --- a/investing_algorithm_framework/domain/models/order/order.py +++ b/investing_algorithm_framework/domain/models/order/order.py @@ -42,7 +42,8 @@ def __init__( position_id=None, order_fee=None, order_fee_currency=None, - order_fee_rate=None + order_fee_rate=None, + id=None ): if target_symbol is None: raise OperationalException("Target symbol is not specified") @@ -83,6 +84,7 @@ def __init__( self.order_fee = order_fee self.order_fee_currency = order_fee_currency self.order_fee_rate = order_fee_rate + self.id = id def get_id(self): return self.id @@ -321,6 +323,7 @@ def from_dict(data: dict): order_fee=data.get("order_fee", None), order_fee_currency=data.get("order_fee_currency", None), order_fee_rate=data.get("order_fee_rate", None), + id=data.get("id", None) ) @staticmethod diff --git a/investing_algorithm_framework/domain/models/portfolio/portfolio.py b/investing_algorithm_framework/domain/models/portfolio/portfolio.py index 2b314feb..05dc7d99 100644 --- a/investing_algorithm_framework/domain/models/portfolio/portfolio.py +++ b/investing_algorithm_framework/domain/models/portfolio/portfolio.py @@ -16,21 +16,25 @@ def __init__( total_net_gain=0, total_trade_volume=0, created_at=None, - updated_at=None + updated_at=None, + initialized=False, + initial_balance=None ): self.identifier = identifier self.updated_at = None self.trading_symbol = trading_symbol.upper() self.net_size = net_size self.unallocated = unallocated + self.initial_balance = initial_balance self.realized = realized self.total_revenue = total_revenue self.total_cost = total_cost self.total_net_gain = total_net_gain self.total_trade_volume = total_trade_volume - self.market = market + self.market = market.upper() self.created_at = created_at self.updated_at = updated_at + self.initialized = initialized def __repr__(self): return self.repr( @@ -42,6 +46,7 @@ def __repr__(self): total_revenue=self.total_revenue, total_cost=self.total_cost, market=self.market, + initial_balance=self.initial_balance ) def get_identifier(self): @@ -80,12 +85,41 @@ def get_trading_symbol(self): def get_market(self): return self.market + def get_initial_balance(self): + return self.initial_balance + @staticmethod def from_portfolio_configuration(portfolio_configuration): + """ + Function to create a portfolio from a portfolio configuration + + We assume that a portfolio that is created from a configuration + is always un initialized. + + Args: + portfolio_configuration: PortfolioConfiguration + + Returns: + Portfolio + """ return Portfolio( identifier=portfolio_configuration.identifier, trading_symbol=portfolio_configuration.trading_symbol, unallocated=portfolio_configuration.initial_balance, net_size=portfolio_configuration.initial_balance, - market=portfolio_configuration.market + market=portfolio_configuration.market, + initial_balance=portfolio_configuration.initial_balance, + initialized=False ) + + def to_dict(self): + return { + "trading_symbol": self.trading_symbol, + "market": self.market, + "unallocated": self.unallocated, + "identifier": self.identifier, + "created_at": self.created_at, + "updated_at": self.updated_at, + "initialized": self.initialized, + "initial_balance": self.initial_balance, + } diff --git a/investing_algorithm_framework/domain/utils/backtesting.py b/investing_algorithm_framework/domain/utils/backtesting.py index 57a379aa..7c051a7f 100644 --- a/investing_algorithm_framework/domain/utils/backtesting.py +++ b/investing_algorithm_framework/domain/utils/backtesting.py @@ -524,7 +524,6 @@ def get_backtest_report( get_start_date_from_backtest_report_file( os.path.join(root, file) ) - print(backtest_start_date) backtest_end_date = \ get_end_date_from_backtest_report_file( os.path.join(root, file) diff --git a/investing_algorithm_framework/infrastructure/__init__.py b/investing_algorithm_framework/infrastructure/__init__.py index ed22bae3..cc6be269 100644 --- a/investing_algorithm_framework/infrastructure/__init__.py +++ b/investing_algorithm_framework/infrastructure/__init__.py @@ -8,7 +8,8 @@ from .repositories import SQLOrderRepository, SQLPositionRepository, \ SQLPortfolioRepository, \ SQLPortfolioSnapshotRepository, SQLPositionSnapshotRepository -from .services import PerformanceService, CCXTMarketService +from .services import PerformanceService, CCXTMarketService, \ + AzureBlobStorageStateHandler __all__ = [ "create_all_tables", @@ -34,4 +35,5 @@ "CSVTickerMarketDataSource", "CCXTOHLCVBacktestMarketDataSource", "CCXTOrderBookMarketDataSource", + "AzureBlobStorageStateHandler" ] diff --git a/investing_algorithm_framework/infrastructure/database/sql_alchemy.py b/investing_algorithm_framework/infrastructure/database/sql_alchemy.py index 84ca4c70..2b7a8233 100644 --- a/investing_algorithm_framework/infrastructure/database/sql_alchemy.py +++ b/investing_algorithm_framework/infrastructure/database/sql_alchemy.py @@ -19,18 +19,11 @@ def __init__(self, app): raise OperationalException("SQLALCHEMY_DATABASE_URI not set") global Session - - if app.config[SQLALCHEMY_DATABASE_URI] != "sqlite:///:memory:": - engine = create_engine( - app.config[SQLALCHEMY_DATABASE_URI], - connect_args={'check_same_thread': False}, - poolclass=StaticPool - ) - else: - engine = create_engine( - app.config[SQLALCHEMY_DATABASE_URI], - ) - + engine = create_engine( + app.config[SQLALCHEMY_DATABASE_URI], + connect_args={'check_same_thread': False}, + poolclass=StaticPool + ) Session.configure(bind=engine) diff --git a/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py b/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py index 1dc0c50b..ddea17bc 100644 --- a/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +++ b/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py @@ -13,7 +13,7 @@ from investing_algorithm_framework.infrastructure.services import \ CCXTMarketService -logger = logging.getLogger(__name__) +logger = logging.getLogger("investing_algorithm_framework") class CCXTOHLCVBacktestMarketDataSource( @@ -63,7 +63,6 @@ def prepare_data( config, backtest_start_date, backtest_end_date, - **kwargs ): """ Prepare data implementation of ccxt based ohlcv backtest market @@ -86,6 +85,10 @@ def prepare_data( Returns: None """ + + if config is None: + config = self.config + # Calculating the backtest data start date backtest_data_start_date = \ backtest_start_date - timedelta( @@ -103,8 +106,7 @@ def prepare_data( # Creating the backtest data directory and file self.backtest_data_directory = os.path.join( - config.get(RESOURCE_DIRECTORY), - config.get(BACKTEST_DATA_DIRECTORY_NAME) + config[RESOURCE_DIRECTORY], config[BACKTEST_DATA_DIRECTORY_NAME] ) if not os.path.isdir(self.backtest_data_directory): @@ -303,6 +305,7 @@ def prepare_data( When downloading the data it will use the ccxt library. """ + config = self.config total_minutes = TimeFrame.from_string(self.time_frame)\ .amount_of_minutes self.backtest_data_start_date = \ @@ -311,8 +314,7 @@ def prepare_data( # Creating the backtest data directory and file self.backtest_data_directory = os.path.join( - config.get(RESOURCE_DIRECTORY), - config.get(BACKTEST_DATA_DIRECTORY_NAME) + config[RESOURCE_DIRECTORY], config[BACKTEST_DATA_DIRECTORY_NAME] ) if not os.path.isdir(self.backtest_data_directory): @@ -521,6 +523,9 @@ def get_data(self, **kwargs): else: storage_path = self.get_storage_path() + logger.info( + f"Getting OHLCV data for {self.symbol} from {start_date} to {end_date}" + ) data = None if storage_path is not None: diff --git a/investing_algorithm_framework/infrastructure/models/portfolio/__init__.py b/investing_algorithm_framework/infrastructure/models/portfolio/__init__.py index 9338f074..c2b45922 100644 --- a/investing_algorithm_framework/infrastructure/models/portfolio/__init__.py +++ b/investing_algorithm_framework/infrastructure/models/portfolio/__init__.py @@ -1,4 +1,4 @@ -from .portfolio import SQLPortfolio +from .sql_portfolio import SQLPortfolio from .portfolio_snapshot import SQLPortfolioSnapshot __all__ = ['SQLPortfolio', "SQLPortfolioSnapshot"] diff --git a/investing_algorithm_framework/infrastructure/models/portfolio/portfolio.py b/investing_algorithm_framework/infrastructure/models/portfolio/sql_portfolio.py similarity index 89% rename from investing_algorithm_framework/infrastructure/models/portfolio/portfolio.py rename to investing_algorithm_framework/infrastructure/models/portfolio/sql_portfolio.py index bdc08d15..8a252b0c 100644 --- a/investing_algorithm_framework/infrastructure/models/portfolio/portfolio.py +++ b/investing_algorithm_framework/infrastructure/models/portfolio/sql_portfolio.py @@ -1,6 +1,6 @@ from datetime import datetime -from sqlalchemy import Column, Integer, String, DateTime, Float +from sqlalchemy import Column, Integer, String, DateTime, Float, Boolean from sqlalchemy import UniqueConstraint from sqlalchemy.orm import relationship from sqlalchemy.orm import validates @@ -23,6 +23,7 @@ class SQLPortfolio(Portfolio, SQLBaseModel, SQLAlchemyModelExtension): total_trade_volume = Column(Float, nullable=False, default=0) net_size = Column(Float, nullable=False, default=0) unallocated = Column(Float, nullable=False, default=0) + initial_balance = Column(Float, nullable=True) market = Column(String, nullable=False) positions = relationship( "SQLPosition", @@ -32,6 +33,8 @@ class SQLPortfolio(Portfolio, SQLBaseModel, SQLAlchemyModelExtension): ) created_at = Column(DateTime, nullable=False, default=datetime.utcnow) updated_at = Column(DateTime, nullable=False, default=datetime.utcnow) + initialized = Column(Boolean, nullable=False, default=False) + __table_args__ = ( UniqueConstraint( 'trading_symbol', @@ -53,8 +56,11 @@ def __init__( trading_symbol, market, unallocated, + initialized, + initial_balance=None, identifier=None, created_at=None, + updated_at=None, ): if identifier is None: @@ -73,6 +79,9 @@ def __init__( total_revenue=0, total_cost=0, created_at=created_at, + updated_at=updated_at, + initialized=initialized, + initial_balance=initial_balance, ) def update(self, data): diff --git a/investing_algorithm_framework/infrastructure/repositories/order_repository.py b/investing_algorithm_framework/infrastructure/repositories/order_repository.py index 50a06ebe..0a2112e2 100644 --- a/investing_algorithm_framework/infrastructure/repositories/order_repository.py +++ b/investing_algorithm_framework/infrastructure/repositories/order_repository.py @@ -7,6 +7,7 @@ class SQLOrderRepository(Repository): base_class = SQLOrder + DEFAULT_NOT_FOUND_MESSAGE = "The requested order was not found" def _apply_query_params(self, db, query, query_params): external_id_query_param = self.get_query_param( diff --git a/investing_algorithm_framework/infrastructure/repositories/repository.py b/investing_algorithm_framework/infrastructure/repositories/repository.py index 16e671f0..8d1d68a1 100644 --- a/investing_algorithm_framework/infrastructure/repositories/repository.py +++ b/investing_algorithm_framework/infrastructure/repositories/repository.py @@ -243,3 +243,15 @@ def get_query_param(self, key, params, default=None, many=False): return new_selection[0] return new_selection + + def save(self, object): + + with Session() as db: + try: + db.add(object) + db.commit() + return self.get(object) + except SQLAlchemyError as e: + logger.error(e) + db.rollback() + raise ApiException("Error saving object") diff --git a/investing_algorithm_framework/infrastructure/services/__init__.py b/investing_algorithm_framework/infrastructure/services/__init__.py index 3186c9e7..3cd88f29 100644 --- a/investing_algorithm_framework/infrastructure/services/__init__.py +++ b/investing_algorithm_framework/infrastructure/services/__init__.py @@ -1,7 +1,9 @@ from .market_service import CCXTMarketService from .performance_service import PerformanceService +from .azure import AzureBlobStorageStateHandler __all__ = [ "PerformanceService", - "CCXTMarketService" + "CCXTMarketService", + "AzureBlobStorageStateHandler" ] diff --git a/investing_algorithm_framework/infrastructure/services/azure/__init__.py b/investing_algorithm_framework/infrastructure/services/azure/__init__.py new file mode 100644 index 00000000..8a8440f4 --- /dev/null +++ b/investing_algorithm_framework/infrastructure/services/azure/__init__.py @@ -0,0 +1,5 @@ +from .state_handler import AzureBlobStorageStateHandler + +__all__ = [ + "AzureBlobStorageStateHandler" +] diff --git a/investing_algorithm_framework/infrastructure/services/azure/state_handler.py b/investing_algorithm_framework/infrastructure/services/azure/state_handler.py new file mode 100644 index 00000000..68e0a4ca --- /dev/null +++ b/investing_algorithm_framework/infrastructure/services/azure/state_handler.py @@ -0,0 +1,142 @@ +import os +import logging + +from azure.storage.blob import ContainerClient +from investing_algorithm_framework.domain import OperationalException + +logger = logging.getLogger("investing_algorithm_framework") + + +class AzureBlobStorageStateHandler: + + def __init__( + self, connection_string: str = None, container_name: str = None + ): + self.connection_string = connection_string + self.container_name = container_name + self._initialize() + + def _initialize(self): + """ + Internal helper to initialize the state handler. + """ + + if self.connection_string is None: + + # Check if environment variable is set + self.connection_string = \ + os.getenv("AZURE_STORAGE_CONNECTION_STRING") + + if self.connection_string is None: + raise OperationalException( + "Azure Blob Storage state handler requires a connection string or an environment variable AZURE_STORAGE_CONNECTION_STRING to be set" + ) + + if self.container_name is None: + + # Check if environment variable is set + self.container_name = os.getenv("AZURE_STORAGE_CONTAINER_NAME") + + if self.container_name is None: + raise OperationalException( + "Azure Blob Storage state handler requires a container name or an environment variable AZURE_STORAGE_CONTAINER_NAME to be set" + ) + + def save(self, source_directory: str): + """ + Save the state to Azure Blob Storage. + + Parameters: + source_directory (str): Directory to save the state + + Returns: + None + """ + logger.info("Saving state to Azure Blob Storage ...") + + try: + container_client = self._create_container_client() + + # Create container if it does not exist + if not container_client.exists(): + container_client.create_container() + + # Walk through the directory + for root, _, files in os.walk(source_directory): + for file_name in files: + # Get the full path of the file + file_path = os.path.join(root, file_name) + + # Construct the blob name (relative path in the container) + blob_name = os.path.relpath(file_path, source_directory)\ + .replace("\\", "/") + + # Upload the file + with open(file_path, "rb") as data: + container_client.upload_blob(name=blob_name, data=data, overwrite=True) + + except Exception as ex: + logger.error(f"Error saving state to Azure Blob Storage: {ex}") + raise ex + + def load(self, target_directory: str): + """ + Load the state from Azure Blob Storage. + + Parameters: + target_directory (str): Directory to load the state + + Returns: + None + """ + logger.info("Loading state from Azure Blob Storage ...") + + try: + container_client = self._create_container_client() + + # Ensure the local directory exists + if not os.path.exists(target_directory): + os.makedirs(target_directory) + + # List and download blobs + for blob in container_client.list_blobs(): + blob_name = blob.name + blob_file_path = os.path.join(target_directory, blob_name) + + # Create subdirectories locally if needed + os.makedirs(os.path.dirname(blob_file_path), exist_ok=True) + + # Download blob to file + with open(blob_file_path, "wb") as file: + blob_client = container_client.get_blob_client(blob_name) + file.write(blob_client.download_blob().readall()) + + except Exception as ex: + logger.error(f"Error loading state from Azure Blob Storage: {ex}") + raise ex + + def _create_container_client(self): + """ + Internal helper to create a Container clinet. + + Returns: + ContainerClient + """ + + # Ensure the container exists + try: + container_client = ContainerClient.from_connection_string( + conn_str=self.connection_string, + container_name=self.container_name + ) + container_client.create_container(timeout=10) + except Exception as e: + + if "ContainerAlreadyExists" in str(e): + pass + else: + raise OperationalException( + f"Error occurred while creating the container: {e}" + ) + + return container_client diff --git a/investing_algorithm_framework/services/backtesting/backtest_service.py b/investing_algorithm_framework/services/backtesting/backtest_service.py index 62dc528b..cf2dab2c 100644 --- a/investing_algorithm_framework/services/backtesting/backtest_service.py +++ b/investing_algorithm_framework/services/backtesting/backtest_service.py @@ -440,6 +440,7 @@ def _check_if_required_market_data_sources_are_registered(self): if a ticker market data source is registered for the symbol and market. """ symbols = self._configuration_service.config[SYMBOLS] + print(symbols) if symbols is not None: diff --git a/investing_algorithm_framework/services/configuration_service.py b/investing_algorithm_framework/services/configuration_service.py index 2fd82d6e..5bd74bb7 100644 --- a/investing_algorithm_framework/services/configuration_service.py +++ b/investing_algorithm_framework/services/configuration_service.py @@ -1,29 +1,77 @@ -from investing_algorithm_framework.domain import Config +import os +from investing_algorithm_framework.domain import Environment, \ + RESOURCE_DIRECTORY +DEFAULT_CONFIGURATION = { + "ENVIRONMENT": Environment.PROD.value, + "LOG_LEVEL": 'DEBUG', + "APP_DIR": os.path.abspath(os.path.dirname(__file__)), + "PROJECT_ROOT": os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(__file__)), os.pardir)), + "RESOURCE_DIRECTORY": os.getenv(RESOURCE_DIRECTORY), + "CHECK_PENDING_ORDERS": True, + "SQLITE_INITIALIZED": False, + "BACKTEST_DATA_DIRECTORY_NAME": "backtest_data", + "SYMBOLS": None, + "DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S", + "DATABASE_DIRECTORY_PATH": None +} + +DEFAULT_FLASK_CONFIGURATION = { + "DEBUG_TB_INTERCEPT_REDIRECTS": False, + "SQLALCHEMY_TRACK_MODIFICATIONS": False, + "CACHE_TYPE": 'simple', + "CORS_ORIGIN_WHITELIST": [ + 'http://0.0.0.0:4100', + 'http://localhost:4100', + 'http://0.0.0.0:8000', + 'http://localhost:8000', + 'http://0.0.0.0:4200', + 'http://localhost:4200', + 'http://0.0.0.0:4000', + 'http://localhost:4000', + ], + "SCHEDULER_API_ENABLED": True, +} class ConfigurationService: def __init__(self): - self._config = Config() + self._config = DEFAULT_CONFIGURATION + self._flask_config = DEFAULT_FLASK_CONFIGURATION @property def config(self): - return self._config + # Make a copy of the config to prevent external modifications + copy = self._config.copy() + return copy def get_config(self): - return self._config + copy = self._config.copy() + return copy - def set_config(self, config): - self._config = config + def get_flask_config(self): + # Make a copy of the config to prevent external modifications + copy = self._flask_config.copy() + return copy def add_value(self, key, value): - self._config.set(key, value) + self._config[key] = value - def get_value(self, key): - return self._config.get(key) + def add_dict(self, dictionary): + self._config.update(dictionary) def remove_value(self, key): self._config.pop(key) - def initialize_from_dict(self, data): - self._config = Config.from_dict(data) + def initialize_from_dict(self, data: dict): + """ + Initialize the configuration from a dictionary. + + Args: + data (dict): The dictionary containing the configuration values. + + Returns: + None + """ + + self._config.update(data) diff --git a/investing_algorithm_framework/services/market_credential_service.py b/investing_algorithm_framework/services/market_credential_service.py index 624d0fbb..014e2be0 100644 --- a/investing_algorithm_framework/services/market_credential_service.py +++ b/investing_algorithm_framework/services/market_credential_service.py @@ -23,3 +23,11 @@ def get(self, market) -> Union[MarketCredential, None]: def get_all(self) -> List[MarketCredential]: return list(self._market_credentials.values()) + + def initialize(self): + """ + Initialize all market credentials. + """ + + for market_credential in self.get_all(): + market_credential.initialize() diff --git a/investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py b/investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py index b6adb770..174964f3 100644 --- a/investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py +++ b/investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py @@ -2,7 +2,7 @@ from investing_algorithm_framework.domain import MarketService, \ MarketDataSource, OHLCVMarketDataSource, TickerMarketDataSource, \ - OrderBookMarketDataSource, TimeFrame + OrderBookMarketDataSource, TimeFrame, OperationalException from investing_algorithm_framework.services.market_credential_service \ import MarketCredentialService @@ -85,12 +85,19 @@ def get_ohlcv( ) def get_data(self, identifier): + for market_data_source in self._market_data_sources: if market_data_source.get_identifier() == identifier: return market_data_source.get_data( market_credential_service=self._market_credential_service ) + if isinstance(identifier, str): + raise OperationalException( + f"Market data source with identifier {identifier} not found. " + "Please make sure that the market data source is registered to the app if you refer to it by identifier in your strategy." + ) + def get_ticker_market_data_source(self, symbol, market=None): if self.market_data_sources is not None: @@ -188,6 +195,10 @@ def market_data_sources(self, market_data_sources): def add(self, market_data_source): + # Check if the market data source is an instance of MarketDataSource + if not isinstance(market_data_source, MarketDataSource): + return + # Check if there is already a market data source with the same # identifier for existing_market_data_source in self._market_data_sources: diff --git a/investing_algorithm_framework/services/order_service/order_backtest_service.py b/investing_algorithm_framework/services/order_service/order_backtest_service.py index fc5afcd8..d836be3b 100644 --- a/investing_algorithm_framework/services/order_service/order_backtest_service.py +++ b/investing_algorithm_framework/services/order_service/order_backtest_service.py @@ -35,9 +35,10 @@ def __init__( market_data_source_service def create(self, data, execute=True, validate=True, sync=True) -> Order: + config = self.configuration_service.get_config() + # Make sure the created_at is set to the current backtest time - data["created_at"] = self.configuration_service\ - .config[BACKTESTING_INDEX_DATETIME] + data["created_at"] = config[BACKTESTING_INDEX_DATETIME] # Call super to have standard behavior return super(OrderBacktestService, self)\ .create(data, execute, validate, sync) @@ -106,13 +107,12 @@ def check_pending_orders(self): f"{config[BACKTESTING_PENDING_ORDER_CHECK_INTERVAL]} " ) + config = self.configuration_service.get_config() df = self._market_data_source_service.get_ohlcv( symbol=symbol, market=portfolio.market, time_frame=time_frame, - to_timestamp=self.configuration_service.config.get( - BACKTESTING_INDEX_DATETIME - ), + to_timestamp=config[BACKTESTING_INDEX_DATETIME], from_timestamp=order.get_created_at(), ) diff --git a/investing_algorithm_framework/services/order_service/order_service.py b/investing_algorithm_framework/services/order_service/order_service.py index c2ab390f..7abf2bd2 100644 --- a/investing_algorithm_framework/services/order_service/order_service.py +++ b/investing_algorithm_framework/services/order_service/order_service.py @@ -254,9 +254,16 @@ def validate_limit_order(self, order_data, portfolio): "symbol": portfolio.trading_symbol } ) - amount = unallocated_position.get_amount() + unallocated_amount = unallocated_position.get_amount() - if amount < total_price: + if unallocated_amount is None: + raise OperationalException( + "Unallocated amount of the portfolio is None" + + "can't validate limit order. Please check if " + + "the portfolio configuration is correct" + ) + + if unallocated_amount < total_price: raise OperationalException( f"Order total: {total_price} " f"{portfolio.trading_symbol}, is " @@ -336,11 +343,10 @@ def _sync_portfolio_with_created_buy_order(self, order): "symbol": portfolio.trading_symbol } ) - - self.portfolio_repository.update( + portfolio = self.portfolio_repository.update( portfolio.id, {"unallocated": portfolio.get_unallocated() - size} ) - self.position_repository.update( + position = self.position_repository.update( trading_symbol_position.id, { "amount": trading_symbol_position.get_amount() - size diff --git a/investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py b/investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py index f5c45475..581e99e6 100644 --- a/investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py +++ b/investing_algorithm_framework/services/portfolios/backtest_portfolio_service.py @@ -16,5 +16,6 @@ def create_portfolio_from_configuration( "market": portfolio_configuration.market, "trading_symbol": portfolio_configuration.trading_symbol, "unallocated": portfolio_configuration.initial_balance, + "initialized": False } return self.create(data) diff --git a/investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py b/investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py index 91af4079..2c8e0208 100644 --- a/investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py +++ b/investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py @@ -6,6 +6,9 @@ class PortfolioConfigurationService: + """ + Service to manage portfolio configurations. This service will manage the portfolio configurations that the user has registered in the app. + """ def __init__(self, portfolio_repository, position_repository): self.portfolio_repository = portfolio_repository diff --git a/investing_algorithm_framework/services/portfolios/portfolio_service.py b/investing_algorithm_framework/services/portfolios/portfolio_service.py index dc729c04..bf99d45f 100644 --- a/investing_algorithm_framework/services/portfolios/portfolio_service.py +++ b/investing_algorithm_framework/services/portfolios/portfolio_service.py @@ -2,7 +2,8 @@ from datetime import datetime from investing_algorithm_framework.domain import OrderSide, OrderStatus, \ - OperationalException, MarketService, MarketCredentialService, Portfolio + OperationalException, MarketService, MarketCredentialService, Portfolio, \ + Environment, ENVIRONMENT from investing_algorithm_framework.services.configuration_service import \ ConfigurationService from investing_algorithm_framework.services.repository_service \ @@ -45,8 +46,48 @@ def find(self, query_params): portfolio.configuration = portfolio_configuration return portfolio + def get_all(self, query_params=None): + selection = super().get_all(query_params) + + for portfolio in selection: + portfolio_configuration = self.portfolio_configuration_service\ + .get(portfolio.identifier) + portfolio.configuration = portfolio_configuration + + return selection + def create(self, data): unallocated = data.get("unallocated", 0) + market = data.get("market") + + # Check if the app is in backtest mode + config = self.configuration_service.get_config() + environment = config[ENVIRONMENT] + + if not Environment.BACKTEST.equals(environment): + # Check if there is a market credential + # for the portfolio configuration + market_credential = self.market_credential_service.get(market) + + if market_credential is None: + raise OperationalException( + f"No market credential found for market " + f"{market}. Cannot " + f"initialize portfolio configuration." + ) + + identifier = data.get("identifier") + # Check if the portfolio already exists + # If the portfolio already exists, return the portfolio after checking + # the unallocated balance of the portfolio on the exchange + if identifier is not None and self.repository.exists( + {"identifier": identifier} + ): + portfolio = self.repository.find( + {"identifier": identifier} + ) + return portfolio + portfolio = super(PortfolioService, self).create(data) self.position_service.create( { @@ -87,6 +128,12 @@ def create_portfolio_from_configuration( provided. If the portfolio already exists, it will be returned. If the portfolio does not exist, it will be created. + + Args: + portfolio_configuration (PortfolioConfiguration) Portfolio configuration to create the portfolio from + + Returns: + Portfolio: Portfolio created from the configuration """ logger.info("Creating portfolios") @@ -117,4 +164,7 @@ def create_portfolio_from_configuration( portfolio = Portfolio.from_portfolio_configuration( portfolio_configuration ) + data = portfolio.to_dict() + self.create(data) + return portfolio diff --git a/investing_algorithm_framework/services/portfolios/portfolio_sync_service.py b/investing_algorithm_framework/services/portfolios/portfolio_sync_service.py index ee1b4419..b84592b3 100644 --- a/investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +++ b/investing_algorithm_framework/services/portfolios/portfolio_sync_service.py @@ -1,8 +1,8 @@ import logging from investing_algorithm_framework.domain import OperationalException, \ - AbstractPortfolioSyncService, RESERVED_BALANCES, APP_MODE, SYMBOLS, \ - OrderSide, AppMode + AbstractPortfolioSyncService, RESERVED_BALANCES, SYMBOLS, \ + OrderSide, OrderStatus from investing_algorithm_framework.services.trade_service import TradeService logger = logging.getLogger(__name__) @@ -14,15 +14,15 @@ class PortfolioSyncService(AbstractPortfolioSyncService): """ def __init__( - self, - trade_service: TradeService, - configuration_service, - order_repository, - position_repository, - portfolio_repository, - portfolio_configuration_service, - market_credential_service, - market_service + self, + trade_service: TradeService, + configuration_service, + order_repository, + position_repository, + portfolio_repository, + portfolio_configuration_service, + market_credential_service, + market_service ): self.trade_service = trade_service self.configuration_service = configuration_service @@ -40,23 +40,18 @@ def sync_unallocated(self, portfolio): available balance of the portfolio from the exchange and update the unallocated balance of the portfolio accordingly. - If the algorithm is running stateless it will update the unallocated - balance of the portfolio to the available balance on the exchange. - - If the algorithm is running stateful, the unallocated balance of the - portfolio will only check if the amount on the exchange is less - than the unallocated balance of the portfolio. If the amount on the - exchange is less than the unallocated balance of the portfolio, the - unallocated balance of the portfolio will be updated to the amount on - the exchange or an OperationalException will be raised if the - throw_exception_on_insufficient_balance is set to True. - - If in the config the RESERVED_BALANCES key is set, the reserved amount - will be subtracted from the unallocated amount. This is to prevent - the algorithm from using reserved balances for trading. The reserved - config is not used for the stateless mode, because this would mean - that the algorithm should be aware of how much it already used for - trading. This is not possible in stateless mode. + If the portfolio already exists (exists in the database), then a check is done if the exchange has the available balance of + the portfolio unallocated balance. If the exchange does not have + the available balance of the portfolio, an OperationalException will be raised. + + If the portfolio does not exist, the portfolio will be created with + the unallocated balance of the portfolio set to the available balance on the exchange. If also a initial balance is set in the portfolio configuration, the unallocated balance will be set to the initial balance (given the balance is available on the exchange). If the initial balance is not set, the unallocated balance will be set to the available balance on the exchange. + + Args: + portfolio: Portfolio object + + Returns: + Portfolio object """ market_credential = self.market_credential_service.get( portfolio.market @@ -71,70 +66,71 @@ def sync_unallocated(self, portfolio): # Get the unallocated balance of the portfolio from the exchange balances = self.market_service.get_balance(market=portfolio.market) - if portfolio.trading_symbol.upper() not in balances: - unallocated = 0 - else: - unallocated = float(balances[portfolio.trading_symbol.upper()]) - - reserved_unallocated = 0 - config = self.configuration_service.config - mode = config.get(APP_MODE) - - if not AppMode.STATELESS.equals(mode): - if RESERVED_BALANCES in config: - reserved = config[RESERVED_BALANCES] - - if portfolio.trading_symbol.upper() in reserved: - reserved_unallocated \ - = reserved[portfolio.trading_symbol.upper()] - - unallocated = unallocated - reserved_unallocated - - if portfolio.unallocated is not None and \ - unallocated != portfolio.unallocated: + if not portfolio.initialized: + # Check if the portfolio has an initial balance set + if portfolio.initial_balance is not None: + available = float(balances[portfolio.trading_symbol.upper()]) - if unallocated < portfolio.unallocated: + if portfolio.initial_balance > available: raise OperationalException( - "There seems to be a mismatch between " - "the portfolio configuration and the balances on" - " the exchange. " - f"Please make sure that the available " - f"{portfolio.trading_symbol} " - f"on your exchange {portfolio.market} " - "matches the portfolio configuration amount of: " - f"{portfolio.unallocated} " - f"{portfolio.trading_symbol}. " - f"You have currently {unallocated} " - f"{portfolio.trading_symbol} available on the " - f"exchange." + "The initial balance of the " + + "portfolio configuration " + + f"({portfolio.initial_balance} " + f"{portfolio.trading_symbol}) is more " + + "than the available balance on the exchange. " + + "Please make sure that the initial balance of " + + "the portfolio configuration is less " + + "than the available balance on the " + + f"exchange {available} {portfolio.trading_symbol}." ) + else: + unallocated = portfolio.initial_balance + else: + # If the portfolio does not have an initial balance set, get the available balance on the exchange + if portfolio.trading_symbol.upper() not in balances: + raise OperationalException( + f"There is no available balance on the exchange for " + f"{portfolio.trading_symbol.upper()} on market " + f"{portfolio.market}. Please make sure that you have " + f"an available balance on the exchange for " + f"{portfolio.trading_symbol.upper()} on market " + f"{portfolio.market}." + ) + else: + unallocated = float(balances[portfolio.trading_symbol.upper()]) - # If portfolio does not exist and initial balance is set, - # create the portfolio with the initial balance - if unallocated > portfolio.unallocated and \ - not self.portfolio_repository.exists( - {"identifier": portfolio.identifier} - ): - unallocated = portfolio.unallocated - - if not self.portfolio_repository.exists( - {"identifier": portfolio.identifier} - ): - create_data = { - "identifier": portfolio.get_identifier(), - "market": portfolio.get_market().upper(), - "trading_symbol": portfolio.get_trading_symbol(), - "unallocated": unallocated, - } - portfolio = self.portfolio_repository.create(create_data) - else: update_data = { "unallocated": unallocated, + "net_size": unallocated, + "initialized": True } portfolio = self.portfolio_repository.update( portfolio.id, update_data ) + # Update also a trading symbol position + trading_symbol_position = self.position_repository.find( + { + "symbol": portfolio.trading_symbol, + "portfolio_id": portfolio.id + } + ) + self.position_repository.update( + trading_symbol_position.id, {"amount": unallocated} + ) + + else: + # Check if the portfolio unallocated balance is available on the exchange + if portfolio.unallocated > 0: + if portfolio.trading_symbol.upper() not in balances or portfolio.unallocated > float(balances[portfolio.trading_symbol.upper()]): + raise OperationalException( + f"Out of sync: the unallocated balance" + " of the portfolio is more than the available" + " balance on the exchange. Please make sure that you" f" have at least {portfolio.unallocated}" + f" {portfolio.trading_symbol.upper()} available" + " on the exchange." + ) + return portfolio def sync_positions(self, portfolio): @@ -198,121 +194,39 @@ def sync_positions(self, portfolio): def sync_orders(self, portfolio): """ Function to sync all local orders with the orders on the exchange. - This function will retrieve all orders from the exchange and - update the portfolio balances and positions accordingly. - - First all orders are retrieved from the exchange and updated in the - database. If the order does not exist in the database, it will be - created and treated as a new order. - - When an order is closed on the exchange, the order will be updated - in the database to closed. We will also then update the portfolio - and position balances accordingly. + This method will go over all local open orders and check if they are + changed on the exchange. If they are, the local order will be updated to match the status on the exchange. - If the order exists, we will check if the filled amount of the order - has changed. If the filled amount has changed, we will update the - order in the database and update the portfolio and position balances + Args: + portfolio: Portfolio object - if the status of an existing order has changed, we will update the - order in the database and update the portfolio and position balances - - During the syncing of the orders, new orders are not executed. They - are only created in the database. This is to prevent the algorithm - from executing orders that are already executed on the exchange. + Returns: + None """ portfolio_configuration = self.portfolio_configuration_service \ .get(portfolio.identifier) - symbols = self._get_symbols(portfolio) - positions = self.position_repository.get_all( - {"portfolio_id": portfolio_configuration.identifier} - ) - # Remove the portfolio trading symbol from the symbols - symbols = [ - symbol for symbol in symbols if symbol != portfolio.trading_symbol - ] + open_orders = self.order_repository.get_all( + { + "portfolio": portfolio.identifier, + "status": OrderStatus.OPEN.value + } + ) - # Check if there are orders for the available symbols - for symbol in symbols: - symbol = f"{symbol.upper()}" - orders = self.market_service.get_orders( - symbol=symbol, - since=portfolio_configuration.track_from, + for order in open_orders: + external_orders = self.market_service.get_orders( + symbol=order.get_symbol(), + since=order.created_at, market=portfolio.market ) - if orders is not None and len(orders) > 0: - # Order the list of orders by created_at - ordered_external_order_list = sorted( - orders, key=lambda x: x.created_at - ) + for external_order in external_orders: - if portfolio_configuration.track_from is not None: - ordered_external_order_list = [ - order for order in ordered_external_order_list - if order.created_at >= portfolio_configuration - .track_from - ] - - for external_order in ordered_external_order_list: - - if self.order_repository.exists( - {"external_id": external_order.external_id} - ): - logger.info("Updating existing order") - order = self.order_repository.find( - {"external_id": external_order.external_id} - ) - self.order_repository.update( - order.id, external_order.to_dict() - ) - else: - logger.info( - "Creating new order, based on external order" - ) - data = external_order.to_dict() - data.pop("trade_closed_at", None) - data.pop("trade_closed_price", None) - data.pop("trade_closed_amount", None) - position_id = None - - # Get position id - for position in positions: - if position.symbol == external_order.target_symbol: - position_id = position.id - break - - # Create the new order - new_order_data = { - "target_symbol": external_order.target_symbol, - "trading_symbol": portfolio.trading_symbol, - "amount": external_order.amount, - "price": external_order.price, - "order_side": external_order.order_side, - "order_type": external_order.order_type, - "external_id": external_order.external_id, - "status": "open", - "position_id": position_id, - "created_at": external_order.created_at, - } - new_order = self.order_repository.create( - new_order_data, - ) - - # Update the order to its current status - # By default it should not sync the unallocated - # balance as this has already by done. - # Position amounts should be updated - update_data = { - "status": external_order.status, - "filled": external_order.filled, - "remaining": external_order.remaining, - "updated_at": external_order.created_at, - } - self.order_repository.update( - new_order.id, update_data - ) + if order.external_id == external_order.external_id: + self.order_repository.update( + order.id, external_order.to_dict() + ) def sync_trades(self, portfolio): orders = self.order_repository.get_all( diff --git a/investing_algorithm_framework/services/repository_service.py b/investing_algorithm_framework/services/repository_service.py index fb8a1c04..42887e7c 100644 --- a/investing_algorithm_framework/services/repository_service.py +++ b/investing_algorithm_framework/services/repository_service.py @@ -32,3 +32,6 @@ def count(self, query_params=None): def exists(self, query_params): return self.repository.exists(query_params) + + def save(self, object): + return self.repository.save(object) diff --git a/investing_algorithm_framework/services/strategy_orchestrator_service.py b/investing_algorithm_framework/services/strategy_orchestrator_service.py index ab14ee3f..44503f5b 100644 --- a/investing_algorithm_framework/services/strategy_orchestrator_service.py +++ b/investing_algorithm_framework/services/strategy_orchestrator_service.py @@ -69,11 +69,9 @@ def run_strategy(self, strategy, algorithm, sync=False): and len(strategy.market_data_sources) > 0: for data_id in strategy.market_data_sources: - if isinstance(data_id, MarketDataSource): market_data[data_id.get_identifier()] = \ - self.market_data_source_service\ - .get_data(identifier=data_id.get_identifier()) + data_id.get_data() else: market_data[data_id] = \ self.market_data_source_service \ diff --git a/poetry.lock b/poetry.lock index 0169cd64..a06ba9a0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiodns" @@ -341,6 +341,138 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] +[[package]] +name = "azure-common" +version = "1.1.28" +description = "Microsoft Azure Client Library for Python (Common)" +optional = false +python-versions = "*" +files = [ + {file = "azure-common-1.1.28.zip", hash = "sha256:4ac0cd3214e36b6a1b6a442686722a5d8cc449603aa833f3f0f40bda836704a3"}, + {file = "azure_common-1.1.28-py2.py3-none-any.whl", hash = "sha256:5c12d3dcf4ec20599ca6b0d3e09e86e146353d443e7fcc050c9a19c1f9df20ad"}, +] + +[[package]] +name = "azure-core" +version = "1.32.0" +description = "Microsoft Azure Core Library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "azure_core-1.32.0-py3-none-any.whl", hash = "sha256:eac191a0efb23bfa83fddf321b27b122b4ec847befa3091fa736a5c32c50d7b4"}, + {file = "azure_core-1.32.0.tar.gz", hash = "sha256:22b3c35d6b2dae14990f6c1be2912bf23ffe50b220e708a28ab1bb92b1c730e5"}, +] + +[package.dependencies] +requests = ">=2.21.0" +six = ">=1.11.0" +typing-extensions = ">=4.6.0" + +[package.extras] +aio = ["aiohttp (>=3.0)"] + +[[package]] +name = "azure-identity" +version = "1.19.0" +description = "Microsoft Azure Identity Library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "azure_identity-1.19.0-py3-none-any.whl", hash = "sha256:e3f6558c181692d7509f09de10cca527c7dce426776454fb97df512a46527e81"}, + {file = "azure_identity-1.19.0.tar.gz", hash = "sha256:500144dc18197d7019b81501165d4fa92225f03778f17d7ca8a2a180129a9c83"}, +] + +[package.dependencies] +azure-core = ">=1.31.0" +cryptography = ">=2.5" +msal = ">=1.30.0" +msal-extensions = ">=1.2.0" +typing-extensions = ">=4.0.0" + +[[package]] +name = "azure-mgmt-core" +version = "1.5.0" +description = "Microsoft Azure Management Core Library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "azure_mgmt_core-1.5.0-py3-none-any.whl", hash = "sha256:18aaa5a723ee8ae05bf1bfc9f6d0ffb996631c7ea3c922cc86f522973ce07b5f"}, + {file = "azure_mgmt_core-1.5.0.tar.gz", hash = "sha256:380ae3dfa3639f4a5c246a7db7ed2d08374e88230fd0da3eb899f7c11e5c441a"}, +] + +[package.dependencies] +azure-core = ">=1.31.0" + +[[package]] +name = "azure-mgmt-resource" +version = "23.2.0" +description = "Microsoft Azure Resource Management Client Library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "azure_mgmt_resource-23.2.0-py3-none-any.whl", hash = "sha256:7af2bca928ecd58e57ea7f7731d245f45e9d927036d82f1d30b96baa0c26b569"}, + {file = "azure_mgmt_resource-23.2.0.tar.gz", hash = "sha256:747b750df7af23ab30e53d3f36247ab0c16de1e267d666b1a5077c39a4292529"}, +] + +[package.dependencies] +azure-common = ">=1.1" +azure-mgmt-core = ">=1.3.2" +isodate = ">=0.6.1" +typing-extensions = ">=4.6.0" + +[[package]] +name = "azure-mgmt-storage" +version = "21.2.1" +description = "Microsoft Azure Storage Management Client Library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "azure-mgmt-storage-21.2.1.tar.gz", hash = "sha256:503a7ff9c31254092b0656445f5728bfdfda2d09d46a82e97019eaa9a1ecec64"}, + {file = "azure_mgmt_storage-21.2.1-py3-none-any.whl", hash = "sha256:f97df1fa39cde9dbacf2cd96c9cba1fc196932185e24853e276f74b18a0bd031"}, +] + +[package.dependencies] +azure-common = ">=1.1" +azure-mgmt-core = ">=1.3.2" +isodate = ">=0.6.1" + +[[package]] +name = "azure-mgmt-web" +version = "7.3.1" +description = "Microsoft Azure Web Apps Management Client Library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "azure-mgmt-web-7.3.1.tar.gz", hash = "sha256:87b771436bc99a7a8df59d0ad185b96879a06dce14764a06b3fc3dafa8fcb56b"}, + {file = "azure_mgmt_web-7.3.1-py3-none-any.whl", hash = "sha256:ccf881e3ab31c3fdbf9cbff32773d9c0006b5dcd621ea074d7ec89e51049fb72"}, +] + +[package.dependencies] +azure-common = ">=1.1" +azure-mgmt-core = ">=1.3.2" +isodate = ">=0.6.1" +typing-extensions = ">=4.6.0" + +[[package]] +name = "azure-storage-blob" +version = "12.24.0" +description = "Microsoft Azure Blob Storage Client Library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "azure_storage_blob-12.24.0-py3-none-any.whl", hash = "sha256:4f0bb4592ea79a2d986063696514c781c9e62be240f09f6397986e01755bc071"}, + {file = "azure_storage_blob-12.24.0.tar.gz", hash = "sha256:eaaaa1507c8c363d6e1d1342bd549938fdf1adec9b1ada8658c8f5bf3aea844e"}, +] + +[package.dependencies] +azure-core = ">=1.30.0" +cryptography = ">=2.1.4" +isodate = ">=0.6.1" +typing-extensions = ">=4.6.0" + +[package.extras] +aio = ["azure-core[aio] (>=1.30.0)"] + [[package]] name = "babel" version = "2.16.0" @@ -1452,6 +1584,17 @@ widgetsnbextension = ">=4.0.12,<4.1.0" [package.extras] test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"] +[[package]] +name = "isodate" +version = "0.7.2" +description = "An ISO 8601 date/time/duration parser and formatter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15"}, + {file = "isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6"}, +] + [[package]] name = "isoduration" version = "20.11.0" @@ -1984,6 +2127,40 @@ files = [ {file = "mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8"}, ] +[[package]] +name = "msal" +version = "1.31.1" +description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect." +optional = false +python-versions = ">=3.7" +files = [ + {file = "msal-1.31.1-py3-none-any.whl", hash = "sha256:29d9882de247e96db01386496d59f29035e5e841bcac892e6d7bf4390bf6bd17"}, + {file = "msal-1.31.1.tar.gz", hash = "sha256:11b5e6a3f802ffd3a72107203e20c4eac6ef53401961b880af2835b723d80578"}, +] + +[package.dependencies] +cryptography = ">=2.5,<46" +PyJWT = {version = ">=1.0.0,<3", extras = ["crypto"]} +requests = ">=2.0.0,<3" + +[package.extras] +broker = ["pymsalruntime (>=0.14,<0.18)", "pymsalruntime (>=0.17,<0.18)"] + +[[package]] +name = "msal-extensions" +version = "1.2.0" +description = "Microsoft Authentication Library extensions (MSAL EX) provides a persistence API that can save your data on disk, encrypted on Windows, macOS and Linux. Concurrent data access will be coordinated by a file lock mechanism." +optional = false +python-versions = ">=3.7" +files = [ + {file = "msal_extensions-1.2.0-py3-none-any.whl", hash = "sha256:cf5ba83a2113fa6dc011a254a72f1c223c88d7dfad74cc30617c4679a417704d"}, + {file = "msal_extensions-1.2.0.tar.gz", hash = "sha256:6f41b320bfd2933d631a215c91ca0dd3e67d84bd1a2f50ce917d5874ec646bef"}, +] + +[package.dependencies] +msal = ">=1.29,<2" +portalocker = ">=1.4,<3" + [[package]] name = "multidict" version = "6.1.0" @@ -2506,6 +2683,25 @@ timezone = ["backports-zoneinfo", "tzdata"] xlsx2csv = ["xlsx2csv (>=0.8.0)"] xlsxwriter = ["xlsxwriter"] +[[package]] +name = "portalocker" +version = "2.10.1" +description = "Wraps the portalocker recipe for easy usage" +optional = false +python-versions = ">=3.8" +files = [ + {file = "portalocker-2.10.1-py3-none-any.whl", hash = "sha256:53a5984ebc86a025552264b459b46a2086e269b21823cb572f8f28ee759e45bf"}, + {file = "portalocker-2.10.1.tar.gz", hash = "sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f"}, +] + +[package.dependencies] +pywin32 = {version = ">=226", markers = "platform_system == \"Windows\""} + +[package.extras] +docs = ["sphinx (>=1.7.1)"] +redis = ["redis"] +tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "pytest-timeout (>=2.1.0)", "redis", "sphinx (>=6.0.0)", "types-redis"] + [[package]] name = "prometheus-client" version = "0.21.0" @@ -2863,6 +3059,26 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pyjwt" +version = "2.10.1" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, + {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, +] + +[package.dependencies] +cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -2877,6 +3093,20 @@ files = [ [package.dependencies] six = ">=1.5" +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + [[package]] name = "python-json-logger" version = "2.0.7" @@ -4000,4 +4230,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.0" python-versions = ">=3.10" -content-hash = "dab26b4b5c0d8b06086270c4700cdba75ef193bb6865ffe74992ea9c9848d820" +content-hash = "c3b534a33f06b7123370d8957387ec9aac9c91b87d5c475650c24f2ed8be6db0" diff --git a/pyproject.toml b/pyproject.toml index 93ff40df..5608fdc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,12 @@ jupyter = "^1.0.0" numpy = "^2.1.3" scipy = "^1.14.1" tulipy = "^0.4.0" +azure-storage-blob = "^12.24.0" +azure-identity = "^1.19.0" +azure-mgmt-storage = "^21.2.1" +azure-mgmt-web = "^7.3.1" +azure-mgmt-resource = "^23.2.0" +python-dotenv = "^1.0.1" [tool.poetry.group.test.dependencies] coverage= "7.4.2" @@ -38,4 +44,8 @@ ipykernel = "^6.29.5" [build-system] requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" \ No newline at end of file +build-backend = "poetry.core.masonry.api" + +[tool.poetry.scripts] +deploy_to_azure_function = "investing_algorithm_framework.cli.deploy_to_azure_function:cli" +create_azure_function_app_skeleton = "investing_algorithm_framework.cli.create_azure_function_app_skeleton:cli" diff --git a/tests/app/algorithm/test_create_market_sell_order.py b/tests/app/algorithm/test_create_market_sell_order.py index 05289e8a..d20f17a4 100644 --- a/tests/app/algorithm/test_create_market_sell_order.py +++ b/tests/app/algorithm/test_create_market_sell_order.py @@ -41,7 +41,7 @@ def test_create_market_sell_order(self): order_service = self.app.container.order_service() trading_symbol_position = position_service.find({"symbol": "EUR"}) self.assertEqual(990, trading_symbol_position.get_amount()) - self.app.run(number_of_iterations=1, sync=False) + self.app.run(number_of_iterations=1) trading_symbol_position = position_service.find({"symbol": "EUR"}) self.assertEqual(990, trading_symbol_position.get_amount()) btc_position = position_service.find({"symbol": "BTC"}) diff --git a/tests/app/algorithm/test_get_data.py b/tests/app/algorithm/test_get_data.py deleted file mode 100644 index eaa34407..00000000 --- a/tests/app/algorithm/test_get_data.py +++ /dev/null @@ -1,66 +0,0 @@ -# import os -# from datetime import datetime, timedelta -# from investing_algorithm_framework import create_app, -# TradingStrategy, TimeUnit, RESOURCE_DIRECTORY, PortfolioConfiguration, -# TradingDataType, TradingTimeFrame -# from tests.resources import TestBase, MarketServiceStub -# -# -# class StrategyOne(TradingStrategy): -# time_unit = TimeUnit.SECOND -# interval = 2 -# trading_data_types = [ -# TradingDataType.OHLCV, -# TradingDataType.TICKER, -# TradingDataType.ORDER_BOOK -# ] -# trading_time_frame = TradingTimeFrame.ONE_MINUTE -# trading_time_frame_start_date = datetime.utcnow() - timedelta(days=1) -# symbols = ["ETH/EUR", "BTC/EUR"] -# market = "BITVAVO" -# market_data = False -# -# def apply_strategy(self, algorithm, market_data): -# algorithm.create_limit_order( -# target_symbol="BTC", -# amount=1, -# order_side="BUY", -# price=10, -# ) -# -# -# class Test(TestBase): -# -# def setUp(self) -> None: -# self.resource_dir = os.path.abspath( -# os.path.join( -# os.path.join( -# os.path.join( -# os.path.join( -# os.path.realpath(__file__), -# os.pardir -# ), -# os.pardir -# ), -# os.pardir -# ), -# "resources" -# ) -# ) -# self.app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir}) -# self.app.add_portfolio_configuration( -# PortfolioConfiguration( -# market="bitvavo", -# api_key="test", -# secret_key="test", -# trading_symbol="EUR" -# ) -# ) -# self.app.container.market_service.override(MarketServiceStub(None)) -# self.app.add_strategy(StrategyOne) -# self.app.initialize() -# -# def test_get_data(self): -# self.app.run(number_of_iterations=1, sync=False) -# order_service = self.app.container.order_service() -# order_service.check_pending_orders() diff --git a/tests/app/algorithm/test_get_pending_orders.py b/tests/app/algorithm/test_get_pending_orders.py index f661c4b3..559d30a3 100644 --- a/tests/app/algorithm/test_get_pending_orders.py +++ b/tests/app/algorithm/test_get_pending_orders.py @@ -18,7 +18,7 @@ class TestPortfolioService(TestBase): secret_key="secret_key", ) ] - external_orders = [ + initial_orders = [ Order.from_dict( { "id": "1323", @@ -65,14 +65,9 @@ class TestPortfolioService(TestBase): }, ), ] - external_available_symbols = [ - "BTC/EUR", "DOT/EUR", "ADA/EUR", "ETH/EUR" - ] external_balances = { "EUR": 700, - "BTC": 10, } - config = {SYMBOLS: ["BTC/EUR", "DOT/EUR", "ETH/EUR", "ADA/EUR"]} def test_get_pending_orders(self): """ @@ -115,7 +110,7 @@ def test_get_pending_orders(self): # Check if all positions are made position_service = self.app.container.position_service() - self.assertEqual(5, position_service.count()) + self.assertEqual(4, position_service.count()) # Check if btc position exists btc_position = position_service.find( @@ -139,7 +134,7 @@ def test_get_pending_orders(self): eur_position = position_service.find( {"portfolio_id": portfolio.id, "symbol": "EUR"} ) - self.assertEqual(700, eur_position.amount) + self.assertEqual(400, eur_position.amount) pending_orders = self.app.algorithm.get_pending_orders() self.assertEqual(2, len(pending_orders)) diff --git a/tests/app/algorithm/test_get_portfolio.py b/tests/app/algorithm/test_get_portfolio.py index 07d4e8a8..a669d60c 100644 --- a/tests/app/algorithm/test_get_portfolio.py +++ b/tests/app/algorithm/test_get_portfolio.py @@ -24,6 +24,6 @@ class Test(TestBase): ) ] - def test_create_limit_buy_order_with_percentage_of_portfolio(self): + def test_get_portfolio(self): portfolio = self.app.algorithm.get_portfolio() self.assertEqual(Decimal(1000), portfolio.get_unallocated()) diff --git a/tests/app/algorithm/test_get_unfilled_buy_value.py b/tests/app/algorithm/test_get_unfilled_buy_value.py index 2861e9b0..806745b4 100644 --- a/tests/app/algorithm/test_get_unfilled_buy_value.py +++ b/tests/app/algorithm/test_get_unfilled_buy_value.py @@ -8,7 +8,7 @@ class Test(TestBase): """ Test for functionality of algorithm get_unfilled_buy_value """ - external_orders = [ + initial_orders = [ Order.from_dict( { "id": "1323", @@ -56,15 +56,8 @@ class Test(TestBase): ) ] external_balances = { - "EUR": 1000, - "BTC": 10, - "DOT": 0, - "ETH": 0 + "EUR": 1000 } - config = { - SYMBOLS: ["BTC/EUR", "DOT/EUR", "ETH/EUR"], - } - external_available_symbols = ["BTC/EUR", "DOT/EUR", "ETH/EUR"] portfolio_configurations = [ PortfolioConfiguration( market="BITVAVO", @@ -85,6 +78,14 @@ def test_get_unfilled_buy_value(self): The test should make sure that the portfolio service can sync existing orders from the market service to the order service. + + Orders overview: + - BTC/EUR: 10 10.0 (filled) + - DOT/EUR: 10 10.0 (unfilled) + - ETH/EUR: 10 10.0 (unfilled) + + The unfilled buy value should be 200 + The unallocated amount should be 700 """ portfolio_service: PortfolioService \ = self.app.container.portfolio_service() @@ -144,7 +145,7 @@ def test_get_unfilled_buy_value(self): eur_position = position_service.find( {"portfolio_id": portfolio.id, "symbol": "EUR"} ) - self.assertEqual(1000, eur_position.amount) + self.assertEqual(700, eur_position.amount) pending_orders = self.app.algorithm.get_pending_orders() self.assertEqual(2, len(pending_orders)) diff --git a/tests/app/algorithm/test_get_unfilled_sell_value.py b/tests/app/algorithm/test_get_unfilled_sell_value.py index 55960ba3..96940771 100644 --- a/tests/app/algorithm/test_get_unfilled_sell_value.py +++ b/tests/app/algorithm/test_get_unfilled_sell_value.py @@ -22,11 +22,7 @@ class Test(TestBase): secret_key="secret_key", ) ] - config = { - SYMBOLS: ["BTC/EUR", "DOT/EUR", "ADA/EUR", "ETH/EUR"] - } - external_available_symbols = ["BTC/EUR", "DOT/EUR", "ADA/EUR", "ETH/EUR"] - external_orders = [ + initial_orders = [ Order.from_dict( { "id": "1323", @@ -116,10 +112,7 @@ class Test(TestBase): ), ] external_balances = { - "EUR": 1000, - "BTC": 0, - "DOT": 0, - "ETH": 0, + "EUR": 1000 } def test_get_unfilled_sell_value(self): @@ -128,6 +121,16 @@ def test_get_unfilled_sell_value(self): The test should make sure that the portfolio service can sync existing orders from the market service to the order service. + + Order overview: + - BTC/EUR BUY 10 10.0 + - BTC/EUR SELL 10 20.0 (filled, profit 10*10=100) + - DOT/EUR BUY 10 10.0 + - ETH/EUR BUY 10 10.0 + - ETH/EUR SELL 10 10.0 (unfilled) + - DOT/EUR SELL 10 10.0 (unfilled) + + At the end of the order history unallocated amount should be 900 """ portfolio_service: PortfolioService \ = self.app.container.portfolio_service() @@ -163,7 +166,7 @@ def test_get_unfilled_sell_value(self): # Check if all positions are made position_service = self.app.container.position_service() - self.assertEqual(5, position_service.count()) + self.assertEqual(4, position_service.count()) # Check if btc position exists btc_position = position_service.find( @@ -187,12 +190,12 @@ def test_get_unfilled_sell_value(self): eur_position = position_service.find( {"portfolio_id": portfolio.id, "symbol": "EUR"} ) - self.assertEqual(1000, eur_position.amount) + self.assertEqual(900, eur_position.amount) pending_orders = self.app.algorithm.get_pending_orders() self.assertEqual(2, len(pending_orders)) - # Check the unfilled buy value + # Check the unfilled sell value unfilled_sell_value = self.app.algorithm.get_unfilled_sell_value() self.assertEqual(200, unfilled_sell_value) @@ -215,3 +218,9 @@ def test_get_unfilled_sell_value(self): # Check the unfilled buy value unfilled_sell_value = self.app.algorithm.get_unfilled_sell_value() self.assertEqual(100, unfilled_sell_value) + + # Check if eur position exists + eur_position = position_service.find( + {"portfolio_id": portfolio.id, "symbol": "EUR"} + ) + self.assertEqual(1000, eur_position.amount) diff --git a/tests/app/algorithm/test_has_open_buy_orders.py b/tests/app/algorithm/test_has_open_buy_orders.py index a2dd1d7e..dfc236a4 100644 --- a/tests/app/algorithm/test_has_open_buy_orders.py +++ b/tests/app/algorithm/test_has_open_buy_orders.py @@ -1,7 +1,7 @@ from decimal import Decimal from investing_algorithm_framework import PortfolioConfiguration, \ - MarketCredential, SYMBOLS, Order + MarketCredential from tests.resources import TestBase @@ -20,117 +20,24 @@ class Test(TestBase): secret_key="secret_key", ) ] - config = { - SYMBOLS: ["BTC/EUR", "DOT/EUR", "ADA/EUR", "ETH/EUR"] - } - external_available_symbols = ["BTC/EUR", "DOT/EUR", "ADA/EUR", "ETH/EUR"] - external_orders = [ - Order.from_dict( - { - "id": "1323", - "side": "buy", - "symbol": "BTC/EUR", - "amount": 10, - "price": 10.0, - "status": "CLOSED", - "order_type": "limit", - "order_side": "buy", - "created_at": "2023-08-08T14:40:56.626362Z", - "filled": 10, - "remaining": 0, - }, - ), - Order.from_dict( - { - "id": "132343", - "order_side": "SELL", - "symbol": "BTC/EUR", - "amount": 10, - "price": 20.0, - "status": "CLOSED", - "order_type": "limit", - "created_at": "2023-08-08T14:40:56.626362Z", - "filled": 10, - "remaining": 0, - }, - ), - Order.from_dict( - { - "id": "14354", - "side": "buy", - "symbol": "DOT/EUR", - "amount": 10, - "price": 10.0, - "status": "CLOSED", - "order_type": "limit", - "order_side": "buy", - "created_at": "2023-09-22T14:40:56.626362Z", - "filled": 10, - "remaining": 0, - }, - ), - Order.from_dict( - { - "id": "49394", - "side": "buy", - "symbol": "ETH/EUR", - "amount": 10, - "price": 10.0, - "status": "CLOSED", - "order_type": "limit", - "order_side": "buy", - "created_at": "2023-08-08T14:40:56.626362Z", - "filled": 10, - "remaining": 0, - }, - ), - Order.from_dict( - { - "id": "4939424", - "order_side": "sell", - "symbol": "ETH/EUR", - "amount": 10, - "price": 10.0, - "status": "OPEN", - "order_type": "limit", - "created_at": "2023-08-08T14:40:56.626362Z", - "filled": 0, - "remaining": 0, - }, - ), - Order.from_dict( - { - "id": "493943434", - "order_side": "sell", - "symbol": "DOT/EUR", - "amount": 10, - "price": 10.0, - "status": "OPEN", - "order_type": "limit", - "created_at": "2023-08-08T14:40:56.626362Z", - "filled": 0, - "remaining": 0, - }, - ), - ] + external_orders = [] external_balances = { "EUR": 1000, - "BTC": 0, - "DOT": 0, - "ETH": 0, } def test_has_open_buy_orders(self): trading_symbol_position = self.app.algorithm.get_position("EUR") self.assertEqual(Decimal(1000), trading_symbol_position.get_amount()) - self.assertTrue(self.app.algorithm.position_exists(symbol="BTC")) - self.app.algorithm.create_limit_order( + order = self.app.algorithm.create_limit_order( target_symbol="BTC", amount=1, price=10, order_side="BUY", ) order_service = self.app.container.order_service() + order = order_service.find({"symbol": "BTC/EUR"}) + position_service = self.app.container.position_service() + position = position_service.find({"symbol": "BTC"}) self.assertTrue(self.app.algorithm.has_open_buy_orders("BTC")) order_service.check_pending_orders() self.assertFalse(self.app.algorithm.has_open_buy_orders("BTC")) diff --git a/tests/app/algorithm/test_run_strategy.py b/tests/app/algorithm/test_run_strategy.py index 19e18269..07262d9b 100644 --- a/tests/app/algorithm/test_run_strategy.py +++ b/tests/app/algorithm/test_run_strategy.py @@ -38,7 +38,6 @@ class Test(TestCase): secret_key="secret_key", ) ] - external_available_symbols = ["BTC/EUR", "DOT/EUR", "ADA/EUR", "ETH/EUR"] external_balances = { "EUR": 1000, } @@ -61,6 +60,20 @@ def setUp(self) -> None: ) ) + def tearDown(self) -> None: + super().tearDown() + # Delete the resources database directory + + database_dir = os.path.join(self.resource_dir, "databases") + + if os.path.exists(database_dir): + for root, dirs, files in os.walk(database_dir, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + + def test_with_strategy_object(self): app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir}) app.container.market_service.override(MarketServiceStub(None)) @@ -119,7 +132,7 @@ def run_strategy(algorithm, market_data): self.assertTrue(strategy_orchestration_service.has_run("run_strategy")) def test_stateless(self): - app = create_app(stateless=True) + app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir}) app.container.market_service.override(MarketServiceStub(None)) app.container.portfolio_configuration_service().clear() app.add_portfolio_configuration( diff --git a/tests/app/backtesting/test_backtest_report.py b/tests/app/backtesting/test_backtest_report.py index 720cccbe..b47f3da5 100644 --- a/tests/app/backtesting/test_backtest_report.py +++ b/tests/app/backtesting/test_backtest_report.py @@ -38,13 +38,23 @@ def setUp(self) -> None: ) ) - def test_report_csv_creation(self): + def tearDown(self) -> None: + database_dir = os.path.join( + self.resource_dir, "databases" + ) + + if os.path.exists(database_dir): + for root, dirs, files in os.walk(database_dir, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + + def test_report_json_creation(self): """ Test if the backtest report is created as a CSV file """ - app = create_app( - config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir} - ) + app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir}) algorithm = Algorithm() algorithm.add_strategy(TestStrategy()) app.add_algorithm(algorithm) @@ -71,89 +81,13 @@ def test_report_csv_creation(self): os.path.isfile(os.path.join(self.resource_dir, file_path)) ) - def test_report_csv_creation_without_strategy_identifier(self): - """ - Test if the backtest report is created as a CSV file - when the strategy does not have an identifier - """ - app = create_app( - config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir} - ) - strategy = TestStrategy() - strategy.strategy_id = None - algorithm = Algorithm() - algorithm.add_strategy(strategy) - app.add_portfolio_configuration( - PortfolioConfiguration( - market="bitvavo", - trading_symbol="EUR", - initial_balance=1000 - ) - ) - backtest_date_range = BacktestDateRange( - start_date=datetime.utcnow() - timedelta(days=1), - end_date=datetime.utcnow() - ) - report = app.run_backtest( - algorithm=algorithm, - backtest_date_range=backtest_date_range - ) - file_path = BacktestReportWriterService.create_report_name( - report, os.path.join(self.resource_dir, "backtest_reports") - ) - # Check if the backtest report exists - self.assertTrue( - os.path.isfile(os.path.join(self.resource_dir, file_path)) - ) - - def test_report_csv_creation_with_multiple_strategies(self): - """ - Test if the backtest report is created as a CSV file - when there are multiple strategies - """ - app = create_app( - config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir} - ) - strategy = TestStrategy() - strategy.strategy_id = None - algorithm = Algorithm() - algorithm.add_strategy(strategy) - - @algorithm.strategy() - def run_strategy(algorithm, market_data): - pass - - app.add_portfolio_configuration( - PortfolioConfiguration( - market="bitvavo", - trading_symbol="EUR", - initial_balance=1000 - ) - ) - - self.assertEqual(2, len(algorithm.strategies)) - backtest_date_range = BacktestDateRange( - start_date=datetime.utcnow() - timedelta(days=1), - end_date=datetime.utcnow() - ) - report = app.run_backtest( - algorithm=algorithm, backtest_date_range=backtest_date_range - ) - file_path = BacktestReportWriterService.create_report_name( - report, os.path.join(self.resource_dir, "backtest_reports") - ) - # Check if the backtest report exists - self.assertTrue( - os.path.isfile(os.path.join(self.resource_dir, file_path)) - ) - def test_report_json_creation_with_multiple_strategies_with_id(self): """ Test if the backtest report is created as a CSV file when there are multiple strategies with identifiers """ app = create_app( - config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir} + config={RESOURCE_DIRECTORY: self.resource_dir} ) algorithm = Algorithm() diff --git a/tests/app/backtesting/test_run_backtest.py b/tests/app/backtesting/test_run_backtest.py index 33168bbf..308803c1 100644 --- a/tests/app/backtesting/test_run_backtest.py +++ b/tests/app/backtesting/test_run_backtest.py @@ -38,6 +38,18 @@ def setUp(self) -> None: ) ) + def tearDown(self) -> None: + database_dir = os.path.join( + self.resource_dir, "databases" + ) + + if os.path.exists(database_dir): + for root, dirs, files in os.walk(database_dir, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + def test_report_csv_creation(self): """ Test if the backtest report is created as a CSV file @@ -74,7 +86,7 @@ def test_report_csv_creation_without_strategy_identifier(self): when the strategy does not have an identifier """ app = create_app( - config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir} + config={RESOURCE_DIRECTORY: self.resource_dir} ) strategy = TestStrategy() strategy.strategy_id = None @@ -106,7 +118,7 @@ def test_report_csv_creation_with_multiple_strategies(self): when there are multiple strategies """ app = create_app( - config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir} + config={RESOURCE_DIRECTORY: self.resource_dir} ) strategy = TestStrategy() strategy.strategy_id = None @@ -145,7 +157,7 @@ def test_report_csv_creation_with_multiple_strategies_with_id(self): when there are multiple strategies with identifiers """ app = create_app( - config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir} + config={RESOURCE_DIRECTORY: self.resource_dir} ) algorithm = Algorithm() diff --git a/tests/app/backtesting/test_run_backtests.py b/tests/app/backtesting/test_run_backtests.py index 2fd1a159..387d2ea3 100644 --- a/tests/app/backtesting/test_run_backtests.py +++ b/tests/app/backtesting/test_run_backtests.py @@ -38,12 +38,22 @@ def setUp(self) -> None: ) ) + def tearDown(self) -> None: + database_dir = os.path.join(self.resource_dir, "databases") + + if os.path.exists(database_dir): + for root, dirs, files in os.walk(database_dir, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + def test_run_backtests(self): """ Test if all backtests are run when multiple algorithms are provided """ app = create_app( - config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir} + config={RESOURCE_DIRECTORY: self.resource_dir} ) # Add all algorithms diff --git a/tests/app/test_add_config.py b/tests/app/test_add_config.py index 4b43b489..9c3c32a3 100644 --- a/tests/app/test_add_config.py +++ b/tests/app/test_add_config.py @@ -1,29 +1,29 @@ -import os -from unittest import TestCase +# import os +# from unittest import TestCase -from investing_algorithm_framework import create_app, RESOURCE_DIRECTORY +# from investing_algorithm_framework import create_app, RESOURCE_DIRECTORY -class Test(TestCase): +# class Test(TestCase): - def setUp(self) -> None: - self.resource_dir = os.path.abspath( - os.path.join( - os.path.join( - os.path.join( - os.path.realpath(__file__), - os.pardir - ), - os.pardir - ), - "resources" - ) - ) +# def setUp(self) -> None: +# self.resource_dir = os.path.abspath( +# os.path.join( +# os.path.join( +# os.path.join( +# os.path.realpath(__file__), +# os.pardir +# ), +# os.pardir +# ), +# "resources" +# ) +# ) - def test_add(self): - app = create_app( - config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir} - ) - self.assertIsNotNone(app.config) - self.assertIsNotNone(app.config.get("test")) - self.assertIsNotNone(app.config.get(RESOURCE_DIRECTORY)) +# def test_add(self): +# app = create_app( +# config={"test": "test", RESOURCE_DIRECTORY: self.resource_dir} +# ) +# self.assertIsNotNone(app.config) +# self.assertIsNotNone(app.config.get("test")) +# self.assertIsNotNone(app.config.get(RESOURCE_DIRECTORY)) diff --git a/tests/app/test_add_portfolio_configuration.py b/tests/app/test_add_portfolio_configuration.py index 491ddfae..a71c2ab2 100644 --- a/tests/app/test_add_portfolio_configuration.py +++ b/tests/app/test_add_portfolio_configuration.py @@ -1,27 +1,31 @@ -from investing_algorithm_framework import PortfolioConfiguration, \ - MarketCredential -from tests.resources import TestBase +# from investing_algorithm_framework import PortfolioConfiguration, \ +# MarketCredential +# from tests.resources import TestBase -class Test(TestBase): - portfolio_configurations = [ - PortfolioConfiguration( - market="BITVAVO", - trading_symbol="EUR" - ) - ] - market_credentials = [ - MarketCredential( - market="BITVAVO", - api_key="api_key", - secret_key="secret_key" - ) - ] - external_balances = { - "EUR": 1000, - } +# class Test(TestBase): +# portfolio_configurations = [ +# PortfolioConfiguration( +# market="BITVAVO", +# trading_symbol="EUR" +# ) +# ] +# market_credentials = [ +# MarketCredential( +# market="BITVAVO", +# api_key="api_key", +# secret_key="secret_key" +# ) +# ] +# external_balances = { +# "EUR": 1000, +# } - def test_add(self): - self.assertEqual(1, self.app.algorithm.portfolio_service.count()) - self.assertEqual(1, self.app.algorithm.position_service.count()) - self.assertEqual(1000, self.app.algorithm.get_unallocated()) +# def test_add(self): +# self.assertEqual(1, self.app.algorithm.portfolio_service.count()) +# self.assertEqual(1, self.app.algorithm.position_service.count()) +# self.assertEqual(1000, self.app.algorithm.get_unallocated()) + +# # Make sure that the portfolio is initialized +# portfolio = self.app.algorithm.get_portfolio() +# self.assertTrue(portfolio.initialized) diff --git a/tests/app/test_app_initialize.py b/tests/app/test_app_initialize.py index 6fb325c4..66b5624f 100644 --- a/tests/app/test_app_initialize.py +++ b/tests/app/test_app_initialize.py @@ -1,126 +1,97 @@ -import os -from unittest import TestCase +# import os +# from unittest import TestCase -from investing_algorithm_framework import create_app, PortfolioConfiguration, \ - MarketCredential, Algorithm, AppMode, APP_MODE -from investing_algorithm_framework.domain import SQLALCHEMY_DATABASE_URI -from tests.resources import MarketServiceStub +# from investing_algorithm_framework import create_app, PortfolioConfiguration, \ +# MarketCredential, Algorithm, AppMode, APP_MODE, RESOURCE_DIRECTORY +# from investing_algorithm_framework.domain import SQLALCHEMY_DATABASE_URI +# from tests.resources import MarketServiceStub -class TestAppInitialize(TestCase): - portfolio_configurations = [ - PortfolioConfiguration( - market="BITVAVO", - trading_symbol="EUR" - ) - ] - market_credentials = [ - MarketCredential( - market="BITVAVO", - api_key="api_key", - secret_key="secret_key" - ) - ] - external_balances = { - "EUR": 1000, - } +# class TestAppInitialize(TestCase): +# portfolio_configurations = [ +# PortfolioConfiguration( +# market="BITVAVO", +# trading_symbol="EUR" +# ) +# ] +# market_credentials = [ +# MarketCredential( +# market="BITVAVO", +# api_key="api_key", +# secret_key="secret_key" +# ) +# ] +# external_balances = { +# "EUR": 1000, +# } - def setUp(self) -> None: - self.resource_dir = os.path.abspath( - os.path.join( - os.path.join( - os.path.join( - os.path.realpath(__file__), - os.pardir - ), - os.pardir - ), - "resources" - ) - ) +# def setUp(self) -> None: +# self.resource_dir = os.path.abspath( +# os.path.join( +# os.path.join( +# os.path.join( +# os.path.realpath(__file__), +# os.pardir +# ), +# os.pardir +# ), +# "resources" +# ) +# ) - def test_app_initialize_default(self): - app = create_app( - config={"test": "test", 'resource_directory': self.resource_dir} - ) - app.container.market_service.override( - MarketServiceStub(app.container.market_credential_service()) - ) - app.add_portfolio_configuration( - PortfolioConfiguration( - market="BITVAVO", - trading_symbol="EUR", - ) - ) - algorithm = Algorithm() - app.add_algorithm(algorithm) - app.add_market_credential( - MarketCredential( - market="BITVAVO", - api_key="api_key", - secret_key="secret_key" - ) - ) - app.initialize() - self.assertIsNotNone(app.config) - self.assertIsNotNone(app._flask_app) - self.assertTrue(AppMode.DEFAULT.equals(app.config[APP_MODE])) - order_service = app.container.order_service() - self.assertEqual(0, order_service.count()) +# def test_app_initialize_default(self): +# app = create_app( +# config={RESOURCE_DIRECTORY: self.resource_dir} +# ) +# app.container.market_service.override( +# MarketServiceStub(app.container.market_credential_service()) +# ) +# app.add_portfolio_configuration( +# PortfolioConfiguration( +# market="BITVAVO", +# trading_symbol="EUR", +# ) +# ) +# algorithm = Algorithm() +# app.add_algorithm(algorithm) +# app.add_market_credential( +# MarketCredential( +# market="BITVAVO", +# api_key="api_key", +# secret_key="secret_key" +# ) +# ) +# app.initialize() +# self.assertIsNotNone(app.config) +# self.assertIsNone(app._flask_app) +# self.assertTrue(AppMode.DEFAULT.equals(app.config[APP_MODE])) +# order_service = app.container.order_service() +# self.assertEqual(0, order_service.count()) - def test_app_initialize_web(self): - app = create_app( - config={"test": "test", 'resource_directory': self.resource_dir}, - web=True - ) - app.container.market_service.override(MarketServiceStub(None)) - app.add_portfolio_configuration( - PortfolioConfiguration( - market="BITVAVO", - trading_symbol="EUR", - ) - ) - algorithm = Algorithm() - app.add_algorithm(algorithm) - app.add_market_credential( - MarketCredential( - market="BITVAVO", - api_key="api_key", - secret_key="secret_key" - ) - ) - app.initialize() - self.assertIsNotNone(app.config) - self.assertIsNotNone(app._flask_app) - self.assertTrue(AppMode.WEB.equals(app.config[APP_MODE])) - order_service = app.container.order_service() - self.assertEqual(0, order_service.count()) - - def test_app_initialize_stateless(self): - app = create_app( - config={"test": "test"}, - stateless=True - ) - app.container.market_service.override(MarketServiceStub(None)) - app.add_portfolio_configuration( - PortfolioConfiguration( - market="BITVAVO", - trading_symbol="EUR" - ) - ) - algorithm = Algorithm() - app.add_algorithm(algorithm) - app.add_market_credential( - MarketCredential( - market="BITVAVO", - api_key="api_key", - secret_key="secret_key" - ) - ) - app.initialize() - order_service = app.container.order_service() - self.assertIsNotNone(app.config) - self.assertIsNotNone(app._flask_app) - self.assertTrue(AppMode.STATELESS.equals(app.config[APP_MODE])) - self.assertEqual(app.config[SQLALCHEMY_DATABASE_URI], "sqlite://") - self.assertEqual(0, order_service.count()) +# def test_app_initialize_web(self): +# app = create_app( +# config={RESOURCE_DIRECTORY: self.resource_dir}, +# web=True +# ) +# app.container.market_service.override(MarketServiceStub(None)) +# app.add_portfolio_configuration( +# PortfolioConfiguration( +# market="BITVAVO", +# trading_symbol="EUR", +# ) +# ) +# algorithm = Algorithm() +# app.add_algorithm(algorithm) +# app.add_market_credential( +# MarketCredential( +# market="BITVAVO", +# api_key="api_key", +# secret_key="secret_key" +# ) +# ) +# app.initialize() +# self.assertIsNotNone(app.config) +# self.assertIsNotNone(app._flask_app) +# self.assertTrue(AppMode.WEB.equals(app.config[APP_MODE])) +# order_service = app.container.order_service() +# self.assertEqual(0, order_service.count()) diff --git a/tests/app/test_app_stateless.py b/tests/app/test_app_stateless.py deleted file mode 100644 index 4fee8c51..00000000 --- a/tests/app/test_app_stateless.py +++ /dev/null @@ -1,99 +0,0 @@ -from investing_algorithm_framework import PortfolioConfiguration, \ - MarketCredential, APP_MODE, AppMode, StatelessAction, create_app -from tests.resources import MarketServiceStub -from tests.resources import TestBase - - -class Test(TestBase): - config = { - APP_MODE: "stateless" - } - portfolio_configurations = [ - PortfolioConfiguration( - market="BITVAVO", - trading_symbol="EUR" - ) - ] - market_credentials = [ - MarketCredential( - market="BITVAVO", - api_key="api_key", - secret_key="secret_key" - ) - ] - external_balances = { - "EUR": 1000, - } - - def test_run(self): - config = self.app.config - self.assertTrue(AppMode.STATELESS.equals(config[APP_MODE])) - self.app.run( - number_of_iterations=1, - payload={"action": StatelessAction.RUN_STRATEGY.value} - ) - self.assertEqual(1000, self.app.algorithm.get_unallocated()) - trading_symbol_position = self.app.algorithm.get_position( - symbol="EUR" - ) - self.assertEqual(1000, trading_symbol_position.get_amount()) - - def test_run_with_changed_external_positions(self): - app = create_app(config={APP_MODE: AppMode.STATELESS.value}) - app.add_algorithm(self.algorithm) - app.add_market_credential( - MarketCredential( - market="BITVAVO", - api_key="api_key", - secret_key="secret_key" - ) - ) - app.add_portfolio_configuration( - PortfolioConfiguration( - market="BITVAVO", - trading_symbol="EUR" - ) - ) - market_service = MarketServiceStub(None) - market_service.balances = { - "EUR": 1000, - "BTC": 1 - } - app.container.market_service.override(market_service) - app.initialize() - self.assertTrue(AppMode.STATELESS.equals(app.config[APP_MODE])) - self.app.run(payload={ - "action": StatelessAction.RUN_STRATEGY.value, - }) - self.assertEqual(1000, app.algorithm.get_unallocated()) - trading_symbol_position = self.app.algorithm.get_position( - symbol="EUR" - ) - self.assertEqual(1000, trading_symbol_position.get_amount()) - app = create_app(config={APP_MODE: AppMode.STATELESS.value}) - app.add_algorithm(self.algorithm) - app.add_market_credential( - MarketCredential( - market="BITVAVO", - api_key="api_key", - secret_key="secret_key" - ) - ) - app.add_portfolio_configuration( - PortfolioConfiguration( - market="BITVAVO", - trading_symbol="EUR" - ) - ) - market_service = MarketServiceStub(None) - market_service.balances = { - "EUR": 2000, - "BTC": 1 - } - app.container.market_service.override(market_service) - app.initialize() - self.assertEqual(2000, app.algorithm.get_unallocated()) - trading_symbol_position = self.app.algorithm.get_position( - symbol="EUR" - ) - self.assertEqual(2000, trading_symbol_position.get_amount()) diff --git a/tests/app/test_backtesting.py b/tests/app/test_backtesting.py index 735090d6..f4ecb8c9 100644 --- a/tests/app/test_backtesting.py +++ b/tests/app/test_backtesting.py @@ -1,43 +1,43 @@ -from investing_algorithm_framework import TradingStrategy, Algorithm +# from investing_algorithm_framework import TradingStrategy, Algorithm -class SimpleTradingStrategy(TradingStrategy): - interval = 2 - time_unit = "hour" +# class SimpleTradingStrategy(TradingStrategy): +# interval = 2 +# time_unit = "hour" - def apply_strategy(self, algorithm: Algorithm, market_data): +# def apply_strategy(self, algorithm: Algorithm, market_data): - if algorithm.has_open_orders(): - return +# if algorithm.has_open_orders(): +# return - algorithm.create_limit_order( - target_symbol="BTC", - amount=0.01, - price=10000, - order_side="buy" - ) +# algorithm.create_limit_order( +# target_symbol="BTC", +# amount=0.01, +# price=10000, +# order_side="buy" +# ) -# class Test(TestCase): -# -# def setUp(self) -> None: -# self.resource_dir = os.path.abspath( -# os.path.join( -# os.path.join( -# os.path.join( -# os.path.realpath(__file__), -# os.pardir -# ), -# os.pardir -# ), -# "resources" -# ) -# ) -# self.app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir}) -# self.app.add_portfolio_configuration( -# PortfolioConfiguration( -# market="BITVAVO", -# trading_symbol="USDT" -# ) -# ) -# self.app.add_strategy(SimpleTradingStrategy) +# # class Test(TestCase): +# # +# # def setUp(self) -> None: +# # self.resource_dir = os.path.abspath( +# # os.path.join( +# # os.path.join( +# # os.path.join( +# # os.path.realpath(__file__), +# # os.pardir +# # ), +# # os.pardir +# # ), +# # "resources" +# # ) +# # ) +# # self.app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir}) +# # self.app.add_portfolio_configuration( +# # PortfolioConfiguration( +# # market="BITVAVO", +# # trading_symbol="USDT" +# # ) +# # ) +# # self.app.add_strategy(SimpleTradingStrategy) diff --git a/tests/app/test_config.py b/tests/app/test_config.py index 9414e122..02e10ae7 100644 --- a/tests/app/test_config.py +++ b/tests/app/test_config.py @@ -1,34 +1,25 @@ -from unittest import TestCase +# from unittest import TestCase -from investing_algorithm_framework import create_app -from investing_algorithm_framework.domain import BACKTEST_DATA_DIRECTORY_NAME -from tests.resources import random_string +# from investing_algorithm_framework import create_app +# from investing_algorithm_framework.domain import RESOURCE_DIRECTORY +# from tests.resources import random_string -TEST_VALUE = random_string(10) +# TEST_VALUE = random_string(10) -class TestConfig(TestCase): - ATTRIBUTE_ONE = "ATTRIBUTE_ONE" +# class TestConfig(TestCase): +# ATTRIBUTE_ONE = "ATTRIBUTE_ONE" - def test_config(self): - app = create_app( - config={self.ATTRIBUTE_ONE: self.ATTRIBUTE_ONE} - ) - self.assertIsNotNone(app.config) +# def test_config(self): +# app = create_app( +# config={self.ATTRIBUTE_ONE: self.ATTRIBUTE_ONE} +# ) +# app.initialize_config() +# self.assertIsNotNone(app.config) +# self.assertIsNotNone(app.config[self.ATTRIBUTE_ONE]) - def test_get_item(self): - app = create_app( - config={self.ATTRIBUTE_ONE: self.ATTRIBUTE_ONE} - ) - self.assertIsNotNone(app.config) - self.assertIsNotNone(app.config.get(self.ATTRIBUTE_ONE)) - self.assertIsNotNone(app.config.get(BACKTEST_DATA_DIRECTORY_NAME)) - - def test_set_item(self): - app = create_app( - config={self.ATTRIBUTE_ONE: self.ATTRIBUTE_ONE} - ) - self.assertIsNotNone(app.config) - new_value = random_string(10) - app.config.set(self.ATTRIBUTE_ONE, new_value) - self.assertEqual(app.config.get(self.ATTRIBUTE_ONE), new_value) +# def test_resource_directory_exists(self): +# app = create_app() +# app.initialize_config() +# self.assertIsNotNone(app.config) +# self.assertIsNotNone(app.config[RESOURCE_DIRECTORY]) diff --git a/tests/app/test_start.py b/tests/app/test_start.py index 14a72305..5b5bba0e 100644 --- a/tests/app/test_start.py +++ b/tests/app/test_start.py @@ -1,94 +1,95 @@ -import os +# import os -from investing_algorithm_framework import create_app, TradingStrategy, \ - TimeUnit, RESOURCE_DIRECTORY, PortfolioConfiguration, Algorithm, \ - MarketCredential -from tests.resources import TestBase, MarketServiceStub +# from investing_algorithm_framework import create_app, TradingStrategy, \ +# TimeUnit, RESOURCE_DIRECTORY, PortfolioConfiguration, Algorithm, \ +# MarketCredential +# from tests.resources import TestBase, MarketServiceStub -class StrategyOne(TradingStrategy): - time_unit = TimeUnit.SECOND - interval = 2 +# class StrategyOne(TradingStrategy): +# time_unit = TimeUnit.SECOND +# interval = 2 - def apply_strategy( - self, - algorithm, - market_date=None, - **kwargs - ): - pass +# def apply_strategy( +# self, +# algorithm, +# market_date=None, +# **kwargs +# ): +# pass -class StrategyTwo(TradingStrategy): - time_unit = TimeUnit.SECOND - interval = 2 +# class StrategyTwo(TradingStrategy): +# time_unit = TimeUnit.SECOND +# interval = 2 - def apply_strategy( - self, - algorithm, - market_date=None, - **kwargs - ): - pass +# def apply_strategy( +# self, +# algorithm, +# market_date=None, +# **kwargs +# ): +# pass -class Test(TestBase): +# class Test(TestBase): - def setUp(self) -> None: - self.resource_dir = os.path.abspath( - os.path.join( - os.path.join( - os.path.join( - os.path.realpath(__file__), - os.pardir - ), - os.pardir - ), - "resources" - ) - ) - self.app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir}) - self.app.add_portfolio_configuration( - PortfolioConfiguration( - market="BITVAVO", - trading_symbol="USDT" - ) - ) - self.app.container.market_service.override(MarketServiceStub(None)) - algorithm = Algorithm() - algorithm.add_strategy(StrategyOne) - algorithm.add_strategy(StrategyTwo) - self.app.add_algorithm(algorithm) - self.app.add_market_credential( - MarketCredential( - market="BITVAVO", - api_key="api_key", - secret_key="secret_key" - ) - ) +# def setUp(self) -> None: +# self.resource_dir = os.path.abspath( +# os.path.join( +# os.path.join( +# os.path.join( +# os.path.realpath(__file__), +# os.pardir +# ), +# os.pardir +# ), +# "resources" +# ) +# ) +# self.app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir}) +# self.app.add_portfolio_configuration( +# PortfolioConfiguration( +# market="BITVAVO", +# trading_symbol="EUR" +# ) +# ) - def test_default(self): - self.app.run(number_of_iterations=2) - self.assertFalse(self.app.running) - strategy_orchestrator_service = self.app \ - .algorithm.strategy_orchestrator_service - self.assertTrue(strategy_orchestrator_service.has_run("StrategyOne")) - self.assertTrue(strategy_orchestrator_service.has_run("StrategyTwo")) +# self.app.container.market_service.override(MarketServiceStub(None)) +# algorithm = Algorithm() +# algorithm.add_strategy(StrategyOne) +# algorithm.add_strategy(StrategyTwo) +# self.app.add_algorithm(algorithm) +# self.app.add_market_credential( +# MarketCredential( +# market="BITVAVO", +# api_key="api_key", +# secret_key="secret_key" +# ) +# ) - def test_web(self): - self.app.run(number_of_iterations=2) - self.assertFalse(self.app.running) - strategy_orchestrator_service = self.app \ - .algorithm.strategy_orchestrator_service - self.assertTrue(strategy_orchestrator_service.has_run("StrategyOne")) - self.assertTrue(strategy_orchestrator_service.has_run("StrategyTwo")) +# def test_default(self): +# self.app.run(number_of_iterations=2) +# self.assertFalse(self.app.running) +# strategy_orchestrator_service = self.app \ +# .algorithm.strategy_orchestrator_service +# self.assertTrue(strategy_orchestrator_service.has_run("StrategyOne")) +# self.assertTrue(strategy_orchestrator_service.has_run("StrategyTwo")) - def test_stateless(self): - self.app.run( - number_of_iterations=2, - payload={"ACTION": "RUN_STRATEGY"}, - ) - strategy_orchestrator_service = self.app\ - .algorithm.strategy_orchestrator_service - self.assertTrue(strategy_orchestrator_service.has_run("StrategyOne")) - self.assertTrue(strategy_orchestrator_service.has_run("StrategyTwo")) +# def test_web(self): +# self.app.run(number_of_iterations=2) +# self.assertFalse(self.app.running) +# strategy_orchestrator_service = self.app \ +# .algorithm.strategy_orchestrator_service +# self.assertTrue(strategy_orchestrator_service.has_run("StrategyOne")) +# self.assertTrue(strategy_orchestrator_service.has_run("StrategyTwo")) + +# def test_stateless(self): +# self.app.run( +# number_of_iterations=2, +# payload={"ACTION": "RUN_STRATEGY"}, +# ) +# strategy_orchestrator_service = self.app\ +# .algorithm.strategy_orchestrator_service +# self.assertTrue(strategy_orchestrator_service.has_run("StrategyOne")) +# self.assertTrue(strategy_orchestrator_service.has_run("StrategyTwo")) diff --git a/tests/app/test_start_with_new_external_orders.py b/tests/app/test_start_with_new_external_orders.py new file mode 100644 index 00000000..69f98538 --- /dev/null +++ b/tests/app/test_start_with_new_external_orders.py @@ -0,0 +1,132 @@ +# from investing_algorithm_framework import Order, PortfolioConfiguration, \ +# MarketCredential +# from tests.resources import TestBase + + +# class Test(TestBase): +# initial_orders = [ +# Order.from_dict( +# { +# "id": "1323", +# "side": "buy", +# "symbol": "BTC/EUR", +# "amount": 10, +# "price": 10.0, +# "status": "CLOSED", +# "order_type": "limit", +# "order_side": "buy", +# "created_at": "2023-08-08T14:40:56.626362Z", +# "filled": 10, +# "remaining": 0, +# }, +# ), +# Order.from_dict( +# { +# "id": "14354", +# "side": "buy", +# "symbol": "DOT/EUR", +# "amount": 10, +# "price": 10.0, +# "status": "CLOSED", +# "order_type": "limit", +# "order_side": "buy", +# "created_at": "2023-09-22T14:40:56.626362Z", +# "filled": 10, +# "remaining": 0, +# }, +# ), +# Order.from_dict( +# { +# "id": "1323", +# "side": "buy", +# "symbol": "ETH/EUR", +# "amount": 10, +# "price": 10.0, +# "status": "OPEN", +# "order_type": "limit", +# "order_side": "buy", +# "created_at": "2023-08-08T14:40:56.626362Z", +# "filled": 0, +# "remaining": 0, +# }, +# ), +# ] +# external_orders = [ +# Order.from_dict( +# { +# "id": "1323", +# "side": "buy", +# "symbol": "ETH/EUR", +# "amount": 10, +# "price": 10.0, +# "status": "CLOSED", +# "order_type": "limit", +# "order_side": "buy", +# "created_at": "2023-08-08T14:40:56.626362Z", +# "filled": 10, +# "remaining": 0, +# }, +# ), +# # Order that is not tracked by the trading bot +# Order.from_dict( +# { +# "id": "133423", +# "side": "buy", +# "symbol": "KSM/EUR", +# "amount": 10, +# "price": 10.0, +# "status": "CLOSED", +# "order_type": "limit", +# "order_side": "buy", +# "created_at": "2023-08-08T14:40:56.626362Z", +# "filled": 10, +# "remaining": 0, +# }, +# ), +# ] +# external_balances = {"EUR": 1000} +# portfolio_configurations = [ +# PortfolioConfiguration( +# market="BITVAVO", +# trading_symbol="EUR" +# ) +# ] +# market_credentials = [ +# MarketCredential( +# market="bitvavo", +# api_key="api_key", +# secret_key="secret_key" +# ) +# ] + +# def test_start_with_new_external_positions(self): +# """ +# Test how the framework handles new external positions on an broker or +# exchange. + +# If the positions where not in the database, the algorithm should +# not include them, because they could be positions from another +# user or from another algorithm. +# """ +# self.assertTrue(self.app.algorithm.has_position("BTC")) +# btc_position = self.app.algorithm.get_position("BTC") +# self.assertEqual(10, btc_position.get_amount()) +# self.assertTrue(self.app.algorithm.has_position("DOT")) +# dot_position = self.app.algorithm.get_position("DOT") +# self.assertEqual(10, dot_position.get_amount()) +# # Eth position still has open order +# self.assertFalse(self.app.algorithm.has_position("ETH")) +# eth_position = self.app.algorithm.get_position("ETH") +# self.assertEqual(0, eth_position.get_amount()) +# self.assertFalse(self.app.algorithm.has_position("KSM")) +# self.app.run(number_of_iterations=1) +# self.assertTrue(self.app.algorithm.has_position("BTC")) +# btc_position = self.app.algorithm.get_position("BTC") +# self.assertEqual(10, btc_position.get_amount()) +# self.assertTrue(self.app.algorithm.has_position("DOT")) +# dot_position = self.app.algorithm.get_position("DOT") +# self.assertEqual(10, dot_position.get_amount()) +# self.assertTrue(self.app.algorithm.has_position("ETH")) +# eth_position = self.app.algorithm.get_position("ETH") +# self.assertEqual(10, eth_position.get_amount()) +# self.assertFalse(self.app.algorithm.has_position("KSM")) diff --git a/tests/app/test_start_with_new_external_positions.py b/tests/app/test_start_with_new_external_positions.py new file mode 100644 index 00000000..69f98538 --- /dev/null +++ b/tests/app/test_start_with_new_external_positions.py @@ -0,0 +1,132 @@ +# from investing_algorithm_framework import Order, PortfolioConfiguration, \ +# MarketCredential +# from tests.resources import TestBase + + +# class Test(TestBase): +# initial_orders = [ +# Order.from_dict( +# { +# "id": "1323", +# "side": "buy", +# "symbol": "BTC/EUR", +# "amount": 10, +# "price": 10.0, +# "status": "CLOSED", +# "order_type": "limit", +# "order_side": "buy", +# "created_at": "2023-08-08T14:40:56.626362Z", +# "filled": 10, +# "remaining": 0, +# }, +# ), +# Order.from_dict( +# { +# "id": "14354", +# "side": "buy", +# "symbol": "DOT/EUR", +# "amount": 10, +# "price": 10.0, +# "status": "CLOSED", +# "order_type": "limit", +# "order_side": "buy", +# "created_at": "2023-09-22T14:40:56.626362Z", +# "filled": 10, +# "remaining": 0, +# }, +# ), +# Order.from_dict( +# { +# "id": "1323", +# "side": "buy", +# "symbol": "ETH/EUR", +# "amount": 10, +# "price": 10.0, +# "status": "OPEN", +# "order_type": "limit", +# "order_side": "buy", +# "created_at": "2023-08-08T14:40:56.626362Z", +# "filled": 0, +# "remaining": 0, +# }, +# ), +# ] +# external_orders = [ +# Order.from_dict( +# { +# "id": "1323", +# "side": "buy", +# "symbol": "ETH/EUR", +# "amount": 10, +# "price": 10.0, +# "status": "CLOSED", +# "order_type": "limit", +# "order_side": "buy", +# "created_at": "2023-08-08T14:40:56.626362Z", +# "filled": 10, +# "remaining": 0, +# }, +# ), +# # Order that is not tracked by the trading bot +# Order.from_dict( +# { +# "id": "133423", +# "side": "buy", +# "symbol": "KSM/EUR", +# "amount": 10, +# "price": 10.0, +# "status": "CLOSED", +# "order_type": "limit", +# "order_side": "buy", +# "created_at": "2023-08-08T14:40:56.626362Z", +# "filled": 10, +# "remaining": 0, +# }, +# ), +# ] +# external_balances = {"EUR": 1000} +# portfolio_configurations = [ +# PortfolioConfiguration( +# market="BITVAVO", +# trading_symbol="EUR" +# ) +# ] +# market_credentials = [ +# MarketCredential( +# market="bitvavo", +# api_key="api_key", +# secret_key="secret_key" +# ) +# ] + +# def test_start_with_new_external_positions(self): +# """ +# Test how the framework handles new external positions on an broker or +# exchange. + +# If the positions where not in the database, the algorithm should +# not include them, because they could be positions from another +# user or from another algorithm. +# """ +# self.assertTrue(self.app.algorithm.has_position("BTC")) +# btc_position = self.app.algorithm.get_position("BTC") +# self.assertEqual(10, btc_position.get_amount()) +# self.assertTrue(self.app.algorithm.has_position("DOT")) +# dot_position = self.app.algorithm.get_position("DOT") +# self.assertEqual(10, dot_position.get_amount()) +# # Eth position still has open order +# self.assertFalse(self.app.algorithm.has_position("ETH")) +# eth_position = self.app.algorithm.get_position("ETH") +# self.assertEqual(0, eth_position.get_amount()) +# self.assertFalse(self.app.algorithm.has_position("KSM")) +# self.app.run(number_of_iterations=1) +# self.assertTrue(self.app.algorithm.has_position("BTC")) +# btc_position = self.app.algorithm.get_position("BTC") +# self.assertEqual(10, btc_position.get_amount()) +# self.assertTrue(self.app.algorithm.has_position("DOT")) +# dot_position = self.app.algorithm.get_position("DOT") +# self.assertEqual(10, dot_position.get_amount()) +# self.assertTrue(self.app.algorithm.has_position("ETH")) +# eth_position = self.app.algorithm.get_position("ETH") +# self.assertEqual(10, eth_position.get_amount()) +# self.assertFalse(self.app.algorithm.has_position("KSM")) diff --git a/tests/app/web/controllers/position_controller/test_list_positions.py b/tests/app/web/controllers/position_controller/test_list_positions.py index f507524c..c601d699 100644 --- a/tests/app/web/controllers/position_controller/test_list_positions.py +++ b/tests/app/web/controllers/position_controller/test_list_positions.py @@ -23,9 +23,6 @@ class Test(FlaskTestBase): "EUR": 1000 } - def setUp(self) -> None: - super(Test, self).setUp() - def test_list_portfolios(self): self.iaf_app.algorithm.create_limit_order( amount=10, diff --git a/tests/app/web/schemas/test_order_schema.py b/tests/app/web/schemas/test_order_schema.py deleted file mode 100644 index efdd6ec8..00000000 --- a/tests/app/web/schemas/test_order_schema.py +++ /dev/null @@ -1,25 +0,0 @@ -# from investing_algorithm_framework import SQLLiteOrder -# from investing_algorithm_framework.schemas import OrderSerializer -# from tests.resources import TestBase, TestOrderAndPositionsObjectsMixin -# from tests.resources.serialization_dicts import order_serialization_dict -# -# -# class Test(TestBase, TestOrderAndPositionsObjectsMixin): -# -# def setUp(self): -# super(Test, self).setUp() -# -# self.start_algorithm() -# self.create_buy_order( -# 10, -# self.TARGET_SYMBOL_A, -# self.BASE_SYMBOL_A_PRICE, -# self.algo_app.algorithm.get_portfolio_manager(), -# 10 -# ) -# -# def test(self): -# order = SQLLiteOrder.query.first() -# serializer = OrderSerializer() -# data = serializer.dump(order) -# self.assertEqual(set(data), order_serialization_dict) diff --git a/tests/domain/test_load_backtest_reports.py b/tests/domain/test_load_backtest_reports.py index 7cd7d099..184fe85c 100644 --- a/tests/domain/test_load_backtest_reports.py +++ b/tests/domain/test_load_backtest_reports.py @@ -23,4 +23,3 @@ def setUp(self) -> None: def test_backtest_reports_evaluation(self): path = os.path.join(self.resource_dir, "backtest_reports_for_testing") reports = load_backtest_reports(path) - print(len(reports)) diff --git a/tests/infrastructure/models/test_order.py b/tests/infrastructure/models/test_order.py index 4a4c8835..872eb3d6 100644 --- a/tests/infrastructure/models/test_order.py +++ b/tests/infrastructure/models/test_order.py @@ -1,49 +1,26 @@ -import os - -from investing_algorithm_framework import create_app, RESOURCE_DIRECTORY, \ - PortfolioConfiguration, Algorithm, MarketCredential +from investing_algorithm_framework import \ + PortfolioConfiguration, MarketCredential from investing_algorithm_framework.infrastructure.models import SQLOrder -from tests.resources import TestBase, MarketServiceStub +from tests.resources import TestBase class Test(TestBase): - - def setUp(self) -> None: - self.resource_dir = os.path.abspath( - os.path.join( - os.path.join( - os.path.join( - os.path.join( - os.path.realpath(__file__), - os.pardir - ), - os.pardir - ), - os.pardir - ), - "resources" - ) - ) - self.app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir}) - self.app.add_portfolio_configuration( - PortfolioConfiguration( - market="binance", - trading_symbol="USDT" - ) - ) - self.app.container.market_service.override( - MarketServiceStub(self.app.container.market_credential_service()) + portfolio_configurations = [ + PortfolioConfiguration( + market="binance", + trading_symbol="USDT" ) - algorithm = Algorithm() - self.app.add_algorithm(algorithm) - self.app.add_market_credential( - MarketCredential( - market="binance", - api_key="api_key", - secret_key="secret_key", - ) + ] + external_balances = { + "USDT": 1000 + } + market_credentials = [ + MarketCredential( + market="binance", + api_key="api_key", + secret_key="secret_key" ) - self.app.initialize() + ] def test_creation(self): order = SQLOrder( diff --git a/tests/infrastructure/models/test_position.py b/tests/infrastructure/models/test_position.py index bdb344f1..410fe324 100644 --- a/tests/infrastructure/models/test_position.py +++ b/tests/infrastructure/models/test_position.py @@ -6,43 +6,62 @@ class Test(TestBase): - - def setUp(self) -> None: - self.resource_dir = os.path.abspath( - os.path.join( - os.path.join( - os.path.join( - os.path.join( - os.path.realpath(__file__), - os.pardir - ), - os.pardir - ), - os.pardir - ), - "resources" - ) - ) - self.app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir}) - self.app.add_portfolio_configuration( - PortfolioConfiguration( - market="BITVAVO", - trading_symbol="USDT" - ) - ) - self.app.container.market_service.override( - MarketServiceStub(self.app.container.market_credential_service()) + portfolio_configurations = [ + PortfolioConfiguration( + market="BITVAVO", + trading_symbol="USDT" ) - algorithm = Algorithm() - self.app.add_algorithm(algorithm) - self.app.add_market_credential( - MarketCredential( - market="BITVAVO", - api_key="api_key", - secret_key="secret_key" - ) + ] + external_balances = { + "USDT": 1000 + } + market_credentials = [ + MarketCredential( + market="BITVAVO", + api_key="", + secret_key="" ) - self.app.initialize() + ] + + # def setUp(self) -> None: + # self.resource_dir = os.path.abspath( + # os.path.join( + # os.path.join( + # os.path.join( + # os.path.join( + # os.path.realpath(__file__), + # os.pardir + # ), + # os.pardir + # ), + # os.pardir + # ), + # "resources" + # ) + # ) + # self.app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir}) + # self.app.add_portfolio_configuration( + # PortfolioConfiguration( + # market="BITVAVO", + # trading_symbol="USDT" + # ) + # ) + # self.app.container.market_service.override( + # MarketServiceStub(self.app.container.market_credential_service()) + # ) + # algorithm = Algorithm() + # self.app.add_algorithm(algorithm) + # self.app.add_market_credential( + # MarketCredential( + # market="BITVAVO", + # api_key="api_key", + # secret_key="secret_key" + # ) + # ) + # self.app.initialize() + + # def tearDown(self): + # return super().tearDown() def test_store_position_amount(self): self.portfolio_service = self.app.container.portfolio_service() diff --git a/tests/resources/test_base.py b/tests/resources/test_base.py index d6c80d8e..cdfaf627 100644 --- a/tests/resources/test_base.py +++ b/tests/resources/test_base.py @@ -1,12 +1,14 @@ import logging import os +from decimal import Decimal from unittest import TestCase from flask_testing import TestCase as FlaskTestCase from investing_algorithm_framework import create_app, Algorithm, App, \ - TradingStrategy, TimeUnit -from investing_algorithm_framework.domain import RESOURCE_DIRECTORY + TradingStrategy, TimeUnit, OrderStatus +from investing_algorithm_framework.domain import RESOURCE_DIRECTORY, \ + ENVIRONMENT, Environment from investing_algorithm_framework.infrastructure.database import Session from tests.resources.stubs import MarketServiceStub @@ -33,7 +35,7 @@ class TestBase(TestCase): algorithm = Algorithm() external_balances = {} external_orders = [] - external_available_symbols = [] + initial_orders = [] market_credentials = [] market_service = MarketServiceStub(None) market_data_source_service = None @@ -45,8 +47,8 @@ def setUp(self) -> None: self.resource_directory = os.path.dirname(__file__) config = self.config config[RESOURCE_DIRECTORY] = self.resource_directory + config[ENVIRONMENT] = Environment.TEST.value self.app: App = create_app(config=config) - self.market_service.symbols = self.external_available_symbols self.market_service.balances = self.external_balances self.market_service.orders = self.external_orders self.app.container.market_service.override(self.market_service) @@ -71,32 +73,66 @@ def setUp(self) -> None: if self.initialize: self.app.initialize() + if self.initial_orders is not None: + for order in self.initial_orders: + created_order = self.app.algorithm.create_order( + target_symbol=order.get_target_symbol(), + amount=order.get_amount(), + price=order.get_price(), + order_side=order.get_order_side(), + order_type=order.get_order_type() + ) + + # Update the order to the correct status + order_service = self.app.container.order_service() + + if OrderStatus.CLOSED.value == order.get_status(): + order_service.update( + created_order.get_id(), + { + "status": "CLOSED", + "filled": order.get_filled(), + "remaining": Decimal('0'), + } + ) + def tearDown(self) -> None: - database_path = os.path.join( - self.resource_directory, "databases/prod-database.sqlite3" + database_dir = os.path.join( + self.resource_directory, "databases" ) - if os.path.exists(database_path): - session = Session() - session.commit() - session.close() - - try: - os.remove(database_path) - except Exception as e: - logger.error(e) + if os.path.exists(database_dir): + for root, dirs, files in os.walk(database_dir, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) def remove_database(self): - try: - database_path = os.path.join( - self.resource_directory, "databases/prod-database.sqlite3" - ) + database_dir = os.path.join( + self.resource_directory, "databases" + ) + + if os.path.exists(database_dir): + for root, dirs, files in os.walk(database_dir, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + + @classmethod + def tearDownClass(cls) -> None: + database_dir = os.path.join( + cls.resource_directory, "databases" + ) - if os.path.exists(database_path): - os.remove(database_path) - except Exception as e: - logger.error(e) + if os.path.exists(database_dir): + for root, dirs, files in os.walk(database_dir, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) class FlaskTestBase(FlaskTestCase): @@ -106,17 +142,17 @@ class FlaskTestBase(FlaskTestCase): config = {} algorithm = Algorithm() external_balances = {} + initial_orders = [] external_orders = [] - external_available_symbols = [] market_service = MarketServiceStub(None) initialize = True + resource_directory = os.path.dirname(__file__) def create_app(self): self.resource_directory = os.path.dirname(__file__) self.iaf_app: App = create_app( {RESOURCE_DIRECTORY: self.resource_directory}, web=True ) - self.market_service.symbols = self.external_available_symbols self.market_service.balances = self.external_balances self.market_service.orders = self.external_orders self.iaf_app.container.market_service.override(self.market_service) @@ -138,19 +174,52 @@ def create_app(self): self.iaf_app.initialize() + if self.initial_orders is not None: + for order in self.initial_orders: + created_order = self.app.algorithm.create_order( + target_symbol=order.get_target_symbol(), + amount=order.get_amount(), + price=order.get_price(), + order_side=order.get_order_side(), + order_type=order.get_order_type() + ) + + # Update the order to the correct status + order_service = self.app.container.order_service() + + if OrderStatus.CLOSED.value == order.get_status(): + order_service.update( + created_order.get_id(), + { + "status": "CLOSED", + "filled": order.get_filled(), + "remaining": Decimal('0'), + } + ) + return self.iaf_app._flask_app def tearDown(self) -> None: - database_path = os.path.join( - os.path.dirname(__file__), "databases/prod-database.sqlite3" + database_dir = os.path.join( + self.resource_directory, "databases" ) - if os.path.exists(database_path): - session = Session() - session.commit() - session.close() + if os.path.exists(database_dir): + for root, dirs, files in os.walk(database_dir, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) + + @classmethod + def tearDownClass(cls) -> None: + database_dir = os.path.join( + cls.resource_directory, "databases" + ) - try: - os.remove(database_path) - except Exception as e: - logger.error(e) + if os.path.exists(database_dir): + for root, dirs, files in os.walk(database_dir, topdown=False): + for name in files: + os.remove(os.path.join(root, name)) + for name in dirs: + os.rmdir(os.path.join(root, name)) diff --git a/tests/services/test_market_data_source_service.py b/tests/services/test_market_data_source_service.py index 7c72dfee..52cfca95 100644 --- a/tests/services/test_market_data_source_service.py +++ b/tests/services/test_market_data_source_service.py @@ -1,56 +1,42 @@ import os -from investing_algorithm_framework import create_app, RESOURCE_DIRECTORY, \ - PortfolioConfiguration, CSVTickerMarketDataSource, MarketCredential, \ - Algorithm -from tests.resources import TestBase, MarketServiceStub +from investing_algorithm_framework import RESOURCE_DIRECTORY, \ + PortfolioConfiguration, CSVTickerMarketDataSource, MarketCredential +from tests.resources import TestBase class TestMarketDataSourceService(TestBase): - - def setUp(self) -> None: - self.resource_dir = os.path.abspath( - os.path.join( - os.path.join( - os.path.join( - os.path.realpath(__file__), - os.pardir - ), - os.pardir - ), - "resources" - ) - ) - self.app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir}) - self.app.add_portfolio_configuration( - PortfolioConfiguration( - market="binance", - trading_symbol="USDT" - ) + portfolio_configurations = [ + PortfolioConfiguration( + market="binance", + trading_symbol="USDT" ) - self.app.container.market_service.override( - MarketServiceStub(self.app.container.market_credential_service()) + ] + market_credentials = [ + MarketCredential( + market="binance", + api_key="api_key", + secret_key="secret_key" ) + ] + external_balances = { + "USDT": 1000 + } + + def setUp(self) -> None: + super(TestMarketDataSourceService, self).setUp() + configuration_service = self.app.container.configuration_service() + config = configuration_service.get_config() self.app.add_market_data_source(CSVTickerMarketDataSource( identifier="BTC/EUR-ticker", market="BITVAVO", symbol="BTC/EUR", csv_file_path=os.path.join( - self.resource_dir, + config[RESOURCE_DIRECTORY], "market_data_sources", "TICKER_BTC-EUR_BINANCE_2023-08-23:22:00_2023-12-02:00:00.csv" ) )) - algorithm = Algorithm() - self.app.add_algorithm(algorithm) - self.app.add_market_credential( - MarketCredential( - market="binance", - api_key="api_key", - secret_key="secret_key", - ) - ) - self.app.initialize() def test_get_ticker_market_data_source(self): market_data_source_service = self.app.container\ diff --git a/tests/services/test_order_backtest_service.py b/tests/services/test_order_backtest_service.py index 50c58db3..c8bc5d53 100644 --- a/tests/services/test_order_backtest_service.py +++ b/tests/services/test_order_backtest_service.py @@ -77,8 +77,12 @@ def setUp(self) -> None: def test_create_limit_order(self): order_service = self.app.container.order_service() - config = self.app.config - config[BACKTESTING_INDEX_DATETIME] = datetime.utcnow() + configuration_service = self.app.container.configuration_service() + configuration_service.add_value( + BACKTESTING_INDEX_DATETIME, + datetime.utcnow() + ) + order = order_service.create( { "target_symbol": "ADA", @@ -104,8 +108,12 @@ def test_create_limit_order(self): def test_update_order(self): order_service = self.app.container.order_service() - config = self.app.config - config[BACKTESTING_INDEX_DATETIME] = datetime.utcnow() + configuration_service = self.app.container.configuration_service() + configuration_service.add_value( + BACKTESTING_INDEX_DATETIME, + datetime.utcnow() + ) + order = order_service.create( { "target_symbol": "ADA", @@ -136,8 +144,11 @@ def test_update_order(self): def test_create_limit_buy_order(self): order_service = self.app.container.order_service() - config = self.app.config - config[BACKTESTING_INDEX_DATETIME] = datetime.utcnow() + configuration_service = self.app.container.configuration_service() + configuration_service.add_value( + BACKTESTING_INDEX_DATETIME, + datetime.utcnow() + ) order = order_service.create( { "target_symbol": "ADA", @@ -163,9 +174,11 @@ def test_create_limit_buy_order(self): def test_create_limit_sell_order(self): order_service = self.app.container.order_service() - config = self.app.config - config[BACKTESTING_INDEX_DATETIME] = datetime.utcnow() - + configuration_service = self.app.container.configuration_service() + configuration_service.add_value( + BACKTESTING_INDEX_DATETIME, + datetime.utcnow() + ) order = order_service.create( { "target_symbol": "ADA", @@ -232,8 +245,11 @@ def test_update_sell_order_with_successful_order(self): def test_update_closing_partial_buy_orders(self): order_service = self.app.container.order_service() - config = self.app.config - config[BACKTESTING_INDEX_DATETIME] = datetime.utcnow() + configuration_service = self.app.container.configuration_service() + configuration_service.add_value( + BACKTESTING_INDEX_DATETIME, + datetime.utcnow() + ) buy_order_one = order_service.create( { "target_symbol": "ADA", @@ -356,8 +372,11 @@ def test_update_sell_order_with_cancelled_order(self): def test_trade_closing_winning_trade(self): order_service = self.app.container.order_service() - config = self.app.config - config[BACKTESTING_INDEX_DATETIME] = datetime.utcnow() + configuration_service = self.app.container.configuration_service() + configuration_service.add_value( + BACKTESTING_INDEX_DATETIME, + datetime.utcnow() + ) buy_order = order_service.create( { "target_symbol": "ADA", @@ -416,8 +435,11 @@ def test_trade_closing_winning_trade(self): def test_trade_closing_losing_trade(self): order_service = self.app.container.order_service() - config = self.app.config - config[BACKTESTING_INDEX_DATETIME] = datetime.utcnow() + configuration_service = self.app.container.configuration_service() + configuration_service.add_value( + BACKTESTING_INDEX_DATETIME, + datetime.utcnow() + ) buy_order = order_service.create( { "target_symbol": "ADA", @@ -476,8 +498,11 @@ def test_trade_closing_losing_trade(self): def test_has_executed_buy_order(self): order_service = self.app.container.order_service() - config = self.app.config - config[BACKTESTING_INDEX_DATETIME] = datetime.utcnow() + configuration_service = self.app.container.configuration_service() + configuration_service.add_value( + BACKTESTING_INDEX_DATETIME, + datetime.utcnow() + ) # Create the buy order order = order_service.create( @@ -604,8 +629,11 @@ def test_has_executed_buy_order(self): def test_has_executed_sell_order(self): order_service = self.app.container.order_service() - config = self.app.config - config[BACKTESTING_INDEX_DATETIME] = datetime.utcnow() + configuration_service = self.app.container.configuration_service() + configuration_service.add_value( + BACKTESTING_INDEX_DATETIME, + datetime.utcnow() + ) # Create the buy order order = order_service.create( diff --git a/tests/services/test_portfolio_sync_service.py b/tests/services/test_portfolio_sync_service.py index 8f7b3b81..1ba3a387 100644 --- a/tests/services/test_portfolio_sync_service.py +++ b/tests/services/test_portfolio_sync_service.py @@ -75,9 +75,16 @@ def test_sync_unallocated_with_no_balance(self): self.market_service.balances = {"EUR": 0} self.app.add_algorithm(Algorithm()) - with self.assertRaises(OperationalException): + with self.assertRaises(OperationalException) as context: self.app.initialize() + self.assertEqual( + "The initial balance of the portfolio configuration is more than" " the available balance on the exchange. Please make sure" + " that the initial balance of the portfolio configuration" + " is less than the available balance on the exchange.", + str(context.exception) + ) + def test_sync_unallocated_with_reserved(self): configuration_service = self.app.container.configuration_service() configuration_service.config[RESERVED_BALANCES] = {"EUR": 500} @@ -104,39 +111,6 @@ def test_sync_unallocated_with_reserved(self): .find({"identifier": "test"}) self.assertEqual(500, portfolio.unallocated) - def test_sync_unallocated_with_reserved_error(self): - """ - Test the sync_unallocated method of the PortfolioSyncService class. - - 1. Create a PortfolioSyncService object. - 2. Create a portfolio configuration with 500 unallocated balance. - 3. Set the reserved balance to 500. - 3. Set the market balance to 700 - 4. Check if sync method raises an OperationalException. - """ - configuration_service = self.app.container.configuration_service() - configuration_service.config[RESERVED_BALANCES] = {"EUR": 500} - self.app.add_portfolio_configuration( - PortfolioConfiguration( - identifier="test", - market="binance", - trading_symbol="EUR", - initial_balance=500 - ) - ) - self.app.add_market_credential( - MarketCredential( - market="binance", - api_key="test", - secret_key="test" - ) - ) - self.market_service.balances = {"EUR": 700} - self.app.add_algorithm(Algorithm()) - - with self.assertRaises(OperationalException): - self.app.initialize() - def test_sync_unallocated_with_initial_size(self): self.app.add_portfolio_configuration( PortfolioConfiguration( @@ -160,686 +134,3 @@ def test_sync_unallocated_with_initial_size(self): portfolio = self.app.container.portfolio_service() \ .find({"identifier": "test"}) self.assertEqual(500, portfolio.unallocated) - - def test_sync_unallocated_with_stateless(self): - """ - Test to sync the unallocated amount with initial load set to false. - This means that if the available balance is less than the - initial balance of the portfolio configuration, the - unallocated balance should be set to the available balance. It - should not raise an OperationalException. - """ - self.app.add_portfolio_configuration( - PortfolioConfiguration( - identifier="test", - market="binance", - trading_symbol="EUR", - initial_balance=500 - ) - ) - self.app.add_market_credential( - MarketCredential( - market="binance", - api_key="test", - secret_key="test" - ) - ) - self.market_service.balances = {"EUR": 1200} - self.app.add_algorithm(Algorithm()) - configuration_service = self.app.container.configuration_service() - configuration_service.config[APP_MODE] = AppMode.STATELESS.value - self.app.config[APP_MODE] = AppMode.STATELESS.value - self.app.initialize() - - configuration_service = self.app.container.configuration_service() - configuration_service.config[APP_MODE] = "STATELESS" - portfolio = self.app.container.portfolio_service() \ - .find({"identifier": "test"}) - self.assertEqual(1200, portfolio.unallocated) - - def test_sync_positions(self): - self.app.add_portfolio_configuration( - PortfolioConfiguration( - identifier="test", - market="binance", - trading_symbol="EUR", - initial_balance=500 - ) - ) - self.app.add_market_credential( - MarketCredential( - market="binance", - api_key="test", - secret_key="test" - ) - ) - self.market_service.balances = { - "EUR": 1200, - "BTC": 0.5, - "ETH": 199, - "ADA": 4023, - "XRP": 10, - } - self.app.add_algorithm(Algorithm()) - self.app.initialize() - configuration_service = self.app.container.configuration_service() - configuration_service.config[APP_MODE] = "STATELESS" - portfolio = self.app.container.portfolio_service() \ - .find({"identifier": "test"}) - self.assertEqual(500, portfolio.unallocated) - btc_position = self.app.container.position_service().find( - {"symbol": "BTC", "portfolio_id": portfolio.id} - ) - self.assertEqual(0.5, btc_position.amount) - eth_position = self.app.container.position_service().find( - {"symbol": "ETH", "portfolio_id": portfolio.id} - ) - self.assertEqual(199, eth_position.amount) - ada_position = self.app.container.position_service().find( - {"symbol": "ADA", "portfolio_id": portfolio.id} - ) - self.assertEqual(4023, ada_position.amount) - xrp_position = self.app.container.position_service().find( - {"symbol": "XRP", "portfolio_id": portfolio.id} - ) - self.assertEqual(10, xrp_position.amount) - - def test_sync_positions_with_reserved(self): - self.app.add_portfolio_configuration( - PortfolioConfiguration( - identifier="test", - market="binance", - trading_symbol="EUR", - initial_balance=500 - ) - ) - self.app.add_market_credential( - MarketCredential( - market="binance", - api_key="test", - secret_key="test" - ) - ) - self.market_service.balances = { - "EUR": 1200, - "BTC": 0.5, - "ETH": 199, - "ADA": 4023, - "XRP": 10, - } - self.app.add_algorithm(Algorithm()) - configuration_service = self.app.container.configuration_service() - configuration_service.config[APP_MODE] = AppMode.DEFAULT.value - configuration_service.config[RESERVED_BALANCES] = { - "BTC": 0.5, - "ETH": 1, - "ADA": 1000, - "XRP": 5 - } - self.app.initialize() - - portfolio = self.app.container.portfolio_service() \ - .find({"identifier": "test"}) - self.assertEqual(500, portfolio.unallocated) - btc_position = self.app.container.position_service().find( - {"symbol": "BTC", "portfolio_id": portfolio.id} - ) - self.assertEqual(0, btc_position.amount) - eth_position = self.app.container.position_service().find( - {"symbol": "ETH", "portfolio_id": portfolio.id} - ) - self.assertEqual(198, eth_position.amount) - ada_position = self.app.container.position_service().find( - {"symbol": "ADA", "portfolio_id": portfolio.id} - ) - self.assertEqual(3023, ada_position.amount) - xrp_position = self.app.container.position_service().find( - {"symbol": "XRP", "portfolio_id": portfolio.id} - ) - self.assertEqual(5, xrp_position.amount) - - self.market_service.balances = { - "BTC": 0.6, - "ETH": 200, - "ADA": 1000, - "XRP": 5 - } - portfolio_sync_service = self.app.container.portfolio_sync_service() - portfolio_sync_service.sync_positions(portfolio) - btc_position = self.app.container.position_service().find( - {"symbol": "BTC", "portfolio_id": portfolio.id} - ) - self.assertAlmostEqual(0.1, btc_position.amount) - eth_position = self.app.container.position_service().find( - {"symbol": "ETH", "portfolio_id": portfolio.id} - ) - self.assertEqual(199, eth_position.amount) - ada_position = self.app.container.position_service().find( - {"symbol": "ADA", "portfolio_id": portfolio.id} - ) - self.assertEqual(0, ada_position.amount) - xrp_position = self.app.container.position_service().find( - {"symbol": "XRP", "portfolio_id": portfolio.id} - ) - self.assertEqual(0, xrp_position.amount) - - def test_sync_orders(self): - """ - Test the sync_orders method of the PortfolioSyncService class. - - 1. Create a PortfolioSyncService object. - 2. Create a portfolio with 1000 unallocated balance. - 3. 4 orders are synced to the portfolio. - 4. Check if the portfolio still has the 1000eu - """ - configuration_service = self.app.container.configuration_service() - configuration_service.config[SYMBOLS] = ["KSM/EUR"] - self.market_service.symbols = ["KSM/EUR"] - self.app.add_portfolio_configuration( - PortfolioConfiguration( - identifier="test", - market="binance", - trading_symbol="EUR", - initial_balance=500 - ) - ) - self.app.add_market_credential( - MarketCredential( - market="binance", - api_key="test", - secret_key="test" - ) - ) - self.market_service.balances = { - "EUR": 1200, - "BTC": 0.5, - "ETH": 199, - "ADA": 4023, - "XRP": 10, - } - self.market_service.orders = [ - Order.from_ccxt_order( - { - "id": "12333535", - "symbol": "BTC/EUR", - "amount": 0.5, - "price": 50000, - "side": "buy", - "status": "open", - "type": "limit", - "datetime": "2021-10-10T10:10:10" - }, - ), - Order.from_ccxt_order( - { - "id": "1233353", - "symbol": "ETH/EUR", - "amount": 199, - "price": 2000, - "side": "buy", - "status": "open", - "type": "limit", - "datetime": "2021-10-10T10:10:10" - }, - ), - Order.from_ccxt_order( - { - "id": "1233", - "symbol": "ADA/EUR", - "amount": 4023, - "price": 1, - "side": "buy", - "status": "closed", - "type": "limit", - "datetime": "2021-10-10T10:10:10" - }, - ), - Order.from_ccxt_order( - { - "id": "123", - "symbol": "XRP/EUR", - "amount": 10, - "price": 1, - "side": "buy", - "type": "limit", - "status": "closed", - "datetime": "2021-10-10T10:10:10" - } - ) - ] - - self.app.add_algorithm(Algorithm()) - self.app.initialize() - portfolio = self.app.container.portfolio_service() \ - .find({"identifier": "test"}) - order_service = self.app.container.order_service() - position_service = self.app.container.position_service() - btc_position = position_service.find( - {"symbol": "BTC", "portfolio_id": portfolio.id} - ) - self.assertEqual(1, order_service.count({"position": btc_position.id})) - eth_position = self.app.container.position_service().find( - {"symbol": "ETH", "portfolio_id": portfolio.id} - ) - self.assertEqual(1, order_service.count({"position": eth_position.id})) - orders = order_service.get_all( - {"position": eth_position.id} - ) - order = orders[0] - self.assertEqual(199, order.amount) - self.assertEqual(2000, order.price) - self.assertEqual("BUY", order.order_side) - self.assertEqual("LIMIT", order.order_type) - self.assertEqual("OPEN", order.status) - - ada_position = self.app.container.position_service().find( - {"symbol": "ADA", "portfolio_id": portfolio.id} - ) - self.assertEqual(1, order_service.count({"position": ada_position.id})) - orders = order_service.get_all( - {"position": ada_position.id} - ) - order = orders[0] - self.assertEqual(4023, order.amount) - self.assertEqual(1, order.price) - self.assertEqual("BUY", order.order_side) - self.assertEqual("LIMIT", order.order_type) - self.assertEqual("CLOSED", order.status) - - xrp_position = self.app.container.position_service().find( - {"symbol": "XRP", "portfolio_id": portfolio.id} - ) - self.assertEqual(1, order_service.count({"position": xrp_position.id})) - orders = order_service.get_all( - {"position": xrp_position.id} - ) - order = orders[0] - self.assertEqual(10, order.amount) - self.assertEqual(1, order.price) - self.assertEqual("BUY", order.order_side) - self.assertEqual("LIMIT", order.order_type) - self.assertEqual("CLOSED", order.status) - - ksm_position = self.app.container.position_service().find( - {"symbol": "KSM", "portfolio_id": portfolio.id} - ) - self.assertIsNotNone(ksm_position) - self.assertEqual(0, order_service.count({"position": ksm_position.id})) - - def test_sync_orders_with_track_from_attribute_set(self): - """ - Test the sync_orders method of the PortfolioSyncService class with - the track_from attribute set. - - 1. Create a PortfolioSyncService object. - 2. Create a portfolio with 1000 unallocated balance. - 3. 4 orders are synced to the portfolio. - 4. Check if the portfolio still has the 1000eu - """ - configuration_service = self.app.container.configuration_service() - configuration_service.config[SYMBOLS] = ["KSM/EUR"] - self.market_service.symbols = ["KSM/EUR"] - self.app.add_portfolio_configuration( - PortfolioConfiguration( - identifier="test", - market="binance", - trading_symbol="EUR", - initial_balance=500, - track_from="2021-10-10T10:10:10" - ) - ) - self.app.add_market_credential( - MarketCredential( - market="binance", - api_key="test", - secret_key="test" - ) - ) - self.market_service.balances = { - "EUR": 1200, - "BTC": 0.5, - "ETH": 199, - "ADA": 4023, - "XRP": 10, - } - self.market_service.orders = [ - Order.from_ccxt_order( - { - "id": "12333535", - "symbol": "BTC/EUR", - "amount": 0.5, - "price": 50000, - "side": "buy", - "status": "open", - "type": "limit", - "datetime": "2021-09-10T10:10:10" - }, - ), - Order.from_ccxt_order( - { - "id": "1233353", - "symbol": "ETH/EUR", - "amount": 199, - "price": 2000, - "side": "buy", - "status": "open", - "type": "limit", - "datetime": "2021-09-10T10:10:10" - }, - ), - Order.from_ccxt_order( - { - "id": "1233", - "symbol": "ADA/EUR", - "amount": 4023, - "price": 1, - "side": "buy", - "status": "open", - "type": "limit", - "datetime": "2021-10-10T10:10:10" - }, - ), - Order.from_ccxt_order( - { - "id": "123", - "symbol": "XRP/EUR", - "amount": 10, - "price": 1, - "side": "buy", - "type": "limit", - "status": "open", - "datetime": "2021-10-10T10:10:10" - } - ) - ] - - self.app.add_algorithm(Algorithm()) - self.app.initialize() - portfolio = self.app.container.portfolio_service() \ - .find({"identifier": "test"}) - self.assertEqual(500, portfolio.unallocated) - btc_position = self.app.container.position_service().find( - {"symbol": "BTC", "portfolio_id": portfolio.id} - ) - order_service = self.app.container.order_service() - self.assertEqual(0, order_service.count({"position": btc_position.id})) - - eth_position = self.app.container.position_service().find( - {"symbol": "ETH", "portfolio_id": portfolio.id} - ) - self.assertEqual(0, order_service.count({"position": eth_position.id})) - - ada_position = self.app.container.position_service().find( - {"symbol": "ADA", "portfolio_id": portfolio.id} - ) - self.assertEqual(1, order_service.count({"position": ada_position.id})) - - xrp_position = self.app.container.position_service().find( - {"symbol": "XRP", "portfolio_id": portfolio.id} - ) - self.assertEqual(1, order_service.count({"position": xrp_position.id})) - - ksm_position = self.app.container.position_service().find( - {"symbol": "KSM", "portfolio_id": portfolio.id} - ) - self.assertEqual(0, order_service.count({"position": ksm_position.id})) - - def test_sync_trades(self): - configuration_service = self.app.container.configuration_service() - configuration_service.config[SYMBOLS] = ["KSM/EUR"] - self.market_service.symbols = ["KSM/EUR"] - self.app.add_portfolio_configuration( - PortfolioConfiguration( - identifier="test", - market="binance", - trading_symbol="EUR", - initial_balance=500, - track_from="2021-10-10T10:10:10" - ) - ) - self.app.add_market_credential( - MarketCredential( - market="binance", - api_key="test", - secret_key="test" - ) - ) - self.market_service.balances = { - "EUR": 1200, - "BTC": 0.5, - "ETH": 199, - "ADA": 4023, - "XRP": 10, - } - self.market_service.orders = [ - Order.from_ccxt_order( - { - "id": "12333535", - "symbol": "BTC/EUR", - "amount": 0.5, - "filled": 0.3, - "remaining": 0.2, - "price": 50000, - "side": "buy", - "status": "open", - "type": "limit", - "datetime": "2021-09-10T10:10:10" - }, - ), - Order.from_ccxt_order( - { - "id": "1233353", - "symbol": "ETH/EUR", - "amount": 199, - "filled": 100, - "remaining": 99, - "price": 2000, - "side": "buy", - "status": "open", - "type": "limit", - "datetime": "2021-09-10T10:10:10" - }, - ), - Order.from_ccxt_order( - { - "id": "1233", - "symbol": "ADA/EUR", - "amount": 4023, - "filled": 4023, - "remaining": 0, - "price": 1, - "side": "buy", - "status": "closed", - "type": "limit", - "datetime": "2021-10-12T10:10:10" - }, - ), - Order.from_ccxt_order( - { - "id": "1233324", - "symbol": "ADA/EUR", - "amount": 4023, - "filled": 4023, - "remaining": 0, - "price": 1.10, - "side": "sell", - "status": "closed", - "type": "limit", - "datetime": "2021-10-13T10:10:10" - }, - ), - Order.from_ccxt_order( - { - "id": "123", - "symbol": "XRP/EUR", - "amount": 10, - "filled": 5, - "remaining": 5, - "price": 1, - "side": "buy", - "type": "limit", - "status": "open", - "datetime": "2021-10-10T10:10:10" - } - ) - ] - - self.app.add_algorithm(Algorithm()) - self.app.initialize() - - # Get ada buy order - portfolio = self.app.container.portfolio_service() \ - .find({"identifier": "test"}) - order_service = self.app.container.order_service() - ada_position = self.app.container.position_service().find( - {"symbol": "ADA", "portfolio_id": portfolio.id} - ) - orders = order_service.get_all( - {"position": ada_position.id, "order_side": "buy"} - ) - ada_buy_order = orders[0] - self.assertEqual(4023, ada_buy_order.amount) - self.assertEqual(1, ada_buy_order.price) - self.assertEqual("BUY", ada_buy_order.order_side) - self.assertEqual("LIMIT", ada_buy_order.order_type) - self.assertEqual("CLOSED", ada_buy_order.status) - self.assertEqual( - datetime( - year=2021, month=10, day=13, hour=10, minute=10, second=10 - ), - ada_buy_order.get_trade_closed_at() - ) - self.assertEqual(4023, ada_buy_order.get_filled()) - self.assertEqual(0, ada_buy_order.get_remaining()) - self.assertAlmostEqual(4023 * 0.1, ada_buy_order.get_net_gain()) - - def test_sync_trades_stateless(self): - configuration_service = self.app.container.configuration_service() - configuration_service.config[SYMBOLS] = ["KSM/EUR"] - self.market_service.symbols = ["KSM/EUR"] - configuration_service.config[APP_MODE] = "STATELESS" - - self.app.add_portfolio_configuration( - PortfolioConfiguration( - identifier="test", - market="binance", - trading_symbol="EUR", - initial_balance=500, - track_from="2021-10-10T10:10:10" - ) - ) - self.app.add_market_credential( - MarketCredential( - market="binance", - api_key="test", - secret_key="test" - ) - ) - self.market_service.balances = { - "EUR": 1200, - "BTC": 0.5, - "ETH": 199, - "ADA": 4023, - "XRP": 10, - } - self.market_service.orders = [ - Order.from_ccxt_order( - { - "id": "12333535", - "symbol": "BTC/EUR", - "amount": 0.5, - "filled": 0.3, - "remaining": 0.2, - "price": 50000, - "side": "buy", - "status": "open", - "type": "limit", - "datetime": "2021-09-10T10:10:10" - }, - ), - Order.from_ccxt_order( - { - "id": "1233353", - "symbol": "ETH/EUR", - "amount": 199, - "filled": 100, - "remaining": 99, - "price": 2000, - "side": "buy", - "status": "open", - "type": "limit", - "datetime": "2021-09-10T10:10:10" - }, - ), - Order.from_ccxt_order( - { - "id": "1233", - "symbol": "ADA/EUR", - "amount": 4023, - "filled": 4023, - "remaining": 0, - "price": 1, - "side": "buy", - "status": "closed", - "type": "limit", - "datetime": "2021-10-12T10:10:10" - }, - ), - Order.from_ccxt_order( - { - "id": "1233324", - "symbol": "ADA/EUR", - "amount": 4023, - "filled": 4023, - "remaining": 0, - "price": 1.10, - "side": "sell", - "status": "closed", - "type": "limit", - "datetime": "2021-10-13T10:10:10" - }, - ), - Order.from_ccxt_order( - { - "id": "123", - "symbol": "XRP/EUR", - "amount": 10, - "filled": 5, - "remaining": 5, - "price": 1, - "side": "buy", - "type": "limit", - "status": "open", - "datetime": "2021-10-10T10:10:10" - } - ) - ] - - self.app.add_algorithm(Algorithm()) - self.app.initialize() - - # Get ada buy order - portfolio = self.app.container.portfolio_service() \ - .find({"identifier": "test"}) - order_service = self.app.container.order_service() - ada_position = self.app.container.position_service().find( - {"symbol": "ADA", "portfolio_id": portfolio.id} - ) - orders = order_service.get_all( - {"position": ada_position.id, "order_side": "buy"} - ) - ada_buy_order = orders[0] - self.assertEqual(4023, ada_buy_order.amount) - self.assertEqual(1, ada_buy_order.price) - self.assertEqual("BUY", ada_buy_order.order_side) - self.assertEqual("LIMIT", ada_buy_order.order_type) - self.assertEqual("CLOSED", ada_buy_order.status) - self.assertEqual( - datetime( - year=2021, month=10, day=13, hour=10, minute=10, second=10 - ), - ada_buy_order.get_trade_closed_at() - ) - self.assertEqual(4023, ada_buy_order.get_filled()) - self.assertEqual(0, ada_buy_order.get_remaining()) - self.assertAlmostEqual(4023 * 0.1, ada_buy_order.get_net_gain()) diff --git a/tests/test_create_app.py b/tests/test_create_app.py index dc1cd7e8..d33fafc7 100644 --- a/tests/test_create_app.py +++ b/tests/test_create_app.py @@ -1,7 +1,7 @@ import os from unittest import TestCase -from investing_algorithm_framework import create_app, Config, \ +from investing_algorithm_framework import create_app, \ PortfolioConfiguration, Algorithm, MarketCredential from investing_algorithm_framework.domain import RESOURCE_DIRECTORY from tests.resources import MarketServiceStub @@ -25,29 +25,15 @@ def test_create_app(self): app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir}) self.assertIsNotNone(app) self.assertIsNone(app._flask_app) - self.assertFalse(app._stateless) self.assertIsNotNone(app.container) self.assertIsNone(app.algorithm) def test_create_app_with_config(self): - config = Config( - resource_directory=self.resource_dir - ) - app = create_app(config=config) + app = create_app(config={RESOURCE_DIRECTORY: self.resource_dir}) self.assertIsNotNone(app) self.assertIsNotNone(app.config) self.assertIsNone(app._flask_app) - self.assertFalse(app._stateless) - self.assertIsNotNone(app.container) - self.assertIsNone(app.algorithm) - - def test_create_app_stateless(self): - app = create_app(stateless=True, config={}) - self.assertIsNotNone(app) - self.assertIsNotNone(app.config) - self.assertTrue(app._stateless) self.assertIsNotNone(app.container) - self.assertIsNotNone(app.config) self.assertIsNone(app.algorithm) def test_create_app_web(self): @@ -76,8 +62,12 @@ def test_create_app_web(self): secret_key="secret_key" ) ) + market_service = MarketServiceStub(app.container.market_credential_service()) + market_service.balances = { + "USDT": 1000 + } app.container.market_service.override( - MarketServiceStub(app.container.market_credential_service()) + market_service ) app.initialize() self.assertIsNotNone(app) From 0b5efe356cbbe08c411bbef1f85e274a0cc37984 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Sat, 11 Jan 2025 12:11:42 +0100 Subject: [PATCH 4/9] Fix flake8 warnings --- .../app/algorithm.py | 3 +- investing_algorithm_framework/app/app.py | 22 +-- .../cli/create_azure_function_app_skeleton.py | 45 +++++- .../cli/deploy_to_azure_function.py | 136 ++++++++++++------ .../azure_function_framework_app.py.template | 3 +- .../azure_function_function_app.py.template | 76 ++-------- investing_algorithm_framework/create_app.py | 3 +- .../domain/config.py | 2 +- .../models/market_data_sources/ccxt.py | 3 +- .../services/azure/state_handler.py | 12 +- .../services/configuration_service.py | 5 +- .../market_data_source_service.py | 4 +- .../portfolio_configuration_service.py | 4 +- .../services/portfolios/portfolio_service.py | 3 +- .../portfolios/portfolio_sync_service.py | 40 ++++-- 15 files changed, 207 insertions(+), 154 deletions(-) diff --git a/investing_algorithm_framework/app/algorithm.py b/investing_algorithm_framework/app/algorithm.py index 8cc3ec16..d5a6b714 100644 --- a/investing_algorithm_framework/app/algorithm.py +++ b/investing_algorithm_framework/app/algorithm.py @@ -144,7 +144,8 @@ def start(self, number_of_iterations: int = None): app. Args: - number_of_iterations (int): (Optional) The number of iterations to run the algorithm + number_of_iterations (int): (Optional) The number of + iterations to run the algorithm Returns: None diff --git a/investing_algorithm_framework/app/app.py b/investing_algorithm_framework/app/app.py index 2604c883..7b79a35a 100644 --- a/investing_algorithm_framework/app/app.py +++ b/investing_algorithm_framework/app/app.py @@ -96,7 +96,7 @@ def initialize_config(self): # Check if the resource directory is set if RESOURCE_DIRECTORY not in config \ - or config[RESOURCE_DIRECTORY] is None: + or config[RESOURCE_DIRECTORY] is None: logger.info( "Resource directory not set, setting" + " to current working directory" @@ -115,7 +115,7 @@ def initialize_config(self): config = configuration_service.get_config() if DATABASE_DIRECTORY_PATH not in config \ - or config[DATABASE_DIRECTORY_PATH] is None: + or config[DATABASE_DIRECTORY_PATH] is None: resource_dir = config[RESOURCE_DIRECTORY] configuration_service.add_value( DATABASE_DIRECTORY_PATH, @@ -125,7 +125,7 @@ def initialize_config(self): config = configuration_service.get_config() if SQLALCHEMY_DATABASE_URI not in config \ - or config[SQLALCHEMY_DATABASE_URI] is None: + or config[SQLALCHEMY_DATABASE_URI] is None: path = "sqlite:///" + os.path.join( configuration_service.config[DATABASE_DIRECTORY_PATH], configuration_service.config[DATABASE_NAME] @@ -262,7 +262,6 @@ def sync(self, portfolio): # Sync all trades from exchange with current trade history portfolio_sync_service.sync_trades(portfolio) - def _initialize_standard(self): """ Initialize the app for standard mode by setting the configuration @@ -545,8 +544,9 @@ def run( number of iterations provided. This mode is useful when running the app in a loop for a fixed number of iterations. - This function first checks if there is an algorithm registered. If not, it raises an OperationalException. Then it initializes the algorithm with the services and the configuration. - + This function first checks if there is an algorithm registered. + If not, it raises an OperationalException. Then it + initializes the algorithm with the services and the configuration. Args: payload (dict): The payload to handle for the algorithm @@ -697,8 +697,10 @@ def _initialize_web(self): def _create_resources_if_not_exists(self): """ - Function to create the resources required by the app if they do not exist. This function will check if the resource directory exists and - check if the database directory exists. If they do not exist, it will create them. + Function to create the resources required by the app if they + do not exist. This function will check if the resource directory + exists and check if the database directory exists. If they do + not exist, it will create them. Returns: None @@ -947,8 +949,8 @@ def on_initialize(self, app_hook: AppHook): def after_initialize(self, app_hook: AppHook): """ - Function to add a hook that runs after the app is initialized. The hook - should be an instance of AppHook. + Function to add a hook that runs after the app is initialized. + The hook should be an instance of AppHook. """ if inspect.isclass(app_hook): diff --git a/investing_algorithm_framework/cli/create_azure_function_app_skeleton.py b/investing_algorithm_framework/cli/create_azure_function_app_skeleton.py index a9868d83..9edad3aa 100644 --- a/investing_algorithm_framework/cli/create_azure_function_app_skeleton.py +++ b/investing_algorithm_framework/cli/create_azure_function_app_skeleton.py @@ -2,6 +2,22 @@ import click +def create_file(file_path): + """ + Creates a new file. + + Args: + file_path (str): The path to the file to create. + + Returns: + None + """ + + if not os.path.exists(file_path): + with open(file_path, "w") as file: + file.write("") + + def create_file_from_template(template_path, output_path): """ Creates a new file by replacing placeholders in a template file. @@ -9,16 +25,21 @@ def create_file_from_template(template_path, output_path): Args: template_path (str): The path to the template file. output_path (str): The path to the output file. - replacements (dict): A dictionary of placeholder keys and their replacements. + replacements (dict): A dictionary of placeholder keys and + their replacements. Returns: None """ - with open(template_path, "r") as file: - template = file.read() - with open(output_path, "w") as file: - file.write(template) + # Check if output path already exists + if not os.path.exists(output_path): + with open(template_path, "r") as file: + template = file.read() + + with open(output_path, "w") as file: + file.write(template) + def create_azure_function_skeleton( add_app_template, add_requirements_template @@ -62,6 +83,10 @@ def create_azure_function_skeleton( "templates", "azure_function_framework_app.py.template" ) + create_file_from_template( + function_app_path, + os.path.join(cwd, "app_entry.py") + ) if add_requirements_template: requirements_path = os.path.join( @@ -69,7 +94,12 @@ def create_azure_function_skeleton( "templates", "azure_function_requirements.txt.template" ) + create_file_from_template( + function_app_path, + os.path.join(cwd, "requirements.txt") + ) + create_file(os.path.join(cwd, "__init__.py")) create_file_from_template( template_host_file_path, os.path.join(cwd, "host.json") @@ -87,7 +117,7 @@ def create_azure_function_skeleton( os.path.join(cwd, "requirements.txt") ) print( - f"Function App trading bot skeleton creation completed" + "Function App trading bot skeleton creation completed" ) @@ -110,7 +140,8 @@ def cli(add_app_template, add_requirements_template): Args: add_app_template (bool): Flag to create an app skeleton. - add_requirements_template (bool): Flag to create a requirements template. + add_requirements_template (bool): Flag to create a + requirements template. Returns: None diff --git a/investing_algorithm_framework/cli/deploy_to_azure_function.py b/investing_algorithm_framework/cli/deploy_to_azure_function.py index 79bb820f..01372c07 100644 --- a/investing_algorithm_framework/cli/deploy_to_azure_function.py +++ b/investing_algorithm_framework/cli/deploy_to_azure_function.py @@ -100,7 +100,7 @@ async def publish_function_app( "--settings", f"AZURE_STORAGE_CONNECTION_STRING={storage_connection_string}", f"AZURE_STORAGE_CONTAINER_NAME={storage_container_name}", - f"--resource-group", resource_group_name + "--resource-group", resource_group_name ) _, stderr1 = await add_settings_process.communicate() @@ -114,7 +114,7 @@ async def publish_function_app( raise Exception("Error adding App settings") print( - f"Added app settings to the Function App successfully" + "Added app settings to the Function App successfully" ) # Step 3: Update the cors settings @@ -137,7 +137,7 @@ async def publish_function_app( raise Exception("Error adding cors settings") print("All app settings have been added successfully.") - print(f"Function App creation completed successfully.") + print("Function App creation completed successfully.") except Exception as e: print(f"Error publishing Function App: {e}") @@ -149,7 +149,8 @@ async def create_function_app( region ): """ - Creates an Azure Function App in a Consumption Plan and deploys a Python Function. + Creates an Azure Function App in a Consumption Plan and deploys + a Python Function. Args: resource_group_name (str): Resource group name. @@ -183,9 +184,13 @@ async def create_function_app( print(f"Function App '{deployment_name}' already exists.") return stdout.decode() - # If the return code is non-zero, and the error indicates the Function App doesn't exist, proceed to create it + # If the return code is non-zero, and the error indicates + # the Function App doesn't exist, proceed to create it if "ResourceNotFound" in stderr.decode(): - print(f"Function App '{deployment_name}' does not exist. Proceeding to create it...") + print( + f"Function App '{deployment_name}' does not exist." + + " Proceeding to create it..." + ) else: # If the error is something else, raise it print(f"Error checking for Function App: {stderr.decode()}") @@ -220,8 +225,14 @@ async def create_function_app( # Check the return code for the create command if create_process.returncode != 0: - print(f"Error creating Function App: {create_stderr.decode().strip()}") - raise Exception(f"Error creating Function App: {create_stderr.decode().strip()}") + print( + "Error creating Function App: " + + f"{create_stderr.decode().strip()}" + ) + raise Exception( + "Error creating Function App: " + + f"{create_stderr.decode().strip()}" + ) print(f"Function App '{deployment_name}' created successfully.") return {"status": "created"} @@ -238,7 +249,8 @@ def create_file_from_template(template_path, output_path): Args: template_path (str): The path to the template file. output_path (str): The path to the output file. - replacements (dict): A dictionary of placeholder keys and their replacements. + replacements (dict): A dictionary of placeholder + keys and their replacements. Returns: None @@ -258,7 +270,8 @@ def ensure_consumption_plan( credential ): """ - Ensures that an App Service Plan with the Consumption Plan exists. If not, creates it. + Ensures that an App Service Plan with the Consumption Plan exists. + If not, creates it. Args: resource_group_name (str): The name of the resource group. @@ -273,13 +286,15 @@ def ensure_consumption_plan( try: print( - f"Checking if App Service Plan '{plan_name}' exists in resource group '{resource_group_name}'..." + f"Checking if App Service Plan '{plan_name}' exists" + + f" in resource group '{resource_group_name}'..." ) plan = web_client.app_service_plans.get(resource_group_name, plan_name) print(f"App Service Plan '{plan_name}' already exists.") except Exception: # Plan does not exist print( - f"App Service Plan '{plan_name}' not found. Creating it as a Consumption Plan..." + f"App Service Plan '{plan_name}' not found. " + + "Creating it as a Consumption Plan..." ) plan = web_client.app_service_plans.begin_create_or_update( resource_group_name, @@ -294,6 +309,7 @@ def ensure_consumption_plan( print(f"App Service Plan '{plan_name}' created successfully.") return plan + def ensure_storage_account( storage_account_name, resource_group_name, @@ -304,8 +320,11 @@ def ensure_storage_account( """ Checks if a storage account exists. If it doesn't, creates it. - If no storage account name is provided, a unique name will be generated. - However, before we create a new storage account, we check if there a storage account exists with the prefix 'iafstorageaccount'. If it exists, we use that storage account. + If no storage account name is provided, a unique name will + be generated. However, before we create a new + storage account, we check if there a storage account exists + with the prefix 'iafstorageaccount'. If it exists, we use + that storage account. Args: storage_account_name (str): The name of the storage account. @@ -345,9 +364,11 @@ def ensure_storage_account( resource_group_name, storage_account_name, ).keys[1].value - connection_string = f"DefaultEndpointsProtocol=https;AccountName={storage_account_name};AccountKey={account_key};EndpointSuffix=core.windows.net" + connection_string = "DefaultEndpointsProtocol=https;" + \ + f"AccountName={storage_account_name};" + \ + f"AccountKey={account_key};EndpointSuffix=core.windows.net" return connection_string, storage_account_name - except Exception as e: # If the storage account does not exist + except Exception: # If the storage account does not exist print("Creating storage account ...") # Create storage account @@ -363,13 +384,20 @@ def ensure_storage_account( storage_async_operation.result() if storage_async_operation.status() == "Succeeded": - print(f"Storage account '{storage_account_name}' created successfully.") - - account_key = storage_client.storage_accounts.list_keys( - resource_group_name, - storage_account_name, - ).keys[1].value - connection_string = f"DefaultEndpointsProtocol=https;AccountName={storage_account_name};AccountKey={account_key};EndpointSuffix=core.windows.net" + print( + f"Storage account '{storage_account_name}'" + + "created successfully." + ) + + account_key = storage_client.storage_accounts\ + .list_keys( + resource_group_name, + storage_account_name, + ).keys[1].value + connection_string = f"DefaultEndpointsProtocol=https;"\ + f"AccountName={storage_account_name};" + \ + f"AccountKey={account_key};" + \ + "EndpointSuffix=core.windows.net" return connection_string, storage_account_name @@ -420,10 +448,14 @@ def get_default_subscription_id(): subscription_id = result.stdout.strip() print(f"Default subscription ID: {subscription_id}") return subscription_id - except subprocess.CalledProcessError as e: - print("Error fetching default subscription ID. Please log in with 'az login'.") + except subprocess.CalledProcessError: + print( + "Error fetching default subscription ID." + + " Please log in with 'az login'." + ) raise + def ensure_resource_group( resource_group_name, region, @@ -431,13 +463,15 @@ def ensure_resource_group( create_if_not_exists ): """ - Checks if a resource group exists. If it doesn't, creates it if `create_if_not_exists` is True. + Checks if a resource group exists. If it doesn't, + creates it if `create_if_not_exists` is True. Args: resource_group_name (str): The name of the resource group. region (str): The Azure region for the resources. subscription_id (str): The Azure subscription ID. - create_if_not_exists (bool): Flag to create the resource group if it does not exist. + create_if_not_exists (bool): Flag to create the + resource group if it does not exist. Returns: None @@ -447,21 +481,32 @@ def ensure_resource_group( print(f"Checking if resource group '{resource_group_name}' exists...") try: - resource_group = resource_client.resource_groups.get(resource_group_name) + resource_client.resource_groups.get(resource_group_name) print(f"Resource group '{resource_group_name}' already exists.") except Exception: # If the resource group does not exist try: if create_if_not_exists: - print(f"Resource group '{resource_group_name}' not found. Creating it...") + print( + f"Resource group '{resource_group_name}' not" + + " found. Creating it..." + ) resource_client.resource_groups.create_or_update( resource_group_name, {"location": region}, ) - print(f"Resource group '{resource_group_name}' created successfully.") + print( + f"Resource group '{resource_group_name}'" + + " created successfully." + ) else: - print(f"Resource group '{resource_group_name}' does not exist, and 'create_if_not_exists' is False.") - raise ValueError(f"Resource group '{resource_group_name}' does not exist.") + print( + f"Resource group '{resource_group_name}' does" + + " not exist, and 'create_if_not_exists' is False." + ) + raise ValueError( + f"Resource group '{resource_group_name}' does not exist." + ) except Exception as e: raise Exception(f"Error creating resource group: {e}") @@ -521,7 +566,8 @@ def create_storage_and_function( if not subscription_id: subscription_id = get_default_subscription_id() - # Authenticate using DefaultAzureCredential (requires environment variables or Azure CLI login) + # Authenticate using DefaultAzureCredential + # (requires environment variables or Azure CLI login) credential = DefaultAzureCredential() # Check if the resource group exists @@ -537,13 +583,14 @@ def create_storage_and_function( generate_unique_resource_name(STORAGE_ACCOUNT_NAME_PREFIX) # Ensure storage account exists - storage_account_connection_string, storage_account_name = ensure_storage_account( - storage_account_name, - resource_group_name, - region, - subscription_id, - credential - ) + storage_account_connection_string, storage_account_name = \ + ensure_storage_account( + storage_account_name, + resource_group_name, + region, + subscription_id, + credential + ) # Create Function App asyncio.run( @@ -596,7 +643,8 @@ def create_storage_and_function( @click.option( '--deployment_name', required=True, - help='The name of the deployment. This will be used as the name of the Function App.' + help='The name of the deployment. This will be" + \ + "used as the name of the Function App.' ) @click.option( '--region', @@ -625,7 +673,8 @@ def cli( skip_login ): """ - Command-line tool for creating an Azure storage account, blob container, and Function App. + Command-line tool for creating an Azure storage account, + blob container, and Function App. Args: resource_group (str): The name of the resource group. @@ -634,7 +683,8 @@ def cli( container_name (str): The name of the blob container. function_app (str): The name of the Azure Function App. region (str): The Azure region for the resources. - create_resource_group_if_not_exists (bool): Flag to create the resource group if it does not exist. + create_resource_group_if_not_exists (bool): Flag to create + the resource group if it does not exist. Returns: None diff --git a/investing_algorithm_framework/cli/templates/azure_function_framework_app.py.template b/investing_algorithm_framework/cli/templates/azure_function_framework_app.py.template index ee2d1cbd..aad8fb10 100644 --- a/investing_algorithm_framework/cli/templates/azure_function_framework_app.py.template +++ b/investing_algorithm_framework/cli/templates/azure_function_framework_app.py.template @@ -22,7 +22,6 @@ bitvavo_btc_eur_ticker = CCXTTickerMarketDataSource( symbol="BTC/EUR", ) app = create_app(state_handler=AzureBlobStorageStateHandler()) -app.add_market_data_source(bitvavo_btc_eur_ohlcv_2h) algorithm = Algorithm() app.add_market_credential(MarketCredential(market="bitvavo")) app.add_portfolio_configuration( @@ -39,7 +38,7 @@ app.add_algorithm(algorithm) time_unit=TimeUnit.HOUR, interval=2, # Specify market data sources that need to be passed to the strategy - market_data_sources=[bitvavo_btc_eur_ticker, "BTC-ohlcv"] + market_data_sources=[bitvavo_btc_eur_ticker, bitvavo_btc_eur_ohlcv_2h] ) def perform_strategy(algorithm: Algorithm, market_data: dict): # By default, ohlcv data is passed as polars df in the form of diff --git a/investing_algorithm_framework/cli/templates/azure_function_function_app.py.template b/investing_algorithm_framework/cli/templates/azure_function_function_app.py.template index 7291ffa1..b903c265 100644 --- a/investing_algorithm_framework/cli/templates/azure_function_function_app.py.template +++ b/investing_algorithm_framework/cli/templates/azure_function_function_app.py.template @@ -1,78 +1,20 @@ -import logging - import azure.functions as func -from investing_algorithm_framework import StatelessAction -from app import app as investing_algorithm_framework_app - - import logging -import logging.config - -LOGGING_CONFIG = { - 'version': 1, - 'disable_existing_loggers': False, - 'formatters': { - 'default': { - 'format': '%(asctime)s - %(name)s - %(levelname)s - %(message)s', - }, - }, - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', - 'formatter': 'default', - }, - 'file': { - 'class': 'logging.FileHandler', - 'formatter': 'default', - 'filename': 'app_logs.log', - }, - }, - 'loggers': { # Make sure to add a 'loggers' section - 'investing_algorithm_framework': { # Define your logger here - 'level': 'INFO', # Set the desired level - 'handlers': ['console', 'file'], # Use these handlers - 'propagate': False, # Prevent logs from propagating to the root logger (optional) - }, - }, - 'root': { # Optional: Root logger configuration - 'level': 'WARNING', # Root logger defaults to WARNING - 'handlers': ['console', 'file'], - }, -} - -logging.config.dictConfig(LOGGING_CONFIG) -app: func.FunctionApp = func.FunctionApp() - -# Change your interval here, e.ge. "0 */1 * * * *" for every minute -# or "0 0 */1 * * *" for every hour or "0 */5 * * * *" for every 5 minutes -@func.timer_trigger( - schedule="0 */5 * * * *", - arg_name="myTimer", - run_on_startup=False, - use_monitor=False -) -@func.http_trigger( - route='start', methods=["GET"], auth_level=func.AuthLevel.FUNCTION -) -def app(myTimer: func.TimerRequest) -> None: +from investing_algorithm_framework import DEFAULT_LOGGING_CONFIG,\ + StatelessAction +from app_entry import app as trading_bot_app - if myTimer.past_due: - logging.info('The timer is past due!') +logging.config.dictConfig(DEFAULT_LOGGING_CONFIG) - logging.info('Python timer trigger function ran at %s', myTimer.next) - investing_algorithm_framework_app.run( - payload={"ACTION": StatelessAction.RUN_STRATEGY.value} - ) +app = func.FunctionApp() -@app.route(route="test", auth_level=func.AuthLevel.ANONYMOUS) -def test(req: func.HttpRequest) -> func.HttpResponse: +@app.route(route="trading_bot", auth_level=func.AuthLevel.ANONYMOUS) +def trading_bot(req: func.HttpRequest) -> func.HttpResponse: logging.info('Python HTTP trigger function processed a request.') name = req.params.get('name') - investing_algorithm_framework_app.run( - payload={"ACTION": StatelessAction.RUN_STRATEGY.value} - ) + trading_bot_app.run(payload={"action": StatelessAction.RUN_STRATEGY.value}) if not name: try: req_body = req.get_json() @@ -87,4 +29,4 @@ def test(req: func.HttpRequest) -> func.HttpResponse: return func.HttpResponse( "This HTTP triggered function executed successfully. Pass a name in the query string or in the request body for a personalized response.", status_code=200 - ) + ) \ No newline at end of file diff --git a/investing_algorithm_framework/create_app.py b/investing_algorithm_framework/create_app.py index e2de4557..991c32c3 100644 --- a/investing_algorithm_framework/create_app.py +++ b/investing_algorithm_framework/create_app.py @@ -1,5 +1,4 @@ import logging -import os from dotenv import load_dotenv from .app import App @@ -9,7 +8,7 @@ def create_app( - config: dict =None, + config: dict = None, web=False, state_handler=None ) -> App: diff --git a/investing_algorithm_framework/domain/config.py b/investing_algorithm_framework/domain/config.py index ac2ce7d8..1d969ade 100644 --- a/investing_algorithm_framework/domain/config.py +++ b/investing_algorithm_framework/domain/config.py @@ -74,7 +74,7 @@ def equals(self, other): 'investing_algorithm_framework': { # Define your logger here 'level': 'INFO', # Set the desired level 'handlers': ['console', 'file'], # Use these handlers - 'propagate': False, # Prevent logs from propagating to the root logger (optional) + 'propagate': False, }, }, 'root': { # Optional: Root logger configuration diff --git a/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py b/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py index ddea17bc..6f1fa526 100644 --- a/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py +++ b/investing_algorithm_framework/infrastructure/models/market_data_sources/ccxt.py @@ -524,7 +524,8 @@ def get_data(self, **kwargs): storage_path = self.get_storage_path() logger.info( - f"Getting OHLCV data for {self.symbol} from {start_date} to {end_date}" + f"Getting OHLCV data for {self.symbol} " + + f"from {start_date} to {end_date}" ) data = None diff --git a/investing_algorithm_framework/infrastructure/services/azure/state_handler.py b/investing_algorithm_framework/infrastructure/services/azure/state_handler.py index 68e0a4ca..2b019e8c 100644 --- a/investing_algorithm_framework/infrastructure/services/azure/state_handler.py +++ b/investing_algorithm_framework/infrastructure/services/azure/state_handler.py @@ -29,7 +29,9 @@ def _initialize(self): if self.connection_string is None: raise OperationalException( - "Azure Blob Storage state handler requires a connection string or an environment variable AZURE_STORAGE_CONNECTION_STRING to be set" + "Azure Blob Storage state handler requires" + + " a connection string or an environment" + + " variable AZURE_STORAGE_CONNECTION_STRING to be set" ) if self.container_name is None: @@ -39,7 +41,9 @@ def _initialize(self): if self.container_name is None: raise OperationalException( - "Azure Blob Storage state handler requires a container name or an environment variable AZURE_STORAGE_CONTAINER_NAME to be set" + "Azure Blob Storage state handler requires a" + + " container name or an environment" + + " variable AZURE_STORAGE_CONTAINER_NAME to be set" ) def save(self, source_directory: str): @@ -73,7 +77,9 @@ def save(self, source_directory: str): # Upload the file with open(file_path, "rb") as data: - container_client.upload_blob(name=blob_name, data=data, overwrite=True) + container_client.upload_blob( + name=blob_name, data=data, overwrite=True + ) except Exception as ex: logger.error(f"Error saving state to Azure Blob Storage: {ex}") diff --git a/investing_algorithm_framework/services/configuration_service.py b/investing_algorithm_framework/services/configuration_service.py index 5bd74bb7..c0a0ab66 100644 --- a/investing_algorithm_framework/services/configuration_service.py +++ b/investing_algorithm_framework/services/configuration_service.py @@ -6,7 +6,9 @@ "ENVIRONMENT": Environment.PROD.value, "LOG_LEVEL": 'DEBUG', "APP_DIR": os.path.abspath(os.path.dirname(__file__)), - "PROJECT_ROOT": os.path.abspath(os.path.join(os.path.abspath(os.path.dirname(__file__)), os.pardir)), + "PROJECT_ROOT": os.path.abspath( + os.path.join(os.path.abspath(os.path.dirname(__file__)), os.pardir) + ), "RESOURCE_DIRECTORY": os.getenv(RESOURCE_DIRECTORY), "CHECK_PENDING_ORDERS": True, "SQLITE_INITIALIZED": False, @@ -33,6 +35,7 @@ "SCHEDULER_API_ENABLED": True, } + class ConfigurationService: def __init__(self): diff --git a/investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py b/investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py index 174964f3..a2f52d05 100644 --- a/investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py +++ b/investing_algorithm_framework/services/market_data_source_service/market_data_source_service.py @@ -95,7 +95,9 @@ def get_data(self, identifier): if isinstance(identifier, str): raise OperationalException( f"Market data source with identifier {identifier} not found. " - "Please make sure that the market data source is registered to the app if you refer to it by identifier in your strategy." + "Please make sure that the market data source is " + "registered to the app if you refer to it by " + "identifier in your strategy." ) def get_ticker_market_data_source(self, symbol, market=None): diff --git a/investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py b/investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py index 2c8e0208..7e7acc1a 100644 --- a/investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py +++ b/investing_algorithm_framework/services/portfolios/portfolio_configuration_service.py @@ -7,7 +7,9 @@ class PortfolioConfigurationService: """ - Service to manage portfolio configurations. This service will manage the portfolio configurations that the user has registered in the app. + Service to manage portfolio configurations. This service will + manage the portfolio configurations that the user has + registered in the app. """ def __init__(self, portfolio_repository, position_repository): diff --git a/investing_algorithm_framework/services/portfolios/portfolio_service.py b/investing_algorithm_framework/services/portfolios/portfolio_service.py index bf99d45f..bdbdca9e 100644 --- a/investing_algorithm_framework/services/portfolios/portfolio_service.py +++ b/investing_algorithm_framework/services/portfolios/portfolio_service.py @@ -130,7 +130,8 @@ def create_portfolio_from_configuration( If the portfolio does not exist, it will be created. Args: - portfolio_configuration (PortfolioConfiguration) Portfolio configuration to create the portfolio from + portfolio_configuration (PortfolioConfiguration) + Portfolio configuration to create the portfolio from Returns: Portfolio: Portfolio created from the configuration diff --git a/investing_algorithm_framework/services/portfolios/portfolio_sync_service.py b/investing_algorithm_framework/services/portfolios/portfolio_sync_service.py index b84592b3..7090142b 100644 --- a/investing_algorithm_framework/services/portfolios/portfolio_sync_service.py +++ b/investing_algorithm_framework/services/portfolios/portfolio_sync_service.py @@ -40,12 +40,20 @@ def sync_unallocated(self, portfolio): available balance of the portfolio from the exchange and update the unallocated balance of the portfolio accordingly. - If the portfolio already exists (exists in the database), then a check is done if the exchange has the available balance of - the portfolio unallocated balance. If the exchange does not have - the available balance of the portfolio, an OperationalException will be raised. + If the portfolio already exists (exists in the database), + then a check is done if the exchange has the available + balance of the portfolio unallocated balance. If the exchange + does not have the available balance of the portfolio, + an OperationalException will be raised. If the portfolio does not exist, the portfolio will be created with - the unallocated balance of the portfolio set to the available balance on the exchange. If also a initial balance is set in the portfolio configuration, the unallocated balance will be set to the initial balance (given the balance is available on the exchange). If the initial balance is not set, the unallocated balance will be set to the available balance on the exchange. + the unallocated balance of the portfolio set to the available + balance on the exchange. If also a initial balance is set in + the portfolio configuration, the unallocated balance will be set + to the initial balance (given the balance is available on + the exchange). If the initial balance is not set, the + unallocated balance will be set to the available balance + on the exchange. Args: portfolio: Portfolio object @@ -86,7 +94,8 @@ def sync_unallocated(self, portfolio): else: unallocated = portfolio.initial_balance else: - # If the portfolio does not have an initial balance set, get the available balance on the exchange + # If the portfolio does not have an initial balance + # set, get the available balance on the exchange if portfolio.trading_symbol.upper() not in balances: raise OperationalException( f"There is no available balance on the exchange for " @@ -97,7 +106,9 @@ def sync_unallocated(self, portfolio): f"{portfolio.market}." ) else: - unallocated = float(balances[portfolio.trading_symbol.upper()]) + unallocated = float( + balances[portfolio.trading_symbol.upper()] + ) update_data = { "unallocated": unallocated, @@ -120,13 +131,18 @@ def sync_unallocated(self, portfolio): ) else: - # Check if the portfolio unallocated balance is available on the exchange + # Check if the portfolio unallocated balance is + # available on the exchange if portfolio.unallocated > 0: - if portfolio.trading_symbol.upper() not in balances or portfolio.unallocated > float(balances[portfolio.trading_symbol.upper()]): + if portfolio.trading_symbol.upper() not in balances \ + or portfolio.unallocated > \ + float(balances[portfolio.trading_symbol.upper()]): raise OperationalException( f"Out of sync: the unallocated balance" " of the portfolio is more than the available" - " balance on the exchange. Please make sure that you" f" have at least {portfolio.unallocated}" + " balance on the exchange. Please make sure" + " that you have at least " + f"{portfolio.unallocated}" f" {portfolio.trading_symbol.upper()} available" " on the exchange." ) @@ -195,7 +211,8 @@ def sync_orders(self, portfolio): """ Function to sync all local orders with the orders on the exchange. This method will go over all local open orders and check if they are - changed on the exchange. If they are, the local order will be updated to match the status on the exchange. + changed on the exchange. If they are, the local order will be + updated to match the status on the exchange. Args: portfolio: Portfolio object @@ -204,9 +221,6 @@ def sync_orders(self, portfolio): None """ - portfolio_configuration = self.portfolio_configuration_service \ - .get(portfolio.identifier) - open_orders = self.order_repository.get_all( { "portfolio": portfolio.identifier, From 5ca190572f457d6ced075a1cc5da791838f55266 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Sat, 11 Jan 2025 12:14:56 +0100 Subject: [PATCH 5/9] Add poetry lock --- poetry.lock | 2059 ++++++++++++++++++++++++++------------------------- 1 file changed, 1047 insertions(+), 1012 deletions(-) diff --git a/poetry.lock b/poetry.lock index a06ba9a0..7d7cb859 100644 --- a/poetry.lock +++ b/poetry.lock @@ -16,13 +16,13 @@ pycares = ">=4.0.0" [[package]] name = "aiohappyeyeballs" -version = "2.4.3" +version = "2.4.4" description = "Happy Eyeballs for asyncio" optional = false python-versions = ">=3.8" files = [ - {file = "aiohappyeyeballs-2.4.3-py3-none-any.whl", hash = "sha256:8a7a83727b2756f394ab2895ea0765a0a8c475e3c71e98d43d76f22b4b435572"}, - {file = "aiohappyeyeballs-2.4.3.tar.gz", hash = "sha256:75cf88a15106a5002a8eb1dab212525c00d1f4c0fa96e551c9fbe6f09a621586"}, + {file = "aiohappyeyeballs-2.4.4-py3-none-any.whl", hash = "sha256:a980909d50efcd44795c4afeca523296716d50cd756ddca6af8c65b996e27de8"}, + {file = "aiohappyeyeballs-2.4.4.tar.gz", hash = "sha256:5fdd7d87889c63183afc18ce9271f9b0a7d32c2303e394468dd45d514a757745"}, ] [[package]] @@ -139,13 +139,13 @@ speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] [[package]] name = "aiosignal" -version = "1.3.1" +version = "1.3.2" description = "aiosignal: a list of registered asynchronous callbacks" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" files = [ - {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, - {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, + {file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"}, + {file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"}, ] [package.dependencies] @@ -172,24 +172,24 @@ tz = ["backports.zoneinfo"] [[package]] name = "anyio" -version = "4.6.2.post1" +version = "4.8.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.9" files = [ - {file = "anyio-4.6.2.post1-py3-none-any.whl", hash = "sha256:6d170c36fba3bdd840c73d3868c1e777e33676a69c3a72cf0a0d5d6d8009b61d"}, - {file = "anyio-4.6.2.post1.tar.gz", hash = "sha256:4c8bc31ccdb51c7f7bd251f51c609e038d63e34219b44aa86e47576389880b4c"}, + {file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, + {file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, ] [package.dependencies] exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" sniffio = ">=1.1" -typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] -doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] trio = ["trio (>=0.26.1)"] [[package]] @@ -281,21 +281,18 @@ test = ["dateparser (==1.*)", "pre-commit", "pytest", "pytest-cov", "pytest-mock [[package]] name = "asttokens" -version = "2.4.1" +version = "3.0.0" description = "Annotate AST trees with source code positions" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, - {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, + {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, + {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"}, ] -[package.dependencies] -six = ">=1.12.0" - [package.extras] -astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] -test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] +astroid = ["astroid (>=2,<4)"] +test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "async-lru" @@ -324,19 +321,19 @@ files = [ [[package]] name = "attrs" -version = "24.2.0" +version = "24.3.0" description = "Classes Without Boilerplate" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, - {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, + {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, + {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, ] [package.extras] benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] @@ -520,6 +517,7 @@ files = [ ] [package.dependencies] +tinycss2 = {version = ">=1.1.0,<1.5", optional = true, markers = "extra == \"css\""} webencodings = "*" [package.extras] @@ -538,13 +536,13 @@ files = [ [[package]] name = "ccxt" -version = "4.4.31" +version = "4.4.47" description = "A JavaScript / TypeScript / Python / C# / PHP cryptocurrency trading library with support for 100+ exchanges" optional = false python-versions = "*" files = [ - {file = "ccxt-4.4.31-py2.py3-none-any.whl", hash = "sha256:c1ce1eab0b0e37124e25ca78825347ef670929da951365a4c370ff4565b8042f"}, - {file = "ccxt-4.4.31.tar.gz", hash = "sha256:7d3ef4d2c17902d3dd4f6dce8df3d4f5ded3fef84ef61b56bd236970d13c3097"}, + {file = "ccxt-4.4.47-py2.py3-none-any.whl", hash = "sha256:68a1b098072133b924f944cbe16ccae9104bb6d2c68291f37b1f543640a9a2e7"}, + {file = "ccxt-4.4.47.tar.gz", hash = "sha256:a3d0c8707d21c5eb7cf7292d183168ef64ccf7260886028a859462299f34e13c"}, ] [package.dependencies] @@ -563,13 +561,13 @@ type = ["mypy (==1.6.1)"] [[package]] name = "certifi" -version = "2024.8.30" +version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] [[package]] @@ -653,127 +651,114 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "3.4.0" +version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.7" files = [ - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, - {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, - {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, - {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, - {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, - {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, - {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, - {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, - {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, - {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] [[package]] name = "click" -version = "8.1.7" +version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [package.dependencies] @@ -873,86 +858,86 @@ toml = ["tomli"] [[package]] name = "cryptography" -version = "43.0.3" +version = "44.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false -python-versions = ">=3.7" -files = [ - {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, - {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, - {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, - {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, - {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, - {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, - {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, - {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, - {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, - {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, - {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, +python-versions = "!=3.9.0,!=3.9.1,>=3.7" +files = [ + {file = "cryptography-44.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:84111ad4ff3f6253820e6d3e58be2cc2a00adb29335d4cacb5ab4d4d34f2a123"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15492a11f9e1b62ba9d73c210e2416724633167de94607ec6069ef724fad092"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:831c3c4d0774e488fdc83a1923b49b9957d33287de923d58ebd3cec47a0ae43f"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, + {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, + {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, + {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, + {file = "cryptography-44.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:abc998e0c0eee3c8a1904221d3f67dcfa76422b23620173e28c11d3e626c21bd"}, + {file = "cryptography-44.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:660cb7312a08bc38be15b696462fa7cc7cd85c3ed9c576e81f4dc4d8b2b31591"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1923cb251c04be85eec9fda837661c67c1049063305d6be5721643c22dd4e2b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:404fdc66ee5f83a1388be54300ae978b2efd538018de18556dde92575e05defc"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, + {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, + {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, + {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, + {file = "cryptography-44.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:708ee5f1bafe76d041b53a4f95eb28cdeb8d18da17e597d46d7833ee59b97ede"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:37d76e6863da3774cd9db5b409a9ecfd2c71c981c38788d3fcfaf177f447b731"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:f677e1268c4e23420c3acade68fac427fffcb8d19d7df95ed7ad17cdef8404f4"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7cb1e5e56ca0933b4873c0220a78b773b24d40d186b6738080b73d3d0a756"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:8b3e6eae66cf54701ee7d9c83c30ac0a1e3fa17be486033000f2a73a12ab507c"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:be4ce505894d15d5c5037167ffb7f0ae90b7be6f2a98f9a5c3442395501c32fa"}, + {file = "cryptography-44.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:62901fb618f74d7d81bf408c8719e9ec14d863086efe4185afd07c352aee1d2c"}, + {file = "cryptography-44.0.0.tar.gz", hash = "sha256:cd4e834f340b4293430701e772ec543b0fbe6c2dea510a5286fe0acabe153a02"}, ] [package.dependencies] cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] -nox = ["nox"] -pep8test = ["check-sdist", "click", "mypy", "ruff"] -sdist = ["build"] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"] +docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test = ["certifi (>=2024)", "cryptography-vectors (==44.0.0)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] test-randomorder = ["pytest-randomly"] [[package]] name = "debugpy" -version = "1.8.8" +version = "1.8.11" description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" files = [ - {file = "debugpy-1.8.8-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:e59b1607c51b71545cb3496876544f7186a7a27c00b436a62f285603cc68d1c6"}, - {file = "debugpy-1.8.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6531d952b565b7cb2fbd1ef5df3d333cf160b44f37547a4e7cf73666aca5d8d"}, - {file = "debugpy-1.8.8-cp310-cp310-win32.whl", hash = "sha256:b01f4a5e5c5fb1d34f4ccba99a20ed01eabc45a4684f4948b5db17a319dfb23f"}, - {file = "debugpy-1.8.8-cp310-cp310-win_amd64.whl", hash = "sha256:535f4fb1c024ddca5913bb0eb17880c8f24ba28aa2c225059db145ee557035e9"}, - {file = "debugpy-1.8.8-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:c399023146e40ae373753a58d1be0a98bf6397fadc737b97ad612886b53df318"}, - {file = "debugpy-1.8.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09cc7b162586ea2171eea055985da2702b0723f6f907a423c9b2da5996ad67ba"}, - {file = "debugpy-1.8.8-cp311-cp311-win32.whl", hash = "sha256:eea8821d998ebeb02f0625dd0d76839ddde8cbf8152ebbe289dd7acf2cdc6b98"}, - {file = "debugpy-1.8.8-cp311-cp311-win_amd64.whl", hash = "sha256:d4483836da2a533f4b1454dffc9f668096ac0433de855f0c22cdce8c9f7e10c4"}, - {file = "debugpy-1.8.8-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:0cc94186340be87b9ac5a707184ec8f36547fb66636d1029ff4f1cc020e53996"}, - {file = "debugpy-1.8.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64674e95916e53c2e9540a056e5f489e0ad4872645399d778f7c598eacb7b7f9"}, - {file = "debugpy-1.8.8-cp312-cp312-win32.whl", hash = "sha256:5c6e885dbf12015aed73770f29dec7023cb310d0dc2ba8bfbeb5c8e43f80edc9"}, - {file = "debugpy-1.8.8-cp312-cp312-win_amd64.whl", hash = "sha256:19ffbd84e757a6ca0113574d1bf5a2298b3947320a3e9d7d8dc3377f02d9f864"}, - {file = "debugpy-1.8.8-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:705cd123a773d184860ed8dae99becd879dfec361098edbefb5fc0d3683eb804"}, - {file = "debugpy-1.8.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:890fd16803f50aa9cb1a9b9b25b5ec321656dd6b78157c74283de241993d086f"}, - {file = "debugpy-1.8.8-cp313-cp313-win32.whl", hash = "sha256:90244598214bbe704aa47556ec591d2f9869ff9e042e301a2859c57106649add"}, - {file = "debugpy-1.8.8-cp313-cp313-win_amd64.whl", hash = "sha256:4b93e4832fd4a759a0c465c967214ed0c8a6e8914bced63a28ddb0dd8c5f078b"}, - {file = "debugpy-1.8.8-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:143ef07940aeb8e7316de48f5ed9447644da5203726fca378f3a6952a50a9eae"}, - {file = "debugpy-1.8.8-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f95651bdcbfd3b27a408869a53fbefcc2bcae13b694daee5f1365b1b83a00113"}, - {file = "debugpy-1.8.8-cp38-cp38-win32.whl", hash = "sha256:26b461123a030e82602a750fb24d7801776aa81cd78404e54ab60e8b5fecdad5"}, - {file = "debugpy-1.8.8-cp38-cp38-win_amd64.whl", hash = "sha256:f3cbf1833e644a3100eadb6120f25be8a532035e8245584c4f7532937edc652a"}, - {file = "debugpy-1.8.8-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:53709d4ec586b525724819dc6af1a7703502f7e06f34ded7157f7b1f963bb854"}, - {file = "debugpy-1.8.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a9c013077a3a0000e83d97cf9cc9328d2b0bbb31f56b0e99ea3662d29d7a6a2"}, - {file = "debugpy-1.8.8-cp39-cp39-win32.whl", hash = "sha256:ffe94dd5e9a6739a75f0b85316dc185560db3e97afa6b215628d1b6a17561cb2"}, - {file = "debugpy-1.8.8-cp39-cp39-win_amd64.whl", hash = "sha256:5c0e5a38c7f9b481bf31277d2f74d2109292179081f11108e668195ef926c0f9"}, - {file = "debugpy-1.8.8-py2.py3-none-any.whl", hash = "sha256:ec684553aba5b4066d4de510859922419febc710df7bba04fe9e7ef3de15d34f"}, - {file = "debugpy-1.8.8.zip", hash = "sha256:e6355385db85cbd666be703a96ab7351bc9e6c61d694893206f8001e22aee091"}, + {file = "debugpy-1.8.11-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:2b26fefc4e31ff85593d68b9022e35e8925714a10ab4858fb1b577a8a48cb8cd"}, + {file = "debugpy-1.8.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61bc8b3b265e6949855300e84dc93d02d7a3a637f2aec6d382afd4ceb9120c9f"}, + {file = "debugpy-1.8.11-cp310-cp310-win32.whl", hash = "sha256:c928bbf47f65288574b78518449edaa46c82572d340e2750889bbf8cd92f3737"}, + {file = "debugpy-1.8.11-cp310-cp310-win_amd64.whl", hash = "sha256:8da1db4ca4f22583e834dcabdc7832e56fe16275253ee53ba66627b86e304da1"}, + {file = "debugpy-1.8.11-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:85de8474ad53ad546ff1c7c7c89230db215b9b8a02754d41cb5a76f70d0be296"}, + {file = "debugpy-1.8.11-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ffc382e4afa4aee367bf413f55ed17bd91b191dcaf979890af239dda435f2a1"}, + {file = "debugpy-1.8.11-cp311-cp311-win32.whl", hash = "sha256:40499a9979c55f72f4eb2fc38695419546b62594f8af194b879d2a18439c97a9"}, + {file = "debugpy-1.8.11-cp311-cp311-win_amd64.whl", hash = "sha256:987bce16e86efa86f747d5151c54e91b3c1e36acc03ce1ddb50f9d09d16ded0e"}, + {file = "debugpy-1.8.11-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:84e511a7545d11683d32cdb8f809ef63fc17ea2a00455cc62d0a4dbb4ed1c308"}, + {file = "debugpy-1.8.11-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce291a5aca4985d82875d6779f61375e959208cdf09fcec40001e65fb0a54768"}, + {file = "debugpy-1.8.11-cp312-cp312-win32.whl", hash = "sha256:28e45b3f827d3bf2592f3cf7ae63282e859f3259db44ed2b129093ca0ac7940b"}, + {file = "debugpy-1.8.11-cp312-cp312-win_amd64.whl", hash = "sha256:44b1b8e6253bceada11f714acf4309ffb98bfa9ac55e4fce14f9e5d4484287a1"}, + {file = "debugpy-1.8.11-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:8988f7163e4381b0da7696f37eec7aca19deb02e500245df68a7159739bbd0d3"}, + {file = "debugpy-1.8.11-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c1f6a173d1140e557347419767d2b14ac1c9cd847e0b4c5444c7f3144697e4e"}, + {file = "debugpy-1.8.11-cp313-cp313-win32.whl", hash = "sha256:bb3b15e25891f38da3ca0740271e63ab9db61f41d4d8541745cfc1824252cb28"}, + {file = "debugpy-1.8.11-cp313-cp313-win_amd64.whl", hash = "sha256:d8768edcbeb34da9e11bcb8b5c2e0958d25218df7a6e56adf415ef262cd7b6d1"}, + {file = "debugpy-1.8.11-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:ad7efe588c8f5cf940f40c3de0cd683cc5b76819446abaa50dc0829a30c094db"}, + {file = "debugpy-1.8.11-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:189058d03a40103a57144752652b3ab08ff02b7595d0ce1f651b9acc3a3a35a0"}, + {file = "debugpy-1.8.11-cp38-cp38-win32.whl", hash = "sha256:32db46ba45849daed7ccf3f2e26f7a386867b077f39b2a974bb5c4c2c3b0a280"}, + {file = "debugpy-1.8.11-cp38-cp38-win_amd64.whl", hash = "sha256:116bf8342062246ca749013df4f6ea106f23bc159305843491f64672a55af2e5"}, + {file = "debugpy-1.8.11-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:654130ca6ad5de73d978057eaf9e582244ff72d4574b3e106fb8d3d2a0d32458"}, + {file = "debugpy-1.8.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23dc34c5e03b0212fa3c49a874df2b8b1b8fda95160bd79c01eb3ab51ea8d851"}, + {file = "debugpy-1.8.11-cp39-cp39-win32.whl", hash = "sha256:52d8a3166c9f2815bfae05f386114b0b2d274456980d41f320299a8d9a5615a7"}, + {file = "debugpy-1.8.11-cp39-cp39-win_amd64.whl", hash = "sha256:52c3cf9ecda273a19cc092961ee34eb9ba8687d67ba34cc7b79a521c1c64c4c0"}, + {file = "debugpy-1.8.11-py2.py3-none-any.whl", hash = "sha256:0e22f846f4211383e6a416d04b4c13ed174d24cc5d43f5fd52e7821d0ebc8920"}, + {file = "debugpy-1.8.11.tar.gz", hash = "sha256:6ad2688b69235c43b020e04fecccdf6a96c8943ca9c2fb340b8adc103c655e57"}, ] [[package]] @@ -979,110 +964,100 @@ files = [ [[package]] name = "dependency-injector" -version = "4.43.0" +version = "4.45.0" description = "Dependency injection framework for Python" optional = false -python-versions = "*" +python-versions = ">=3.7" files = [ - {file = "dependency_injector-4.43.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9df575603a6ae75b393917249c1a4808a33e06ecef903f82b813ba3fc094ac8"}, - {file = "dependency_injector-4.43.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1735072bcabd6562dabfb0ae0ad5328aebcfeff0ac595efc9c190773fa203198"}, - {file = "dependency_injector-4.43.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c6d89a4be17a428316dd791eac7e98cb8653824933551fd21486b4677d9e634"}, - {file = "dependency_injector-4.43.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e64bc6f6c96ce2c8b12fd1105420f9fa84c5aa83b2dc52e92b1be2c077819fe0"}, - {file = "dependency_injector-4.43.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c67791ca696a96b8f3e82f9025e6d214e2c2fd8891fa4315a4160b99fe4c4ace"}, - {file = "dependency_injector-4.43.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4d6bb5c0d741882685109579ab556fc6e89ece9d26ccf70e35ac4f9b84097207"}, - {file = "dependency_injector-4.43.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d4f9d1466352810e2f536613e52016632d8a172867bfc9d91e4cfccce14180f1"}, - {file = "dependency_injector-4.43.0-cp310-cp310-win32.whl", hash = "sha256:be408c3ff6e55d40362295013ff543e8524778f598dc59b2a40d9620ac2743c1"}, - {file = "dependency_injector-4.43.0-cp310-cp310-win_amd64.whl", hash = "sha256:9fd7f4f44ddff0cd7e40f8923f93ff83f0e8c018a85d36b37a88a835eca7f47f"}, - {file = "dependency_injector-4.43.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f3b2c1d3a4c75772854ef7b47d725e6e894b9e3590bf13bf19e3bac4935ed88"}, - {file = "dependency_injector-4.43.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39f46a2fe7ddfea96d18521c5c0f265ca3f177060f0ccb4505f9a638c3429b49"}, - {file = "dependency_injector-4.43.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64c46dd2dd5543554e3ffe05c935fdb4e8a805a162e56c7c7519acefdd374ff0"}, - {file = "dependency_injector-4.43.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3252b32935df3c5f34a4da83d43a38838e0f48d54e47405fcda5e59a2ec4a930"}, - {file = "dependency_injector-4.43.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:200c0286c88c9cb521050eb56e6465f50c289cb145b5308d9d8d79b0b4bac4b6"}, - {file = "dependency_injector-4.43.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0791c72a89c4a59c3120b659c3b3e8fbb82e85d4732ffdb8548e395f19425e73"}, - {file = "dependency_injector-4.43.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fb045e5db6078da309889fc1447cd89b8bd1c810853b436773f895eae477b3b"}, - {file = "dependency_injector-4.43.0-cp311-cp311-win32.whl", hash = "sha256:d24ebaa219edf95a6dc4559053bb8e295dfdee396478488382fdaa078de57d92"}, - {file = "dependency_injector-4.43.0-cp311-cp311-win_amd64.whl", hash = "sha256:230a6ae585a18845bea4001688badd3c2b7f373bcac5c6e659b14e1e1b6e03f2"}, - {file = "dependency_injector-4.43.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f8e520e420dbbb376d81c2c668b6e0be166c79877d3b56b97547db17199d636"}, - {file = "dependency_injector-4.43.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f58beecd944dbdb1ead80ca86945d572d314e48e34cb6e32165ac912e5eed0d5"}, - {file = "dependency_injector-4.43.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a447075f567ce894c1d3a5493d7d0cd9542a9d451ca13d8ffce4e0016bdba201"}, - {file = "dependency_injector-4.43.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:36326658415372972711edbd03cc3a68a6866b7b1a5ac7090807601e0fa3847c"}, - {file = "dependency_injector-4.43.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d70a7651fac1e74889617dfe802798cc941a736929cc6b00a62c33529f3291b5"}, - {file = "dependency_injector-4.43.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:177b4f3726e36556d04f442b2743e86a56005319e41fcc8410a98951d159f7bf"}, - {file = "dependency_injector-4.43.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:544843c65aa289a1a9ca79e7a8bd4aad41dbac2d56600ca2735047a404ba4a3b"}, - {file = "dependency_injector-4.43.0-cp312-cp312-win32.whl", hash = "sha256:3e559a7f28b4ffae35e28333b0b10e4fce8021214c078e6fa5b85770e821e1eb"}, - {file = "dependency_injector-4.43.0-cp312-cp312-win_amd64.whl", hash = "sha256:7d80534f4d4712494ec61761f0629588a2eb12789aa3ec9f54247ffacf92f266"}, - {file = "dependency_injector-4.43.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7ceb7fabf22138ec4cd3d6943ce9e1ba4ab9514a87511ebe3597382240ec79e"}, - {file = "dependency_injector-4.43.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3481413efdd3b85ccdce73a0ecbd75d47eadcc823b73cd0d5accdc2c2477fc0c"}, - {file = "dependency_injector-4.43.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38e6f3fb010796ce4691906334156d25e4e365bc6e841b64464518eec0896f2a"}, - {file = "dependency_injector-4.43.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f312bb9dec59a6216800a7d90417cf74be0b08a19fa8f54d8d2bbb3fdf516927"}, - {file = "dependency_injector-4.43.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:11f3129a16f229c54de87e034fd42d64b8de630a6938d2b83f4c1d9815181ec1"}, - {file = "dependency_injector-4.43.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6b6dafac3a69d0ad0de96a6f0fb0ea786c1b5cf8bbe88fec1de3455f4b685647"}, - {file = "dependency_injector-4.43.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fc1b0b335b1d9810204aa687f17c2e8ac1bdebdd935f10bd54a6a7d3af952631"}, - {file = "dependency_injector-4.43.0-cp313-cp313-win32.whl", hash = "sha256:fba63d0fec1fe78b2fae545f219fce16a5fcaa2acc0a2f9564a3e1c5ec27b356"}, - {file = "dependency_injector-4.43.0-cp313-cp313-win_amd64.whl", hash = "sha256:efd7dd1d39e2998c21c5b02f303792a7c4205ea4fda63f2306a5e5b98e2a251a"}, - {file = "dependency_injector-4.43.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c93b20bfbb392ad83f8a900632d0c92c5e04bb5b5fa3961be062f1fa516401f"}, - {file = "dependency_injector-4.43.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690e17cb943edb2d907885e8f11faa476d58cf0b1b4b67278fe3203616147997"}, - {file = "dependency_injector-4.43.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f6bab4c4334321ccef5bcf94313c34b730c1df91812205181620358318145b6"}, - {file = "dependency_injector-4.43.0-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:145d20e8f7e1327a27cfdf87b2cc3aac976da759726e7ac104fc76d05f6902e9"}, - {file = "dependency_injector-4.43.0-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:d886e846fd6128b70ebdcdb40c04fa7a5b6c9f5e3ee2672686606af63eb55ecb"}, - {file = "dependency_injector-4.43.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:5997d3460ef1b1db12be512a0cf57816c8c7940cbad262f0eddfea0a61ae65f8"}, - {file = "dependency_injector-4.43.0-cp36-cp36m-win32.whl", hash = "sha256:9b1991ea8bc60f4cf2593670387d54cc834d470c06a69e9231a179cd1333379b"}, - {file = "dependency_injector-4.43.0-cp36-cp36m-win_amd64.whl", hash = "sha256:315e5244d3c9d02c9d1e134e06c1db044cae01a33295ce65718eb230758db4c4"}, - {file = "dependency_injector-4.43.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6431919fc77666c01d1512de914d0b58ee0ef34722c77b58687832063a64df14"}, - {file = "dependency_injector-4.43.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4bee3b51a433c848f77c3ce45f341ab85213a442f9e2ad9bc92289ef2a7c8bd"}, - {file = "dependency_injector-4.43.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07081a671927cea7b4a9812b69a84f4d554fcd6f6da3fafe8a719c714ecb69a3"}, - {file = "dependency_injector-4.43.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:c4f30c9a505f649ab0cb004cd9f48c3df5b06287c177536417f8d30cfdfa5bc2"}, - {file = "dependency_injector-4.43.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:31f80970567bdaf040165f679b12780fb7b7a90d6921ac26355f401b1a516e3d"}, - {file = "dependency_injector-4.43.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ffbd8e51ffcf8f8ff8cb6edccee5e77932ec4bdda1ca17b9f6a98d396fde5993"}, - {file = "dependency_injector-4.43.0-cp37-cp37m-win32.whl", hash = "sha256:8d8fa0c3fa495dc8605232e730913da4af43757e4be78d4a6cce8d5c1d88d9ac"}, - {file = "dependency_injector-4.43.0-cp37-cp37m-win_amd64.whl", hash = "sha256:fd15638b67f41034edd1eaffbb008ac2ff47839ab32b3fa13ca1b33e8a95cb9f"}, - {file = "dependency_injector-4.43.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9bb96f2762cbfc9b654075075871a7051d37a9cceeabace877a95a03d8142cf0"}, - {file = "dependency_injector-4.43.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b4a5d41813b56d122f13ae0fdbb1078dd41206c8f5bb7192a1582c982a34ad6"}, - {file = "dependency_injector-4.43.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbeb38085c1cb20b21338294e90fa9e7944bf162da63f587734a4d6991c4b1f1"}, - {file = "dependency_injector-4.43.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc58fc7e60924e9a93e2b83976173ea095b55f3508175252d50884aa08340185"}, - {file = "dependency_injector-4.43.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:726131d9f64d26a4915fae413502584e4f739b66eec6b7b23ab43d8bab255b1d"}, - {file = "dependency_injector-4.43.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:fc619ae6b5490beea9f9a53230ddbe084f11609ec1a3d56b0e58966c2ce81815"}, - {file = "dependency_injector-4.43.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:28dc5af50ceee52df01b26f6d7bfcc32ff2aaba938c252d110c52f5de9580b0c"}, - {file = "dependency_injector-4.43.0-cp38-cp38-win32.whl", hash = "sha256:c09a977122a6bb57f74f871802dbf3a204cfdae197d423bf93ca4ec3aba871f4"}, - {file = "dependency_injector-4.43.0-cp38-cp38-win_amd64.whl", hash = "sha256:6ea5fc7a6d2b0f780e444643e46a6bdf739213fe4b9d26c286fb78f37e337bb8"}, - {file = "dependency_injector-4.43.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7190b45fc53dc6f41633fa6942052e38f7c463ad47305ba95d56c5b4d075e887"}, - {file = "dependency_injector-4.43.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:440c2f9cf21b83bfd30c9da3ae8f95f7d395800a8feae538bf60db50c2c8974a"}, - {file = "dependency_injector-4.43.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3b83c0e637f28bb2d8a0555e545ffdd76d9d69fe3e936ab25c6af35e8556e36"}, - {file = "dependency_injector-4.43.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8d6ec07de0479e209899f98ed7babf35fa72878933f153faf37a5abaf4cb125"}, - {file = "dependency_injector-4.43.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:454c2442e2b105d038b41c153cd9a9a55c6d7bf61bac0db8ace38f8c3f62179e"}, - {file = "dependency_injector-4.43.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:52c7ab128131ff67e4b776f6704d47ae90f43686287dce0f5bb702f93f870e7a"}, - {file = "dependency_injector-4.43.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d3bda19adcdfccb3fbb20a7516c5b8330a09714301a71b2c5d7fe4304d6479df"}, - {file = "dependency_injector-4.43.0-cp39-cp39-win32.whl", hash = "sha256:6ad97e9366fc1451ff79de12ec0294a7c81ccb74b0da7b594b6cf8e8a7b9c0bb"}, - {file = "dependency_injector-4.43.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f074aae7be8461ba4faff3be431e4d8dfe596e5fca884642e6e10f1fb585471"}, - {file = "dependency_injector-4.43.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:0647cda56ff7ab2bbf2bf051800edeb6b5bab4c134df04e59ba231cf1c95ce1e"}, - {file = "dependency_injector-4.43.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8acd5819c86424c674f7d30a2521f7883e35241afe4b3b569d481a36bc0bae3"}, - {file = "dependency_injector-4.43.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fec243d4d5fcdb423c543139d93eeaa76526e35f0faa8555eb03fb45fafab605"}, - {file = "dependency_injector-4.43.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:deee43b5aaa62355f3bed9c4a8129676ef68001f48f8244fa1aa6f39d0921ba8"}, - {file = "dependency_injector-4.43.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ece96fb2f0f33abdc80afe28f66dbce51372809cf39b58ebe6baea24f3f2e88f"}, - {file = "dependency_injector-4.43.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2caef27963266c85f6c12df57eabe06e04c38f349f2f2d423d765104164e189"}, - {file = "dependency_injector-4.43.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67b984a51a9635b57d4018d10d32bdb189c6f70d0a93739e0505b86634fa0bd2"}, - {file = "dependency_injector-4.43.0-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf4972a7fa40cb18f8efd0924c8d2999fffafd9817aa81858f2b7869d53c855"}, - {file = "dependency_injector-4.43.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f399de5683a5fa6cd0a4a952e3b5466e56e9516cd0f69cc502b67fb7f8288f8b"}, - {file = "dependency_injector-4.43.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:6aa12b0af78d9d8486bbed435d88cde8190074c3c268a6f9c7d41d10d6a232ab"}, - {file = "dependency_injector-4.43.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3003592fd1b9fbaa585ebec41ffba5d6b6afbf6cb90c144a03a3269ae0100b48"}, - {file = "dependency_injector-4.43.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b30d4a3d544087aca03045af1bf92bad37588831bc361901104394e47b5eeb2"}, - {file = "dependency_injector-4.43.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:199791d9bb3b187eb05c41a9ca9fc87fc2bcbb78a0fcc3b38b60baf7be1880eb"}, - {file = "dependency_injector-4.43.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4fcad4d27a8da1e8f2b8a941bde064b516a80c7bdf75ad51293c3876b7c75cb8"}, - {file = "dependency_injector-4.43.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:6e9e02eb20a58f958e2ac1a9c3fc7506c4365409b56729f20fd481db94b44173"}, - {file = "dependency_injector-4.43.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10a9c66bd93aca63de45403cbcca4061b07e75f702ca203bbd121938d3b3ed0e"}, - {file = "dependency_injector-4.43.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b44e524c7add4b897aac62c7b964307b0604dc8dabeea35bb06c28d65ddb6bc6"}, - {file = "dependency_injector-4.43.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab7664f88262a3ee2aae07c84fcff0a08f0020141e2b3a8e3065b8dbf5fb13e6"}, - {file = "dependency_injector-4.43.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:862f24685eeada264632e333ec5f49affdb1b4aac6a52785b6b7b88e72d0be5d"}, - {file = "dependency_injector-4.43.0.tar.gz", hash = "sha256:d0774234cc4d5c860fa971912182131b45f41ebabd9e3fc2ce80f4b1d05de057"}, + {file = "dependency_injector-4.45.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7ec03cd54b79b5fc8df46ae821392358a24dc371bda01a2a68917ab0e559a96b"}, + {file = "dependency_injector-4.45.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:433883ccdfa353e6c8c2e20ba6eb08f77149a7e15f816f05be65bb791b86e5fe"}, + {file = "dependency_injector-4.45.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:892c40f800e19732e9a10d595237bfbe09637e07a98b55c8b0526ac7f8f7400b"}, + {file = "dependency_injector-4.45.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cedd4f87795b9a9712bf2b6786f0d37e8848418a6b4a59cb185fd1c3866dd888"}, + {file = "dependency_injector-4.45.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:db61b15cfa7fc679b22abb216bbef7888cc332a3f1f0279d6a621cf899661a80"}, + {file = "dependency_injector-4.45.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:75d0f8c748efe5e1f8a0970742483b38687bfe460496a4b7e344440798d0fa99"}, + {file = "dependency_injector-4.45.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21cfd5d2f26cca02bd897069b4460be1cbac1b07875a33885a84f9aa165cead5"}, + {file = "dependency_injector-4.45.0-cp310-cp310-win32.whl", hash = "sha256:cebd7fa8a9bbfed3af38c6f98763caadf0b3c63ade142c3eefb41788247cb5d7"}, + {file = "dependency_injector-4.45.0-cp310-cp310-win_amd64.whl", hash = "sha256:f349e3ceb1913eba37489cab82831e9d46d9008e59c6560cfefe0c57dc1168b0"}, + {file = "dependency_injector-4.45.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9a76ab1e114402f284c71039391826d9e43083169d88568ede109ff4fc85de1e"}, + {file = "dependency_injector-4.45.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4aefc511be8ba97d823a71fb52a3b0ccaa7f9387cb51444862ebcb919822cae"}, + {file = "dependency_injector-4.45.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a013c93a870a3a1ce9829dd272bd02b194ea286df48dfb9d059cf34526738b8"}, + {file = "dependency_injector-4.45.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1641f25faf543df14b2395a3cea51bd0610bc7b2c1cb6ed1c13ec4bec0063e2"}, + {file = "dependency_injector-4.45.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:97dc40a527e488df23e9fb5b33a85f9176b730449ad2e498d6cc556dcad540ae"}, + {file = "dependency_injector-4.45.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9396c71f25744fd0ded9b171fee05ea3ca13a87c12512ddd8b7e4330f70647eb"}, + {file = "dependency_injector-4.45.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d98a490f8a2decf40ca2aa69d0559d01dd96131422fa2086626fd5949837335a"}, + {file = "dependency_injector-4.45.0-cp311-cp311-win32.whl", hash = "sha256:4cec41b8dd678555ea025ad320e6354cad92b18b6433f8cd0b423b02989668c3"}, + {file = "dependency_injector-4.45.0-cp311-cp311-win_amd64.whl", hash = "sha256:5eb99c2ea049ac1065a0b4cf06166f70fac697049ddd342b88cb6a264f106faf"}, + {file = "dependency_injector-4.45.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2144948b5218f480ca94c11e848762aa305539ec67406ccecfbcb556d06a61dd"}, + {file = "dependency_injector-4.45.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82a0c665ee9bbb9434497a9b97bbb2018a9c427c0d014bff77dda2e0586c9020"}, + {file = "dependency_injector-4.45.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:403f44e2073986138dd95f4a8ac864453524b18385c44b838cba8a2f7f742f2e"}, + {file = "dependency_injector-4.45.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c2a38cf7826ff253d30958c35bc1e121af292f0f12fb5ffd1ad3ea5377b63e3b"}, + {file = "dependency_injector-4.45.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2b18413b2f965793f8e401996a6cc01e0aa0336568805fc822285cdc72cfb7d9"}, + {file = "dependency_injector-4.45.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ed3e60296b3b9d29c3eb57cb44a82d7627da1a7eac68ac97fbf9bbd9e2a5cb99"}, + {file = "dependency_injector-4.45.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd1afbbf03ed97007f6d4cb11944776e79207d9d7634ba5c4f897f4802b382ae"}, + {file = "dependency_injector-4.45.0-cp312-cp312-win32.whl", hash = "sha256:2bfca5875aedb5a0995a4b2e5a928aef5791a58f4280d0c3003cb0720385f546"}, + {file = "dependency_injector-4.45.0-cp312-cp312-win_amd64.whl", hash = "sha256:5510a68445741fce2af071fd7fda177c4249f2b576d2b2b2b5af23083e4208e8"}, + {file = "dependency_injector-4.45.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16c17b17d1974a288b16f0d9b4eda7fae892dc2823ff1d95a93207723b681b9a"}, + {file = "dependency_injector-4.45.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1cfd84779e718974b8d40eefc2d664ba69c142ca676ed97f487de05e733e9e"}, + {file = "dependency_injector-4.45.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b03382b9423f008bf307d4dee7e56ea1e070fc0e23481e4e327b01b9b463a9d"}, + {file = "dependency_injector-4.45.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dcba70e44a3d6436196f1ffebad0b02f0a72a7c8014d85617f21bd8535434bf4"}, + {file = "dependency_injector-4.45.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73d52ba9efdbb6a1c0ba49d9c68be9cedb0abfa0e6fbdfb942371c642cb30541"}, + {file = "dependency_injector-4.45.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9ebcefa9218941337466cf0985bdcd5d38cf84ab1d3a4d7a3cc91845e38c0d1e"}, + {file = "dependency_injector-4.45.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d785dacb136479b0536e8b05f83a7375f09404d63eabd1be9b694eca6defea4"}, + {file = "dependency_injector-4.45.0-cp313-cp313-win32.whl", hash = "sha256:3df40ff15bd31058b24cd79781b1dc2f31e0c157ee2067566dfd7a92191969e9"}, + {file = "dependency_injector-4.45.0-cp313-cp313-win_amd64.whl", hash = "sha256:f018f634edf57d4b4214e007eaa2432d2410f304b170f5684bd3fdbffd244b1f"}, + {file = "dependency_injector-4.45.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b2bbc02553633b5b3328c209b1908c2ae8250fdb3dd0536e9021a43a9d6399c"}, + {file = "dependency_injector-4.45.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:229ae0f01d5bc04666256737b958b426452214a1ef1082bbc0114a53b8ea21e3"}, + {file = "dependency_injector-4.45.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3fff67247e3120970b6761a80508cd5852729c4517479ebbbf5c3690fab191"}, + {file = "dependency_injector-4.45.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:2683b1d9588c1af34e5a15f334a5e2d3c7e546bdc5c3226c85e8b6243d60c72b"}, + {file = "dependency_injector-4.45.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:52015c82f0dd2940e7d1902b287d808265d20f4d2c9713543a7aa29986893571"}, + {file = "dependency_injector-4.45.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:c6ae2f448cdcd1fe6ce4e9869e20166df10e334b3328d6a8aff35c0837180d9e"}, + {file = "dependency_injector-4.45.0-cp37-cp37m-win32.whl", hash = "sha256:445c3b35c3c8c029e313f46ae8087a1d6edad8aa74d7f470125bfc22d8ab4831"}, + {file = "dependency_injector-4.45.0-cp37-cp37m-win_amd64.whl", hash = "sha256:6f44c0fca126682804f6428038bf03a2f992f6b694a0cc8515bc58cc10534bd7"}, + {file = "dependency_injector-4.45.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d3b777c80d92eea5c581658bb9e7e1292efb770cfc464bcb47605aa39d3c45fe"}, + {file = "dependency_injector-4.45.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2f10a6410bbc8c58c04e26f8b48db4df511491d5bbb55c59992fcc8645fb5e5"}, + {file = "dependency_injector-4.45.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:580a1c5cc96d88531509b4af285a602b3fa322f06cf684c99bb5506eb2d12cf2"}, + {file = "dependency_injector-4.45.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9b70cb03d38f89aba3ba9ba7494952b1e68e018b9a52857590aa74c1f984798b"}, + {file = "dependency_injector-4.45.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3472714fa547f5df4de1e1f4d1aa854214aad9e53d3bd75bd0d4d1eddc9df3b3"}, + {file = "dependency_injector-4.45.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:480db147baf30b4ee1d38705992694b23ba3c9554d6b2f039a5f13c6f10366d5"}, + {file = "dependency_injector-4.45.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bc2749ed42a7bcb1e62f2ff63a1b931da63f116e50ccdabaf32871d24489999c"}, + {file = "dependency_injector-4.45.0-cp38-cp38-win32.whl", hash = "sha256:83476347e7d3e4b63a3ba4617b87b5b4259b5da7826b7cf1b6c6035c21c8c77d"}, + {file = "dependency_injector-4.45.0-cp38-cp38-win_amd64.whl", hash = "sha256:144dfae8da58ecd326dbe7fa63dab014ddf07af13b5ab6c0084881373852ecde"}, + {file = "dependency_injector-4.45.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:326109be456ffa80698e27fe7f32411bb120c4fb2f65059abff2616910a261d3"}, + {file = "dependency_injector-4.45.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b0cb3cdb0523f75bc8bf3e07406670fc8f79a295e8309c2c638a485f3ae9ee0"}, + {file = "dependency_injector-4.45.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4207434037f31f98c36f39519d217aa3683dbb12ec2a3a69f1490441935c5f2"}, + {file = "dependency_injector-4.45.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aad4e274ec08e359dcf8cf391e08b399b9be67cc5d3c41e2d7b61a8967bb7a29"}, + {file = "dependency_injector-4.45.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ee0e2b1dbda5f19f2751d78a8662f2094fa315e4da054e83093ffb64ccf6b8dd"}, + {file = "dependency_injector-4.45.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c12c44c8c5a168059cff22a0e9a9f7f3ff089b0b38114d037ca1ead48ead178d"}, + {file = "dependency_injector-4.45.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:60de7a0a8bd105b2e11121f81a3e5ec67fce34d2470eb7af2546bca2df76b101"}, + {file = "dependency_injector-4.45.0-cp39-cp39-win32.whl", hash = "sha256:8d351a5f36bc04838c4884acbc5f57315e51437a19f2a03d225a3af9bac1f60c"}, + {file = "dependency_injector-4.45.0-cp39-cp39-win_amd64.whl", hash = "sha256:4f556713c34335afaec011e3e436668686af90605992415be60aea1d625bf91b"}, + {file = "dependency_injector-4.45.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f2cd876d9726db63df33ecbfb097a06de0306ddae5861c6ba71363606184428e"}, + {file = "dependency_injector-4.45.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f35baac99140143aed5f7dbf098c523fc534884063ccacc3f51aaccbacc7f9f3"}, + {file = "dependency_injector-4.45.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c76c944e4732d72787f99b18dd5b3bcb012427efe8373d8b895f9774880f0cc"}, + {file = "dependency_injector-4.45.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:beabb619969c1c87085bcb228b10e2f8751bf7cca66d77dac99c85c3b7a29847"}, + {file = "dependency_injector-4.45.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d2bdf7c6e4f7a74dedf4f0aea774eec4c3d6f4f909d411c769c9cd1346243518"}, + {file = "dependency_injector-4.45.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:713645e6c5e79e7a4a574c6274acf203bf93e34d0fb1d38cf63a7f7b3142ab00"}, + {file = "dependency_injector-4.45.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3018a9882364210b55644d7fe4044f3970f1ead30603859b15baf1fd1efe53f7"}, + {file = "dependency_injector-4.45.0-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06bf830625e647cb84ace9662b205b48be394bfab725c3be871a1aa7a20755f6"}, + {file = "dependency_injector-4.45.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:cf13a5ce71200568b5bed4fa8898291296115c43b91062d2154f69016ef0d4c2"}, + {file = "dependency_injector-4.45.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a77e1815efe7e685a5fe44201f725669010f1d2f6b3034261230dc2cb036371"}, + {file = "dependency_injector-4.45.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d086df3696331908d97b74ce4bbb8f8f86ce231cc3bde7a080c3f535944e1966"}, + {file = "dependency_injector-4.45.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acda1553bfc47aefc12afdc88418d480c8fc50914c2d36392e0412cdbb267318"}, + {file = "dependency_injector-4.45.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7afd6825f926c3506ea85ab5dab7460bf9fd06bbaeee164faa205b0eb0db1efe"}, + {file = "dependency_injector-4.45.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4b4a1618eff7dba76f155eb0754ad5c3910b1cb300c34c5d667f50cb30c19117"}, + {file = "dependency_injector-4.45.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:2e6861b14a52b8ab0956bde2379aad43a5f05ddc8dbfb0233c6c4f71bc029481"}, + {file = "dependency_injector-4.45.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60034184b6dedf7d0486c565eb846a56ccc6fdb25511f1bedf0fbbb7dab23239"}, + {file = "dependency_injector-4.45.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dae95379e1a55cccef583d717f2a18481001fe9519162e480d1e2c3e429c246a"}, + {file = "dependency_injector-4.45.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d35c638544a929bfaf150e280b582bd776e3929b9ffdffa4c7a9ee9dff9f448e"}, + {file = "dependency_injector-4.45.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d1cd1034fdc8c4c9daeafdab146570260d5c8fe70fa87a2d428621fc5a07ea8c"}, + {file = "dependency_injector-4.45.0.tar.gz", hash = "sha256:7effdb9e45f5c2813e2fd9dc2ef2c446dd59716f66f9c772ab27b9ed16efc894"}, ] -[package.dependencies] -six = ">=1.7.0,<=1.16.0" - [package.extras] aiohttp = ["aiohttp"] flask = ["flask"] pydantic = ["pydantic"] +pydantic2 = ["pydantic-settings"] yaml = ["pyyaml"] [[package]] @@ -1115,13 +1090,13 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth [[package]] name = "fastjsonschema" -version = "2.20.0" +version = "2.21.1" description = "Fastest Python implementation of JSON schema" optional = false python-versions = "*" files = [ - {file = "fastjsonschema-2.20.0-py3-none-any.whl", hash = "sha256:5875f0b0fa7a0043a91e93a9b8f793bcbbba9691e7fd83dca95c28ba26d21f0a"}, - {file = "fastjsonschema-2.20.0.tar.gz", hash = "sha256:3d48fc5300ee96f5d116f10fe6f28d938e6008f59a6a025c2649475b87f76a23"}, + {file = "fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667"}, + {file = "fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4"}, ] [package.extras] @@ -1455,13 +1430,13 @@ trio = ["trio (>=0.22.0,<1.0)"] [[package]] name = "httpx" -version = "0.27.2" +version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, - {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] [package.dependencies] @@ -1469,7 +1444,6 @@ anyio = "*" certifi = "*" httpcore = "==1.*" idna = "*" -sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] @@ -1527,13 +1501,13 @@ test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0)", "pytest-asyncio [[package]] name = "ipython" -version = "8.29.0" +version = "8.31.0" description = "IPython: Productive Interactive Computing" optional = false python-versions = ">=3.10" files = [ - {file = "ipython-8.29.0-py3-none-any.whl", hash = "sha256:0188a1bd83267192123ccea7f4a8ed0a78910535dbaa3f37671dca76ebd429c8"}, - {file = "ipython-8.29.0.tar.gz", hash = "sha256:40b60e15b22591450eef73e40a027cf77bd652e757523eebc5bd7c7c498290eb"}, + {file = "ipython-8.31.0-py3-none-any.whl", hash = "sha256:46ec58f8d3d076a61d128fe517a51eb730e3aaf0c184ea8c17d16e366660c6a6"}, + {file = "ipython-8.31.0.tar.gz", hash = "sha256:b6a2274606bec6166405ff05e54932ed6e5cfecaca1fc05f2cacde7bb074d70b"}, ] [package.dependencies] @@ -1543,16 +1517,16 @@ exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} jedi = ">=0.16" matplotlib-inline = "*" pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} -prompt-toolkit = ">=3.0.41,<3.1.0" +prompt_toolkit = ">=3.0.41,<3.1.0" pygments = ">=2.4.0" -stack-data = "*" +stack_data = "*" traitlets = ">=5.13.0" -typing-extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} +typing_extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} [package.extras] all = ["ipython[black,doc,kernel,matplotlib,nbconvert,nbformat,notebook,parallel,qtconsole]", "ipython[test,test-extra]"] black = ["black"] -doc = ["docrepr", "exceptiongroup", "intersphinx-registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli", "typing-extensions"] +doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinxcontrib-jquery", "tomli", "typing_extensions"] kernel = ["ipykernel"] matplotlib = ["matplotlib"] nbconvert = ["nbconvert"] @@ -1641,13 +1615,13 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] [[package]] name = "jinja2" -version = "3.1.4" +version = "3.1.5" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"}, - {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"}, + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, ] [package.dependencies] @@ -1658,13 +1632,13 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "json5" -version = "0.9.28" +version = "0.10.0" description = "A Python implementation of the JSON5 data format." optional = false python-versions = ">=3.8.0" files = [ - {file = "json5-0.9.28-py3-none-any.whl", hash = "sha256:29c56f1accdd8bc2e037321237662034a7e07921e2b7223281a5ce2c46f0c4df"}, - {file = "json5-0.9.28.tar.gz", hash = "sha256:1f82f36e615bc5b42f1bbd49dbc94b12563c56408c6ffa06414ea310890e9a6e"}, + {file = "json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa"}, + {file = "json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559"}, ] [package.extras] @@ -1811,13 +1785,13 @@ test = ["ipykernel", "pre-commit", "pytest (<8)", "pytest-cov", "pytest-timeout" [[package]] name = "jupyter-events" -version = "0.10.0" +version = "0.11.0" description = "Jupyter Event System library" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "jupyter_events-0.10.0-py3-none-any.whl", hash = "sha256:4b72130875e59d57716d327ea70d3ebc3af1944d3717e5a498b8a06c6c159960"}, - {file = "jupyter_events-0.10.0.tar.gz", hash = "sha256:670b8229d3cc882ec782144ed22e0d29e1c2d639263f92ca8383e66682845e22"}, + {file = "jupyter_events-0.11.0-py3-none-any.whl", hash = "sha256:36399b41ce1ca45fe8b8271067d6a140ffa54cec4028e95491c93b78a855cacf"}, + {file = "jupyter_events-0.11.0.tar.gz", hash = "sha256:c0bc56a37aac29c1fbc3bcfbddb8c8c49533f9cf11f1c4e6adadba936574ab90"}, ] [package.dependencies] @@ -1831,7 +1805,7 @@ traitlets = ">=5.3" [package.extras] cli = ["click", "rich"] -docs = ["jupyterlite-sphinx", "myst-parser", "pydata-sphinx-theme", "sphinxcontrib-spelling"] +docs = ["jupyterlite-sphinx", "myst-parser", "pydata-sphinx-theme (>=0.16)", "sphinx (>=8)", "sphinxcontrib-spelling"] test = ["click", "pre-commit", "pytest (>=7.0)", "pytest-asyncio (>=0.19.0)", "pytest-console-scripts", "rich"] [[package]] @@ -1850,13 +1824,13 @@ jupyter-server = ">=1.1.2" [[package]] name = "jupyter-server" -version = "2.14.2" +version = "2.15.0" description = "The backend—i.e. core services, APIs, and REST endpoints—to Jupyter web applications." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "jupyter_server-2.14.2-py3-none-any.whl", hash = "sha256:47ff506127c2f7851a17bf4713434208fc490955d0e8632e95014a9a9afbeefd"}, - {file = "jupyter_server-2.14.2.tar.gz", hash = "sha256:66095021aa9638ced276c248b1d81862e4c50f292d575920bbe960de1c56b12b"}, + {file = "jupyter_server-2.15.0-py3-none-any.whl", hash = "sha256:872d989becf83517012ee669f09604aa4a28097c0bd90b2f424310156c2cdae3"}, + {file = "jupyter_server-2.15.0.tar.gz", hash = "sha256:9d446b8697b4f7337a1b7cdcac40778babdd93ba614b6d68ab1c0c918f1c4084"}, ] [package.dependencies] @@ -1865,7 +1839,7 @@ argon2-cffi = ">=21.1" jinja2 = ">=3.0.3" jupyter-client = ">=7.4.4" jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" -jupyter-events = ">=0.9.0" +jupyter-events = ">=0.11.0" jupyter-server-terminals = ">=0.4.4" nbconvert = ">=6.4.4" nbformat = ">=5.3.0" @@ -1905,13 +1879,13 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (> [[package]] name = "jupyterlab" -version = "4.3.1" +version = "4.3.4" description = "JupyterLab computational environment" optional = false python-versions = ">=3.8" files = [ - {file = "jupyterlab-4.3.1-py3-none-any.whl", hash = "sha256:2d9a1c305bc748e277819a17a5d5e22452e533e835f4237b2f30f3b0e491e01f"}, - {file = "jupyterlab-4.3.1.tar.gz", hash = "sha256:a4a338327556443521731d82f2a6ccf926df478914ca029616621704d47c3c65"}, + {file = "jupyterlab-4.3.4-py3-none-any.whl", hash = "sha256:b754c2601c5be6adf87cb5a1d8495d653ffb945f021939f77776acaa94dae952"}, + {file = "jupyterlab-4.3.4.tar.gz", hash = "sha256:f0bb9b09a04766e3423cccc2fc23169aa2ffedcdf8713e9e0fb33cac0b6859d0"}, ] [package.dependencies] @@ -1925,7 +1899,7 @@ jupyter-server = ">=2.4.0,<3" jupyterlab-server = ">=2.27.1,<3" notebook-shim = ">=0.2" packaging = "*" -setuptools = ">=40.1.0" +setuptools = ">=40.8.0" tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} tornado = ">=6.2.0" traitlets = "*" @@ -1986,13 +1960,13 @@ files = [ [[package]] name = "mako" -version = "1.3.6" +version = "1.3.8" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." optional = false python-versions = ">=3.8" files = [ - {file = "Mako-1.3.6-py3-none-any.whl", hash = "sha256:a91198468092a2f1a0de86ca92690fb0cfc43ca90ee17e15d93662b4c04b241a"}, - {file = "mako-1.3.6.tar.gz", hash = "sha256:9ec3a1583713479fae654f83ed9fa8c9a4c16b7bb0daba0e6bbebff50c0d983d"}, + {file = "Mako-1.3.8-py3-none-any.whl", hash = "sha256:42f48953c7eb91332040ff567eb7eea69b22e7a4affbc5ba8e845e8f730f6627"}, + {file = "mako-1.3.8.tar.gz", hash = "sha256:577b97e414580d3e088d47c2dbbe9594aa7a5146ed2875d4dfa9075af2dd3cc8"}, ] [package.dependencies] @@ -2074,13 +2048,13 @@ files = [ [[package]] name = "marshmallow" -version = "3.23.1" +version = "3.25.0" description = "A lightweight library for converting complex datatypes to and from native Python datatypes." optional = false python-versions = ">=3.9" files = [ - {file = "marshmallow-3.23.1-py3-none-any.whl", hash = "sha256:fece2eb2c941180ea1b7fcbd4a83c51bfdd50093fdd3ad2585ee5e1df2508491"}, - {file = "marshmallow-3.23.1.tar.gz", hash = "sha256:3a8dfda6edd8dcdbf216c0ede1d1e78d230a6dc9c5a088f58c4083b974a0d468"}, + {file = "marshmallow-3.25.0-py3-none-any.whl", hash = "sha256:50894cd57c6b097a6c6ed2bf216af47d10146990a54db52d03e32edb0448c905"}, + {file = "marshmallow-3.25.0.tar.gz", hash = "sha256:5ba94a4eb68894ad6761a505eb225daf7e5cb7b4c32af62d4a45e9d42665bc31"}, ] [package.dependencies] @@ -2088,7 +2062,7 @@ packaging = ">=17.0" [package.extras] dev = ["marshmallow[tests]", "pre-commit (>=3.5,<5.0)", "tox"] -docs = ["alabaster (==1.0.0)", "autodocsumm (==0.2.14)", "sphinx (==8.1.3)", "sphinx-issues (==5.0.0)", "sphinx-version-warning (==1.1.2)"] +docs = ["alabaster (==1.0.0)", "autodocsumm (==0.2.14)", "sphinx (==8.1.3)", "sphinx-issues (==5.0.0)"] tests = ["pytest", "simplejson"] [[package]] @@ -2118,15 +2092,18 @@ files = [ [[package]] name = "mistune" -version = "3.0.2" +version = "3.1.0" description = "A sane and fast Markdown parser with useful plugins and renderers" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mistune-3.0.2-py3-none-any.whl", hash = "sha256:71481854c30fdbc938963d3605b72501f5c10a9320ecd412c121c163a1c7d205"}, - {file = "mistune-3.0.2.tar.gz", hash = "sha256:fc7f93ded930c92394ef2cb6f04a8aabab4117a91449e72dcc8dfa646a508be8"}, + {file = "mistune-3.1.0-py3-none-any.whl", hash = "sha256:b05198cf6d671b3deba6c87ec6cf0d4eb7b72c524636eddb6dbf13823b52cee1"}, + {file = "mistune-3.1.0.tar.gz", hash = "sha256:dbcac2f78292b9dc066cd03b7a3a26b62d85f8159f2ea5fd28e55df79908d667"}, ] +[package.dependencies] +typing-extensions = {version = "*", markers = "python_version < \"3.11\""} + [[package]] name = "msal" version = "1.31.1" @@ -2267,13 +2244,13 @@ typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} [[package]] name = "nbclient" -version = "0.10.0" +version = "0.10.2" description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.9.0" files = [ - {file = "nbclient-0.10.0-py3-none-any.whl", hash = "sha256:f13e3529332a1f1f81d82a53210322476a168bb7090a0289c795fe9cc11c9d3f"}, - {file = "nbclient-0.10.0.tar.gz", hash = "sha256:4b3f1b7dba531e498449c4db4f53da339c91d449dc11e9af3a43b4eb5c5abb09"}, + {file = "nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d"}, + {file = "nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193"}, ] [package.dependencies] @@ -2284,23 +2261,23 @@ traitlets = ">=5.4" [package.extras] dev = ["pre-commit"] -docs = ["autodoc-traits", "mock", "moto", "myst-parser", "nbclient[test]", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling"] -test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.0.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] +docs = ["autodoc-traits", "flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "mock", "moto", "myst-parser", "nbconvert (>=7.1.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling", "testpath", "xmltodict"] +test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.1.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] [[package]] name = "nbconvert" -version = "7.16.4" +version = "7.16.5" description = "Converting Jupyter Notebooks (.ipynb files) to other formats. Output formats include asciidoc, html, latex, markdown, pdf, py, rst, script. nbconvert can be used both as a Python library (`import nbconvert`) or as a command line tool (invoked as `jupyter nbconvert ...`)." optional = false python-versions = ">=3.8" files = [ - {file = "nbconvert-7.16.4-py3-none-any.whl", hash = "sha256:05873c620fe520b6322bf8a5ad562692343fe3452abda5765c7a34b7d1aa3eb3"}, - {file = "nbconvert-7.16.4.tar.gz", hash = "sha256:86ca91ba266b0a448dc96fa6c5b9d98affabde2867b363258703536807f9f7f4"}, + {file = "nbconvert-7.16.5-py3-none-any.whl", hash = "sha256:e12eac052d6fd03040af4166c563d76e7aeead2e9aadf5356db552a1784bd547"}, + {file = "nbconvert-7.16.5.tar.gz", hash = "sha256:c83467bb5777fdfaac5ebbb8e864f300b277f68692ecc04d6dab72f2d8442344"}, ] [package.dependencies] beautifulsoup4 = "*" -bleach = "!=5.0.0" +bleach = {version = "!=5.0.0", extras = ["css"]} defusedxml = "*" jinja2 = ">=3.0" jupyter-core = ">=4.7" @@ -2312,7 +2289,6 @@ nbformat = ">=5.7" packaging = "*" pandocfilters = ">=1.4.1" pygments = ">=2.4.1" -tinycss2 = "*" traitlets = ">=5.1" [package.extras] @@ -2358,26 +2334,26 @@ files = [ [[package]] name = "notebook" -version = "7.0.7" +version = "7.3.2" description = "Jupyter Notebook - A web-based notebook environment for interactive computing" optional = false python-versions = ">=3.8" files = [ - {file = "notebook-7.0.7-py3-none-any.whl", hash = "sha256:289b606d7e173f75a18beb1406ef411b43f97f7a9c55ba03efa3622905a62346"}, - {file = "notebook-7.0.7.tar.gz", hash = "sha256:3bcff00c17b3ac142ef5f436d50637d936b274cfa0b41f6ac0175363de9b4e09"}, + {file = "notebook-7.3.2-py3-none-any.whl", hash = "sha256:e5f85fc59b69d3618d73cf27544418193ff8e8058d5bf61d315ce4f473556288"}, + {file = "notebook-7.3.2.tar.gz", hash = "sha256:705e83a1785f45b383bf3ee13cb76680b92d24f56fb0c7d2136fe1d850cd3ca8"}, ] [package.dependencies] jupyter-server = ">=2.4.0,<3" -jupyterlab = ">=4.0.2,<5" -jupyterlab-server = ">=2.22.1,<3" +jupyterlab = ">=4.3.4,<4.4" +jupyterlab-server = ">=2.27.1,<3" notebook-shim = ">=0.2,<0.3" tornado = ">=6.2.0" [package.extras] dev = ["hatch", "pre-commit"] docs = ["myst-parser", "nbsphinx", "pydata-sphinx-theme", "sphinx (>=1.3.6)", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] -test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.22.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"] +test = ["importlib-resources (>=5.0)", "ipykernel", "jupyter-server[test] (>=2.4.0,<3)", "jupyterlab-server[test] (>=2.27.1,<3)", "nbval", "pytest (>=7.0)", "pytest-console-scripts", "pytest-timeout", "pytest-tornasync", "requests"] [[package]] name = "notebook-shim" @@ -2398,66 +2374,66 @@ test = ["pytest", "pytest-console-scripts", "pytest-jupyter", "pytest-tornasync" [[package]] name = "numpy" -version = "2.1.3" +version = "2.2.1" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.10" files = [ - {file = "numpy-2.1.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c894b4305373b9c5576d7a12b473702afdf48ce5369c074ba304cc5ad8730dff"}, - {file = "numpy-2.1.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b47fbb433d3260adcd51eb54f92a2ffbc90a4595f8970ee00e064c644ac788f5"}, - {file = "numpy-2.1.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:825656d0743699c529c5943554d223c021ff0494ff1442152ce887ef4f7561a1"}, - {file = "numpy-2.1.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:6a4825252fcc430a182ac4dee5a505053d262c807f8a924603d411f6718b88fd"}, - {file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e711e02f49e176a01d0349d82cb5f05ba4db7d5e7e0defd026328e5cfb3226d3"}, - {file = "numpy-2.1.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78574ac2d1a4a02421f25da9559850d59457bac82f2b8d7a44fe83a64f770098"}, - {file = "numpy-2.1.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c7662f0e3673fe4e832fe07b65c50342ea27d989f92c80355658c7f888fcc83c"}, - {file = "numpy-2.1.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fa2d1337dc61c8dc417fbccf20f6d1e139896a30721b7f1e832b2bb6ef4eb6c4"}, - {file = "numpy-2.1.3-cp310-cp310-win32.whl", hash = "sha256:72dcc4a35a8515d83e76b58fdf8113a5c969ccd505c8a946759b24e3182d1f23"}, - {file = "numpy-2.1.3-cp310-cp310-win_amd64.whl", hash = "sha256:ecc76a9ba2911d8d37ac01de72834d8849e55473457558e12995f4cd53e778e0"}, - {file = "numpy-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d"}, - {file = "numpy-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41"}, - {file = "numpy-2.1.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9"}, - {file = "numpy-2.1.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09"}, - {file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a"}, - {file = "numpy-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b"}, - {file = "numpy-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee"}, - {file = "numpy-2.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0"}, - {file = "numpy-2.1.3-cp311-cp311-win32.whl", hash = "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9"}, - {file = "numpy-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2"}, - {file = "numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e"}, - {file = "numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958"}, - {file = "numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8"}, - {file = "numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564"}, - {file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512"}, - {file = "numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b"}, - {file = "numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc"}, - {file = "numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0"}, - {file = "numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9"}, - {file = "numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a"}, - {file = "numpy-2.1.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96fe52fcdb9345b7cd82ecd34547fca4321f7656d500eca497eb7ea5a926692f"}, - {file = "numpy-2.1.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f653490b33e9c3a4c1c01d41bc2aef08f9475af51146e4a7710c450cf9761598"}, - {file = "numpy-2.1.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dc258a761a16daa791081d026f0ed4399b582712e6fc887a95af09df10c5ca57"}, - {file = "numpy-2.1.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:016d0f6f5e77b0f0d45d77387ffa4bb89816b57c835580c3ce8e099ef830befe"}, - {file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c181ba05ce8299c7aa3125c27b9c2167bca4a4445b7ce73d5febc411ca692e43"}, - {file = "numpy-2.1.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5641516794ca9e5f8a4d17bb45446998c6554704d888f86df9b200e66bdcce56"}, - {file = "numpy-2.1.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ea4dedd6e394a9c180b33c2c872b92f7ce0f8e7ad93e9585312b0c5a04777a4a"}, - {file = "numpy-2.1.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0df3635b9c8ef48bd3be5f862cf71b0a4716fa0e702155c45067c6b711ddcef"}, - {file = "numpy-2.1.3-cp313-cp313-win32.whl", hash = "sha256:50ca6aba6e163363f132b5c101ba078b8cbd3fa92c7865fd7d4d62d9779ac29f"}, - {file = "numpy-2.1.3-cp313-cp313-win_amd64.whl", hash = "sha256:747641635d3d44bcb380d950679462fae44f54b131be347d5ec2bce47d3df9ed"}, - {file = "numpy-2.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:996bb9399059c5b82f76b53ff8bb686069c05acc94656bb259b1d63d04a9506f"}, - {file = "numpy-2.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:45966d859916ad02b779706bb43b954281db43e185015df6eb3323120188f9e4"}, - {file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:baed7e8d7481bfe0874b566850cb0b85243e982388b7b23348c6db2ee2b2ae8e"}, - {file = "numpy-2.1.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f7f672a3388133335589cfca93ed468509cb7b93ba3105fce780d04a6576a0"}, - {file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7aac50327da5d208db2eec22eb11e491e3fe13d22653dce51b0f4109101b408"}, - {file = "numpy-2.1.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4394bc0dbd074b7f9b52024832d16e019decebf86caf909d94f6b3f77a8ee3b6"}, - {file = "numpy-2.1.3-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:50d18c4358a0a8a53f12a8ba9d772ab2d460321e6a93d6064fc22443d189853f"}, - {file = "numpy-2.1.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:14e253bd43fc6b37af4921b10f6add6925878a42a0c5fe83daee390bca80bc17"}, - {file = "numpy-2.1.3-cp313-cp313t-win32.whl", hash = "sha256:08788d27a5fd867a663f6fc753fd7c3ad7e92747efc73c53bca2f19f8bc06f48"}, - {file = "numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4"}, - {file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4f2015dfe437dfebbfce7c85c7b53d81ba49e71ba7eadbf1df40c915af75979f"}, - {file = "numpy-2.1.3-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3522b0dfe983a575e6a9ab3a4a4dfe156c3e428468ff08ce582b9bb6bd1d71d4"}, - {file = "numpy-2.1.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c006b607a865b07cd981ccb218a04fc86b600411d83d6fc261357f1c0966755d"}, - {file = "numpy-2.1.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e14e26956e6f1696070788252dcdff11b4aca4c3e8bd166e0df1bb8f315a67cb"}, - {file = "numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761"}, + {file = "numpy-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5edb4e4caf751c1518e6a26a83501fda79bff41cc59dac48d70e6d65d4ec4440"}, + {file = "numpy-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa3017c40d513ccac9621a2364f939d39e550c542eb2a894b4c8da92b38896ab"}, + {file = "numpy-2.2.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:61048b4a49b1c93fe13426e04e04fdf5a03f456616f6e98c7576144677598675"}, + {file = "numpy-2.2.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:7671dc19c7019103ca44e8d94917eba8534c76133523ca8406822efdd19c9308"}, + {file = "numpy-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4250888bcb96617e00bfa28ac24850a83c9f3a16db471eca2ee1f1714df0f957"}, + {file = "numpy-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7746f235c47abc72b102d3bce9977714c2444bdfaea7888d241b4c4bb6a78bf"}, + {file = "numpy-2.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:059e6a747ae84fce488c3ee397cee7e5f905fd1bda5fb18c66bc41807ff119b2"}, + {file = "numpy-2.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f62aa6ee4eb43b024b0e5a01cf65a0bb078ef8c395e8713c6e8a12a697144528"}, + {file = "numpy-2.2.1-cp310-cp310-win32.whl", hash = "sha256:48fd472630715e1c1c89bf1feab55c29098cb403cc184b4859f9c86d4fcb6a95"}, + {file = "numpy-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:b541032178a718c165a49638d28272b771053f628382d5e9d1c93df23ff58dbf"}, + {file = "numpy-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40f9e544c1c56ba8f1cf7686a8c9b5bb249e665d40d626a23899ba6d5d9e1484"}, + {file = "numpy-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f9b57eaa3b0cd8db52049ed0330747b0364e899e8a606a624813452b8203d5f7"}, + {file = "numpy-2.2.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bc8a37ad5b22c08e2dbd27df2b3ef7e5c0864235805b1e718a235bcb200cf1cb"}, + {file = "numpy-2.2.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:9036d6365d13b6cbe8f27a0eaf73ddcc070cae584e5ff94bb45e3e9d729feab5"}, + {file = "numpy-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51faf345324db860b515d3f364eaa93d0e0551a88d6218a7d61286554d190d73"}, + {file = "numpy-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38efc1e56b73cc9b182fe55e56e63b044dd26a72128fd2fbd502f75555d92591"}, + {file = "numpy-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:31b89fa67a8042e96715c68e071a1200c4e172f93b0fbe01a14c0ff3ff820fc8"}, + {file = "numpy-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c86e2a209199ead7ee0af65e1d9992d1dce7e1f63c4b9a616500f93820658d0"}, + {file = "numpy-2.2.1-cp311-cp311-win32.whl", hash = "sha256:b34d87e8a3090ea626003f87f9392b3929a7bbf4104a05b6667348b6bd4bf1cd"}, + {file = "numpy-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:360137f8fb1b753c5cde3ac388597ad680eccbbbb3865ab65efea062c4a1fd16"}, + {file = "numpy-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:694f9e921a0c8f252980e85bce61ebbd07ed2b7d4fa72d0e4246f2f8aa6642ab"}, + {file = "numpy-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3683a8d166f2692664262fd4900f207791d005fb088d7fdb973cc8d663626faa"}, + {file = "numpy-2.2.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:780077d95eafc2ccc3ced969db22377b3864e5b9a0ea5eb347cc93b3ea900315"}, + {file = "numpy-2.2.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:55ba24ebe208344aa7a00e4482f65742969a039c2acfcb910bc6fcd776eb4355"}, + {file = "numpy-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b1d07b53b78bf84a96898c1bc139ad7f10fda7423f5fd158fd0f47ec5e01ac7"}, + {file = "numpy-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5062dc1a4e32a10dc2b8b13cedd58988261416e811c1dc4dbdea4f57eea61b0d"}, + {file = "numpy-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:fce4f615f8ca31b2e61aa0eb5865a21e14f5629515c9151850aa936c02a1ee51"}, + {file = "numpy-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:67d4cda6fa6ffa073b08c8372aa5fa767ceb10c9a0587c707505a6d426f4e046"}, + {file = "numpy-2.2.1-cp312-cp312-win32.whl", hash = "sha256:32cb94448be47c500d2c7a95f93e2f21a01f1fd05dd2beea1ccd049bb6001cd2"}, + {file = "numpy-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:ba5511d8f31c033a5fcbda22dd5c813630af98c70b2661f2d2c654ae3cdfcfc8"}, + {file = "numpy-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f1d09e520217618e76396377c81fba6f290d5f926f50c35f3a5f72b01a0da780"}, + {file = "numpy-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ecc47cd7f6ea0336042be87d9e7da378e5c7e9b3c8ad0f7c966f714fc10d821"}, + {file = "numpy-2.2.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f419290bc8968a46c4933158c91a0012b7a99bb2e465d5ef5293879742f8797e"}, + {file = "numpy-2.2.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5b6c390bfaef8c45a260554888966618328d30e72173697e5cabe6b285fb2348"}, + {file = "numpy-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:526fc406ab991a340744aad7e25251dd47a6720a685fa3331e5c59fef5282a59"}, + {file = "numpy-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f74e6fdeb9a265624ec3a3918430205dff1df7e95a230779746a6af78bc615af"}, + {file = "numpy-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:53c09385ff0b72ba79d8715683c1168c12e0b6e84fb0372e97553d1ea91efe51"}, + {file = "numpy-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f3eac17d9ec51be534685ba877b6ab5edc3ab7ec95c8f163e5d7b39859524716"}, + {file = "numpy-2.2.1-cp313-cp313-win32.whl", hash = "sha256:9ad014faa93dbb52c80d8f4d3dcf855865c876c9660cb9bd7553843dd03a4b1e"}, + {file = "numpy-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:164a829b6aacf79ca47ba4814b130c4020b202522a93d7bff2202bfb33b61c60"}, + {file = "numpy-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4dfda918a13cc4f81e9118dea249e192ab167a0bb1966272d5503e39234d694e"}, + {file = "numpy-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:733585f9f4b62e9b3528dd1070ec4f52b8acf64215b60a845fa13ebd73cd0712"}, + {file = "numpy-2.2.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:89b16a18e7bba224ce5114db863e7029803c179979e1af6ad6a6b11f70545008"}, + {file = "numpy-2.2.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:676f4eebf6b2d430300f1f4f4c2461685f8269f94c89698d832cdf9277f30b84"}, + {file = "numpy-2.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f5cdf9f493b35f7e41e8368e7d7b4bbafaf9660cba53fb21d2cd174ec09631"}, + {file = "numpy-2.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1ad395cf254c4fbb5b2132fee391f361a6e8c1adbd28f2cd8e79308a615fe9d"}, + {file = "numpy-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08ef779aed40dbc52729d6ffe7dd51df85796a702afbf68a4f4e41fafdc8bda5"}, + {file = "numpy-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:26c9c4382b19fcfbbed3238a14abf7ff223890ea1936b8890f058e7ba35e8d71"}, + {file = "numpy-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:93cf4e045bae74c90ca833cba583c14b62cb4ba2cba0abd2b141ab52548247e2"}, + {file = "numpy-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bff7d8ec20f5f42607599f9994770fa65d76edca264a87b5e4ea5629bce12268"}, + {file = "numpy-2.2.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7ba9cc93a91d86365a5d270dee221fdc04fb68d7478e6bf6af650de78a8339e3"}, + {file = "numpy-2.2.1-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:3d03883435a19794e41f147612a77a8f56d4e52822337844fff3d4040a142964"}, + {file = "numpy-2.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4511d9e6071452b944207c8ce46ad2f897307910b402ea5fa975da32e0102800"}, + {file = "numpy-2.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5c5cc0cbabe9452038ed984d05ac87910f89370b9242371bd9079cb4af61811e"}, + {file = "numpy-2.2.1.tar.gz", hash = "sha256:45681fd7128c8ad1c379f0ca0776a8b0c6583d2f69889ddac01559dfe4390918"}, ] [[package]] @@ -2704,13 +2680,13 @@ tests = ["pytest (>=5.4.1)", "pytest-cov (>=2.8.1)", "pytest-mypy (>=0.8.0)", "p [[package]] name = "prometheus-client" -version = "0.21.0" +version = "0.21.1" description = "Python client for the Prometheus monitoring system." optional = false python-versions = ">=3.8" files = [ - {file = "prometheus_client-0.21.0-py3-none-any.whl", hash = "sha256:4fa6b4dd0ac16d58bb587c04b1caae65b8c5043e85f778f42f5f632f6af2e166"}, - {file = "prometheus_client-0.21.0.tar.gz", hash = "sha256:96c83c606b71ff2b0a433c98889d275f51ffec6c5e267de37c7a2b5c9aa9233e"}, + {file = "prometheus_client-0.21.1-py3-none-any.whl", hash = "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301"}, + {file = "prometheus_client-0.21.1.tar.gz", hash = "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb"}, ] [package.extras] @@ -2732,139 +2708,123 @@ wcwidth = "*" [[package]] name = "propcache" -version = "0.2.0" +version = "0.2.1" description = "Accelerated property cache" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:c5869b8fd70b81835a6f187c5fdbe67917a04d7e52b6e7cc4e5fe39d55c39d58"}, - {file = "propcache-0.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:952e0d9d07609d9c5be361f33b0d6d650cd2bae393aabb11d9b719364521984b"}, - {file = "propcache-0.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:33ac8f098df0585c0b53009f039dfd913b38c1d2edafed0cedcc0c32a05aa110"}, - {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:97e48e8875e6c13909c800fa344cd54cc4b2b0db1d5f911f840458a500fde2c2"}, - {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:388f3217649d6d59292b722d940d4d2e1e6a7003259eb835724092a1cca0203a"}, - {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f571aea50ba5623c308aa146eb650eebf7dbe0fd8c5d946e28343cb3b5aad577"}, - {file = "propcache-0.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3dfafb44f7bb35c0c06eda6b2ab4bfd58f02729e7c4045e179f9a861b07c9850"}, - {file = "propcache-0.2.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a3ebe9a75be7ab0b7da2464a77bb27febcb4fab46a34f9288f39d74833db7f61"}, - {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d2f0d0f976985f85dfb5f3d685697ef769faa6b71993b46b295cdbbd6be8cc37"}, - {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a3dc1a4b165283bd865e8f8cb5f0c64c05001e0718ed06250d8cac9bec115b48"}, - {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9e0f07b42d2a50c7dd2d8675d50f7343d998c64008f1da5fef888396b7f84630"}, - {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e63e3e1e0271f374ed489ff5ee73d4b6e7c60710e1f76af5f0e1a6117cd26394"}, - {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:56bb5c98f058a41bb58eead194b4db8c05b088c93d94d5161728515bd52b052b"}, - {file = "propcache-0.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7665f04d0c7f26ff8bb534e1c65068409bf4687aa2534faf7104d7182debb336"}, - {file = "propcache-0.2.0-cp310-cp310-win32.whl", hash = "sha256:7cf18abf9764746b9c8704774d8b06714bcb0a63641518a3a89c7f85cc02c2ad"}, - {file = "propcache-0.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:cfac69017ef97db2438efb854edf24f5a29fd09a536ff3a992b75990720cdc99"}, - {file = "propcache-0.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:63f13bf09cc3336eb04a837490b8f332e0db41da66995c9fd1ba04552e516354"}, - {file = "propcache-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608cce1da6f2672a56b24a015b42db4ac612ee709f3d29f27a00c943d9e851de"}, - {file = "propcache-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:466c219deee4536fbc83c08d09115249db301550625c7fef1c5563a584c9bc87"}, - {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc2db02409338bf36590aa985a461b2c96fce91f8e7e0f14c50c5fcc4f229016"}, - {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6ed8db0a556343d566a5c124ee483ae113acc9a557a807d439bcecc44e7dfbb"}, - {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91997d9cb4a325b60d4e3f20967f8eb08dfcb32b22554d5ef78e6fd1dda743a2"}, - {file = "propcache-0.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c7dde9e533c0a49d802b4f3f218fa9ad0a1ce21f2c2eb80d5216565202acab4"}, - {file = "propcache-0.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffcad6c564fe6b9b8916c1aefbb37a362deebf9394bd2974e9d84232e3e08504"}, - {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:97a58a28bcf63284e8b4d7b460cbee1edaab24634e82059c7b8c09e65284f178"}, - {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:945db8ee295d3af9dbdbb698cce9bbc5c59b5c3fe328bbc4387f59a8a35f998d"}, - {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:39e104da444a34830751715f45ef9fc537475ba21b7f1f5b0f4d71a3b60d7fe2"}, - {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:c5ecca8f9bab618340c8e848d340baf68bcd8ad90a8ecd7a4524a81c1764b3db"}, - {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:c436130cc779806bdf5d5fae0d848713105472b8566b75ff70048c47d3961c5b"}, - {file = "propcache-0.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:191db28dc6dcd29d1a3e063c3be0b40688ed76434622c53a284e5427565bbd9b"}, - {file = "propcache-0.2.0-cp311-cp311-win32.whl", hash = "sha256:5f2564ec89058ee7c7989a7b719115bdfe2a2fb8e7a4543b8d1c0cc4cf6478c1"}, - {file = "propcache-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e2e54267980349b723cff366d1e29b138b9a60fa376664a157a342689553f71"}, - {file = "propcache-0.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ee7606193fb267be4b2e3b32714f2d58cad27217638db98a60f9efb5efeccc2"}, - {file = "propcache-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ee8fc02ca52e24bcb77b234f22afc03288e1dafbb1f88fe24db308910c4ac7"}, - {file = "propcache-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2e900bad2a8456d00a113cad8c13343f3b1f327534e3589acc2219729237a2e8"}, - {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f52a68c21363c45297aca15561812d542f8fc683c85201df0bebe209e349f793"}, - {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e41d67757ff4fbc8ef2af99b338bfb955010444b92929e9e55a6d4dcc3c4f09"}, - {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a64e32f8bd94c105cc27f42d3b658902b5bcc947ece3c8fe7bc1b05982f60e89"}, - {file = "propcache-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:55346705687dbd7ef0d77883ab4f6fabc48232f587925bdaf95219bae072491e"}, - {file = "propcache-0.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00181262b17e517df2cd85656fcd6b4e70946fe62cd625b9d74ac9977b64d8d9"}, - {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6994984550eaf25dd7fc7bd1b700ff45c894149341725bb4edc67f0ffa94efa4"}, - {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:56295eb1e5f3aecd516d91b00cfd8bf3a13991de5a479df9e27dd569ea23959c"}, - {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:439e76255daa0f8151d3cb325f6dd4a3e93043e6403e6491813bcaaaa8733887"}, - {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f6475a1b2ecb310c98c28d271a30df74f9dd436ee46d09236a6b750a7599ce57"}, - {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3444cdba6628accf384e349014084b1cacd866fbb88433cd9d279d90a54e0b23"}, - {file = "propcache-0.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a9d9b4d0a9b38d1c391bb4ad24aa65f306c6f01b512e10a8a34a2dc5675d348"}, - {file = "propcache-0.2.0-cp312-cp312-win32.whl", hash = "sha256:69d3a98eebae99a420d4b28756c8ce6ea5a29291baf2dc9ff9414b42676f61d5"}, - {file = "propcache-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ad9c9b99b05f163109466638bd30ada1722abb01bbb85c739c50b6dc11f92dc3"}, - {file = "propcache-0.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ecddc221a077a8132cf7c747d5352a15ed763b674c0448d811f408bf803d9ad7"}, - {file = "propcache-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0e53cb83fdd61cbd67202735e6a6687a7b491c8742dfc39c9e01e80354956763"}, - {file = "propcache-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92fe151145a990c22cbccf9ae15cae8ae9eddabfc949a219c9f667877e40853d"}, - {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6a21ef516d36909931a2967621eecb256018aeb11fc48656e3257e73e2e247a"}, - {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f88a4095e913f98988f5b338c1d4d5d07dbb0b6bad19892fd447484e483ba6b"}, - {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a5b3bb545ead161be780ee85a2b54fdf7092815995661947812dde94a40f6fb"}, - {file = "propcache-0.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67aeb72e0f482709991aa91345a831d0b707d16b0257e8ef88a2ad246a7280bf"}, - {file = "propcache-0.2.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c997f8c44ec9b9b0bcbf2d422cc00a1d9b9c681f56efa6ca149a941e5560da2"}, - {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2a66df3d4992bc1d725b9aa803e8c5a66c010c65c741ad901e260ece77f58d2f"}, - {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3ebbcf2a07621f29638799828b8d8668c421bfb94c6cb04269130d8de4fb7136"}, - {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1235c01ddaa80da8235741e80815ce381c5267f96cc49b1477fdcf8c047ef325"}, - {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3947483a381259c06921612550867b37d22e1df6d6d7e8361264b6d037595f44"}, - {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d5bed7f9805cc29c780f3aee05de3262ee7ce1f47083cfe9f77471e9d6777e83"}, - {file = "propcache-0.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4a91d44379f45f5e540971d41e4626dacd7f01004826a18cb048e7da7e96544"}, - {file = "propcache-0.2.0-cp313-cp313-win32.whl", hash = "sha256:f902804113e032e2cdf8c71015651c97af6418363bea8d78dc0911d56c335032"}, - {file = "propcache-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:8f188cfcc64fb1266f4684206c9de0e80f54622c3f22a910cbd200478aeae61e"}, - {file = "propcache-0.2.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:53d1bd3f979ed529f0805dd35ddaca330f80a9a6d90bc0121d2ff398f8ed8861"}, - {file = "propcache-0.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:83928404adf8fb3d26793665633ea79b7361efa0287dfbd372a7e74311d51ee6"}, - {file = "propcache-0.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:77a86c261679ea5f3896ec060be9dc8e365788248cc1e049632a1be682442063"}, - {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:218db2a3c297a3768c11a34812e63b3ac1c3234c3a086def9c0fee50d35add1f"}, - {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7735e82e3498c27bcb2d17cb65d62c14f1100b71723b68362872bca7d0913d90"}, - {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:20a617c776f520c3875cf4511e0d1db847a076d720714ae35ffe0df3e440be68"}, - {file = "propcache-0.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67b69535c870670c9f9b14a75d28baa32221d06f6b6fa6f77a0a13c5a7b0a5b9"}, - {file = "propcache-0.2.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4569158070180c3855e9c0791c56be3ceeb192defa2cdf6a3f39e54319e56b89"}, - {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:db47514ffdbd91ccdc7e6f8407aac4ee94cc871b15b577c1c324236b013ddd04"}, - {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:2a60ad3e2553a74168d275a0ef35e8c0a965448ffbc3b300ab3a5bb9956c2162"}, - {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:662dd62358bdeaca0aee5761de8727cfd6861432e3bb828dc2a693aa0471a563"}, - {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:25a1f88b471b3bc911d18b935ecb7115dff3a192b6fef46f0bfaf71ff4f12418"}, - {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:f60f0ac7005b9f5a6091009b09a419ace1610e163fa5deaba5ce3484341840e7"}, - {file = "propcache-0.2.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:74acd6e291f885678631b7ebc85d2d4aec458dd849b8c841b57ef04047833bed"}, - {file = "propcache-0.2.0-cp38-cp38-win32.whl", hash = "sha256:d9b6ddac6408194e934002a69bcaadbc88c10b5f38fb9307779d1c629181815d"}, - {file = "propcache-0.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:676135dcf3262c9c5081cc8f19ad55c8a64e3f7282a21266d05544450bffc3a5"}, - {file = "propcache-0.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:25c8d773a62ce0451b020c7b29a35cfbc05de8b291163a7a0f3b7904f27253e6"}, - {file = "propcache-0.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:375a12d7556d462dc64d70475a9ee5982465fbb3d2b364f16b86ba9135793638"}, - {file = "propcache-0.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1ec43d76b9677637a89d6ab86e1fef70d739217fefa208c65352ecf0282be957"}, - {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f45eec587dafd4b2d41ac189c2156461ebd0c1082d2fe7013571598abb8505d1"}, - {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc092ba439d91df90aea38168e11f75c655880c12782facf5cf9c00f3d42b562"}, - {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa1076244f54bb76e65e22cb6910365779d5c3d71d1f18b275f1dfc7b0d71b4d"}, - {file = "propcache-0.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:682a7c79a2fbf40f5dbb1eb6bfe2cd865376deeac65acf9beb607505dced9e12"}, - {file = "propcache-0.2.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8e40876731f99b6f3c897b66b803c9e1c07a989b366c6b5b475fafd1f7ba3fb8"}, - {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:363ea8cd3c5cb6679f1c2f5f1f9669587361c062e4899fce56758efa928728f8"}, - {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:140fbf08ab3588b3468932974a9331aff43c0ab8a2ec2c608b6d7d1756dbb6cb"}, - {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e70fac33e8b4ac63dfc4c956fd7d85a0b1139adcfc0d964ce288b7c527537fea"}, - {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:b33d7a286c0dc1a15f5fc864cc48ae92a846df287ceac2dd499926c3801054a6"}, - {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f6d5749fdd33d90e34c2efb174c7e236829147a2713334d708746e94c4bde40d"}, - {file = "propcache-0.2.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22aa8f2272d81d9317ff5756bb108021a056805ce63dd3630e27d042c8092798"}, - {file = "propcache-0.2.0-cp39-cp39-win32.whl", hash = "sha256:73e4b40ea0eda421b115248d7e79b59214411109a5bc47d0d48e4c73e3b8fcf9"}, - {file = "propcache-0.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:9517d5e9e0731957468c29dbfd0f976736a0e55afaea843726e887f36fe017df"}, - {file = "propcache-0.2.0-py3-none-any.whl", hash = "sha256:2ccc28197af5313706511fab3a8b66dcd6da067a1331372c82ea1cb74285e036"}, - {file = "propcache-0.2.0.tar.gz", hash = "sha256:df81779732feb9d01e5d513fad0122efb3d53bbc75f61b2a4f29a020bc985e70"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6b3f39a85d671436ee3d12c017f8fdea38509e4f25b28eb25877293c98c243f6"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:39d51fbe4285d5db5d92a929e3e21536ea3dd43732c5b177c7ef03f918dff9f2"}, + {file = "propcache-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6445804cf4ec763dc70de65a3b0d9954e868609e83850a47ca4f0cb64bd79fea"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9479aa06a793c5aeba49ce5c5692ffb51fcd9a7016e017d555d5e2b0045d212"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9631c5e8b5b3a0fda99cb0d29c18133bca1e18aea9effe55adb3da1adef80d3"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3156628250f46a0895f1f36e1d4fbe062a1af8718ec3ebeb746f1d23f0c5dc4d"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6fb63ae352e13748289f04f37868099e69dba4c2b3e271c46061e82c745634"}, + {file = "propcache-0.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:887d9b0a65404929641a9fabb6452b07fe4572b269d901d622d8a34a4e9043b2"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a96dc1fa45bd8c407a0af03b2d5218392729e1822b0c32e62c5bf7eeb5fb3958"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:a7e65eb5c003a303b94aa2c3852ef130230ec79e349632d030e9571b87c4698c"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:999779addc413181912e984b942fbcc951be1f5b3663cd80b2687758f434c583"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:19a0f89a7bb9d8048d9c4370c9c543c396e894c76be5525f5e1ad287f1750ddf"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1ac2f5fe02fa75f56e1ad473f1175e11f475606ec9bd0be2e78e4734ad575034"}, + {file = "propcache-0.2.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:574faa3b79e8ebac7cb1d7930f51184ba1ccf69adfdec53a12f319a06030a68b"}, + {file = "propcache-0.2.1-cp310-cp310-win32.whl", hash = "sha256:03ff9d3f665769b2a85e6157ac8b439644f2d7fd17615a82fa55739bc97863f4"}, + {file = "propcache-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:2d3af2e79991102678f53e0dbf4c35de99b6b8b58f29a27ca0325816364caaba"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ffc3cca89bb438fb9c95c13fc874012f7b9466b89328c3c8b1aa93cdcfadd16"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f174bbd484294ed9fdf09437f889f95807e5f229d5d93588d34e92106fbf6717"}, + {file = "propcache-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70693319e0b8fd35dd863e3e29513875eb15c51945bf32519ef52927ca883bc3"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b480c6a4e1138e1aa137c0079b9b6305ec6dcc1098a8ca5196283e8a49df95a9"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d27b84d5880f6d8aa9ae3edb253c59d9f6642ffbb2c889b78b60361eed449787"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:857112b22acd417c40fa4595db2fe28ab900c8c5fe4670c7989b1c0230955465"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf6c4150f8c0e32d241436526f3c3f9cbd34429492abddbada2ffcff506c51af"}, + {file = "propcache-0.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66d4cfda1d8ed687daa4bc0274fcfd5267873db9a5bc0418c2da19273040eeb7"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2f992c07c0fca81655066705beae35fc95a2fa7366467366db627d9f2ee097f"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4a571d97dbe66ef38e472703067021b1467025ec85707d57e78711c085984e54"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bb6178c241278d5fe853b3de743087be7f5f4c6f7d6d22a3b524d323eecec505"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad1af54a62ffe39cf34db1aa6ed1a1873bd548f6401db39d8e7cd060b9211f82"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e7048abd75fe40712005bcfc06bb44b9dfcd8e101dda2ecf2f5aa46115ad07ca"}, + {file = "propcache-0.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:160291c60081f23ee43d44b08a7e5fb76681221a8e10b3139618c5a9a291b84e"}, + {file = "propcache-0.2.1-cp311-cp311-win32.whl", hash = "sha256:819ce3b883b7576ca28da3861c7e1a88afd08cc8c96908e08a3f4dd64a228034"}, + {file = "propcache-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:edc9fc7051e3350643ad929df55c451899bb9ae6d24998a949d2e4c87fb596d3"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:081a430aa8d5e8876c6909b67bd2d937bfd531b0382d3fdedb82612c618bc41a"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2ccec9ac47cf4e04897619c0e0c1a48c54a71bdf045117d3a26f80d38ab1fb0"}, + {file = "propcache-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:14d86fe14b7e04fa306e0c43cdbeebe6b2c2156a0c9ce56b815faacc193e320d"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:049324ee97bb67285b49632132db351b41e77833678432be52bdd0289c0e05e4"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cd9a1d071158de1cc1c71a26014dcdfa7dd3d5f4f88c298c7f90ad6f27bb46d"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98110aa363f1bb4c073e8dcfaefd3a5cea0f0834c2aab23dda657e4dab2f53b5"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:647894f5ae99c4cf6bb82a1bb3a796f6e06af3caa3d32e26d2350d0e3e3faf24"}, + {file = "propcache-0.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfd3223c15bebe26518d58ccf9a39b93948d3dcb3e57a20480dfdd315356baff"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d71264a80f3fcf512eb4f18f59423fe82d6e346ee97b90625f283df56aee103f"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e73091191e4280403bde6c9a52a6999d69cdfde498f1fdf629105247599b57ec"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3935bfa5fede35fb202c4b569bb9c042f337ca4ff7bd540a0aa5e37131659348"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:f508b0491767bb1f2b87fdfacaba5f7eddc2f867740ec69ece6d1946d29029a6"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1672137af7c46662a1c2be1e8dc78cb6d224319aaa40271c9257d886be4363a6"}, + {file = "propcache-0.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b74c261802d3d2b85c9df2dfb2fa81b6f90deeef63c2db9f0e029a3cac50b518"}, + {file = "propcache-0.2.1-cp312-cp312-win32.whl", hash = "sha256:d09c333d36c1409d56a9d29b3a1b800a42c76a57a5a8907eacdbce3f18768246"}, + {file = "propcache-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:c214999039d4f2a5b2073ac506bba279945233da8c786e490d411dfc30f855c1"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aca405706e0b0a44cc6bfd41fbe89919a6a56999157f6de7e182a990c36e37bc"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:12d1083f001ace206fe34b6bdc2cb94be66d57a850866f0b908972f90996b3e9"}, + {file = "propcache-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d93f3307ad32a27bda2e88ec81134b823c240aa3abb55821a8da553eed8d9439"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba278acf14471d36316159c94a802933d10b6a1e117b8554fe0d0d9b75c9d536"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4e6281aedfca15301c41f74d7005e6e3f4ca143584ba696ac69df4f02f40d629"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b750a8e5a1262434fb1517ddf64b5de58327f1adc3524a5e44c2ca43305eb0b"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf72af5e0fb40e9babf594308911436c8efde3cb5e75b6f206c34ad18be5c052"}, + {file = "propcache-0.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2d0a12018b04f4cb820781ec0dffb5f7c7c1d2a5cd22bff7fb055a2cb19ebce"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e800776a79a5aabdb17dcc2346a7d66d0777e942e4cd251defeb084762ecd17d"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:4160d9283bd382fa6c0c2b5e017acc95bc183570cd70968b9202ad6d8fc48dce"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:30b43e74f1359353341a7adb783c8f1b1c676367b011709f466f42fda2045e95"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:58791550b27d5488b1bb52bc96328456095d96206a250d28d874fafe11b3dfaf"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0f022d381747f0dfe27e99d928e31bc51a18b65bb9e481ae0af1380a6725dd1f"}, + {file = "propcache-0.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:297878dc9d0a334358f9b608b56d02e72899f3b8499fc6044133f0d319e2ec30"}, + {file = "propcache-0.2.1-cp313-cp313-win32.whl", hash = "sha256:ddfab44e4489bd79bda09d84c430677fc7f0a4939a73d2bba3073036f487a0a6"}, + {file = "propcache-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:556fc6c10989f19a179e4321e5d678db8eb2924131e64652a51fe83e4c3db0e1"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:6a9a8c34fb7bb609419a211e59da8887eeca40d300b5ea8e56af98f6fbbb1541"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ae1aa1cd222c6d205853b3013c69cd04515f9d6ab6de4b0603e2e1c33221303e"}, + {file = "propcache-0.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:accb6150ce61c9c4b7738d45550806aa2b71c7668c6942f17b0ac182b6142fd4"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eee736daafa7af6d0a2dc15cc75e05c64f37fc37bafef2e00d77c14171c2097"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7a31fc1e1bd362874863fdeed71aed92d348f5336fd84f2197ba40c59f061bd"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba4cfa1052819d16699e1d55d18c92b6e094d4517c41dd231a8b9f87b6fa681"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f089118d584e859c62b3da0892b88a83d611c2033ac410e929cb6754eec0ed16"}, + {file = "propcache-0.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:781e65134efaf88feb447e8c97a51772aa75e48b794352f94cb7ea717dedda0d"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31f5af773530fd3c658b32b6bdc2d0838543de70eb9a2156c03e410f7b0d3aae"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:a7a078f5d37bee6690959c813977da5291b24286e7b962e62a94cec31aa5188b"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cea7daf9fc7ae6687cf1e2c049752f19f146fdc37c2cc376e7d0032cf4f25347"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b3489ff1ed1e8315674d0775dc7d2195fb13ca17b3808721b54dbe9fd020faf"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9403db39be1393618dd80c746cb22ccda168efce239c73af13c3763ef56ffc04"}, + {file = "propcache-0.2.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5d97151bc92d2b2578ff7ce779cdb9174337390a535953cbb9452fb65164c587"}, + {file = "propcache-0.2.1-cp39-cp39-win32.whl", hash = "sha256:9caac6b54914bdf41bcc91e7eb9147d331d29235a7c967c150ef5df6464fd1bb"}, + {file = "propcache-0.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:92fc4500fcb33899b05ba73276dfb684a20d31caa567b7cb5252d48f896a91b1"}, + {file = "propcache-0.2.1-py3-none-any.whl", hash = "sha256:52277518d6aae65536e9cea52d4e7fd2f7a66f4aa2d30ed3f2fcea620ace3c54"}, + {file = "propcache-0.2.1.tar.gz", hash = "sha256:3f77ce728b19cb537714499928fe800c3dda29e8d9428778fc7c186da4c09a64"}, ] [[package]] name = "psutil" -version = "6.1.0" +version = "6.1.1" description = "Cross-platform lib for process and system monitoring in Python." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" files = [ - {file = "psutil-6.1.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ff34df86226c0227c52f38b919213157588a678d049688eded74c76c8ba4a5d0"}, - {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:c0e0c00aa18ca2d3b2b991643b799a15fc8f0563d2ebb6040f64ce8dc027b942"}, - {file = "psutil-6.1.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:000d1d1ebd634b4efb383f4034437384e44a6d455260aaee2eca1e9c1b55f047"}, - {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:5cd2bcdc75b452ba2e10f0e8ecc0b57b827dd5d7aaffbc6821b2a9a242823a76"}, - {file = "psutil-6.1.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:045f00a43c737f960d273a83973b2511430d61f283a44c96bf13a6e829ba8fdc"}, - {file = "psutil-6.1.0-cp27-none-win32.whl", hash = "sha256:9118f27452b70bb1d9ab3198c1f626c2499384935aaf55388211ad982611407e"}, - {file = "psutil-6.1.0-cp27-none-win_amd64.whl", hash = "sha256:a8506f6119cff7015678e2bce904a4da21025cc70ad283a53b099e7620061d85"}, - {file = "psutil-6.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6e2dcd475ce8b80522e51d923d10c7871e45f20918e027ab682f94f1c6351688"}, - {file = "psutil-6.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0895b8414afafc526712c498bd9de2b063deaac4021a3b3c34566283464aff8e"}, - {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9dcbfce5d89f1d1f2546a2090f4fcf87c7f669d1d90aacb7d7582addece9fb38"}, - {file = "psutil-6.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:498c6979f9c6637ebc3a73b3f87f9eb1ec24e1ce53a7c5173b8508981614a90b"}, - {file = "psutil-6.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d905186d647b16755a800e7263d43df08b790d709d575105d419f8b6ef65423a"}, - {file = "psutil-6.1.0-cp36-cp36m-win32.whl", hash = "sha256:6d3fbbc8d23fcdcb500d2c9f94e07b1342df8ed71b948a2649b5cb060a7c94ca"}, - {file = "psutil-6.1.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1209036fbd0421afde505a4879dee3b2fd7b1e14fee81c0069807adcbbcca747"}, - {file = "psutil-6.1.0-cp37-abi3-win32.whl", hash = "sha256:1ad45a1f5d0b608253b11508f80940985d1d0c8f6111b5cb637533a0e6ddc13e"}, - {file = "psutil-6.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:a8fb3752b491d246034fa4d279ff076501588ce8cbcdbb62c32fd7a377d996be"}, - {file = "psutil-6.1.0.tar.gz", hash = "sha256:353815f59a7f64cdaca1c0307ee13558a0512f6db064e92fe833784f08539c7a"}, + {file = "psutil-6.1.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9ccc4316f24409159897799b83004cb1e24f9819b0dcf9c0b68bdcb6cefee6a8"}, + {file = "psutil-6.1.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ca9609c77ea3b8481ab005da74ed894035936223422dc591d6772b147421f777"}, + {file = "psutil-6.1.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:8df0178ba8a9e5bc84fed9cfa61d54601b371fbec5c8eebad27575f1e105c0d4"}, + {file = "psutil-6.1.1-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:1924e659d6c19c647e763e78670a05dbb7feaf44a0e9c94bf9e14dfc6ba50468"}, + {file = "psutil-6.1.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:018aeae2af92d943fdf1da6b58665124897cfc94faa2ca92098838f83e1b1bca"}, + {file = "psutil-6.1.1-cp27-none-win32.whl", hash = "sha256:6d4281f5bbca041e2292be3380ec56a9413b790579b8e593b1784499d0005dac"}, + {file = "psutil-6.1.1-cp27-none-win_amd64.whl", hash = "sha256:c777eb75bb33c47377c9af68f30e9f11bc78e0f07fbf907be4a5d70b2fe5f030"}, + {file = "psutil-6.1.1-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc0ed7fe2231a444fc219b9c42d0376e0a9a1a72f16c5cfa0f68d19f1a0663e8"}, + {file = "psutil-6.1.1-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:0bdd4eab935276290ad3cb718e9809412895ca6b5b334f5a9111ee6d9aff9377"}, + {file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b6e06c20c05fe95a3d7302d74e7097756d4ba1247975ad6905441ae1b5b66003"}, + {file = "psutil-6.1.1-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97f7cb9921fbec4904f522d972f0c0e1f4fabbdd4e0287813b21215074a0f160"}, + {file = "psutil-6.1.1-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:33431e84fee02bc84ea36d9e2c4a6d395d479c9dd9bba2376c1f6ee8f3a4e0b3"}, + {file = "psutil-6.1.1-cp36-cp36m-win32.whl", hash = "sha256:384636b1a64b47814437d1173be1427a7c83681b17a450bfc309a1953e329603"}, + {file = "psutil-6.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:8be07491f6ebe1a693f17d4f11e69d0dc1811fa082736500f649f79df7735303"}, + {file = "psutil-6.1.1-cp37-abi3-win32.whl", hash = "sha256:eaa912e0b11848c4d9279a93d7e2783df352b082f40111e078388701fd479e53"}, + {file = "psutil-6.1.1-cp37-abi3-win_amd64.whl", hash = "sha256:f35cfccb065fff93529d2afb4a2e89e363fe63ca1e4a5da22b603a85833c2649"}, + {file = "psutil-6.1.1.tar.gz", hash = "sha256:cf8496728c18f2d0b45198f06895be52f36611711746b7f30c464b422b50e2f5"}, ] [package.extras] -dev = ["black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "wheel"] +dev = ["abi3audit", "black", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest-cov", "requests", "rstcheck", "ruff", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"] test = ["pytest", "pytest-xdist", "setuptools"] [[package]] @@ -2894,53 +2854,53 @@ tests = ["pytest"] [[package]] name = "pyarrow" -version = "18.0.0" +version = "18.1.0" description = "Python library for Apache Arrow" optional = false python-versions = ">=3.9" files = [ - {file = "pyarrow-18.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:2333f93260674e185cfbf208d2da3007132572e56871f451ba1a556b45dae6e2"}, - {file = "pyarrow-18.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:4c381857754da44326f3a49b8b199f7f87a51c2faacd5114352fc78de30d3aba"}, - {file = "pyarrow-18.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:603cd8ad4976568954598ef0a6d4ed3dfb78aff3d57fa8d6271f470f0ce7d34f"}, - {file = "pyarrow-18.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58a62549a3e0bc9e03df32f350e10e1efb94ec6cf63e3920c3385b26663948ce"}, - {file = "pyarrow-18.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bc97316840a349485fbb137eb8d0f4d7057e1b2c1272b1a20eebbbe1848f5122"}, - {file = "pyarrow-18.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:2e549a748fa8b8715e734919923f69318c953e077e9c02140ada13e59d043310"}, - {file = "pyarrow-18.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:606e9a3dcb0f52307c5040698ea962685fb1c852d72379ee9412be7de9c5f9e2"}, - {file = "pyarrow-18.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d5795e37c0a33baa618c5e054cd61f586cf76850a251e2b21355e4085def6280"}, - {file = "pyarrow-18.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:5f0510608ccd6e7f02ca8596962afb8c6cc84c453e7be0da4d85f5f4f7b0328a"}, - {file = "pyarrow-18.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616ea2826c03c16e87f517c46296621a7c51e30400f6d0a61be645f203aa2b93"}, - {file = "pyarrow-18.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1824f5b029ddd289919f354bc285992cb4e32da518758c136271cf66046ef22"}, - {file = "pyarrow-18.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:6dd1b52d0d58dd8f685ced9971eb49f697d753aa7912f0a8f50833c7a7426319"}, - {file = "pyarrow-18.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:320ae9bd45ad7ecc12ec858b3e8e462578de060832b98fc4d671dee9f10d9954"}, - {file = "pyarrow-18.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:2c992716cffb1088414f2b478f7af0175fd0a76fea80841b1706baa8fb0ebaad"}, - {file = "pyarrow-18.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:e7ab04f272f98ebffd2a0661e4e126036f6936391ba2889ed2d44c5006237802"}, - {file = "pyarrow-18.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:03f40b65a43be159d2f97fd64dc998f769d0995a50c00f07aab58b0b3da87e1f"}, - {file = "pyarrow-18.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be08af84808dff63a76860847c48ec0416928a7b3a17c2f49a072cac7c45efbd"}, - {file = "pyarrow-18.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c70c1965cde991b711a98448ccda3486f2a336457cf4ec4dca257a926e149c9"}, - {file = "pyarrow-18.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:00178509f379415a3fcf855af020e3340254f990a8534294ec3cf674d6e255fd"}, - {file = "pyarrow-18.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:a71ab0589a63a3e987beb2bc172e05f000a5c5be2636b4b263c44034e215b5d7"}, - {file = "pyarrow-18.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe92efcdbfa0bcf2fa602e466d7f2905500f33f09eb90bf0bcf2e6ca41b574c8"}, - {file = "pyarrow-18.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:907ee0aa8ca576f5e0cdc20b5aeb2ad4d3953a3b4769fc4b499e00ef0266f02f"}, - {file = "pyarrow-18.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:66dcc216ebae2eb4c37b223feaf82f15b69d502821dde2da138ec5a3716e7463"}, - {file = "pyarrow-18.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc1daf7c425f58527900876354390ee41b0ae962a73ad0959b9d829def583bb1"}, - {file = "pyarrow-18.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871b292d4b696b09120ed5bde894f79ee2a5f109cb84470546471df264cae136"}, - {file = "pyarrow-18.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:082ba62bdcb939824ba1ce10b8acef5ab621da1f4c4805e07bfd153617ac19d4"}, - {file = "pyarrow-18.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:2c664ab88b9766413197733c1720d3dcd4190e8fa3bbdc3710384630a0a7207b"}, - {file = "pyarrow-18.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc892be34dbd058e8d189b47db1e33a227d965ea8805a235c8a7286f7fd17d3a"}, - {file = "pyarrow-18.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:28f9c39a56d2c78bf6b87dcc699d520ab850919d4a8c7418cd20eda49874a2ea"}, - {file = "pyarrow-18.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:f1a198a50c409ab2d009fbf20956ace84567d67f2c5701511d4dd561fae6f32e"}, - {file = "pyarrow-18.0.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5bd7fd32e3ace012d43925ea4fc8bd1b02cc6cc1e9813b518302950e89b5a22"}, - {file = "pyarrow-18.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:336addb8b6f5208be1b2398442c703a710b6b937b1a046065ee4db65e782ff5a"}, - {file = "pyarrow-18.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:45476490dd4adec5472c92b4d253e245258745d0ccaabe706f8d03288ed60a79"}, - {file = "pyarrow-18.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:b46591222c864e7da7faa3b19455196416cd8355ff6c2cc2e65726a760a3c420"}, - {file = "pyarrow-18.0.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:eb7e3abcda7e1e6b83c2dc2909c8d045881017270a119cc6ee7fdcfe71d02df8"}, - {file = "pyarrow-18.0.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:09f30690b99ce34e0da64d20dab372ee54431745e4efb78ac938234a282d15f9"}, - {file = "pyarrow-18.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d5ca5d707e158540312e09fd907f9f49bacbe779ab5236d9699ced14d2293b8"}, - {file = "pyarrow-18.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6331f280c6e4521c69b201a42dd978f60f7e129511a55da9e0bfe426b4ebb8d"}, - {file = "pyarrow-18.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:3ac24b2be732e78a5a3ac0b3aa870d73766dd00beba6e015ea2ea7394f8b4e55"}, - {file = "pyarrow-18.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b30a927c6dff89ee702686596f27c25160dd6c99be5bcc1513a763ae5b1bfc03"}, - {file = "pyarrow-18.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:8f40ec677e942374e3d7f2fad6a67a4c2811a8b975e8703c6fd26d3b168a90e2"}, - {file = "pyarrow-18.0.0.tar.gz", hash = "sha256:a6aa027b1a9d2970cf328ccd6dbe4a996bc13c39fd427f502782f5bdb9ca20f5"}, + {file = "pyarrow-18.1.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e21488d5cfd3d8b500b3238a6c4b075efabc18f0f6d80b29239737ebd69caa6c"}, + {file = "pyarrow-18.1.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:b516dad76f258a702f7ca0250885fc93d1fa5ac13ad51258e39d402bd9e2e1e4"}, + {file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f443122c8e31f4c9199cb23dca29ab9427cef990f283f80fe15b8e124bcc49b"}, + {file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a03da7f2758645d17b7b4f83c8bffeae5bbb7f974523fe901f36288d2eab71"}, + {file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ba17845efe3aa358ec266cf9cc2800fa73038211fb27968bfa88acd09261a470"}, + {file = "pyarrow-18.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3c35813c11a059056a22a3bef520461310f2f7eea5c8a11ef9de7062a23f8d56"}, + {file = "pyarrow-18.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9736ba3c85129d72aefa21b4f3bd715bc4190fe4426715abfff90481e7d00812"}, + {file = "pyarrow-18.1.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:eaeabf638408de2772ce3d7793b2668d4bb93807deed1725413b70e3156a7854"}, + {file = "pyarrow-18.1.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:3b2e2239339c538f3464308fd345113f886ad031ef8266c6f004d49769bb074c"}, + {file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f39a2e0ed32a0970e4e46c262753417a60c43a3246972cfc2d3eb85aedd01b21"}, + {file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31e9417ba9c42627574bdbfeada7217ad8a4cbbe45b9d6bdd4b62abbca4c6f6"}, + {file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:01c034b576ce0eef554f7c3d8c341714954be9b3f5d5bc7117006b85fcf302fe"}, + {file = "pyarrow-18.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f266a2c0fc31995a06ebd30bcfdb7f615d7278035ec5b1cd71c48d56daaf30b0"}, + {file = "pyarrow-18.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d4f13eee18433f99adefaeb7e01d83b59f73360c231d4782d9ddfaf1c3fbde0a"}, + {file = "pyarrow-18.1.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:9f3a76670b263dc41d0ae877f09124ab96ce10e4e48f3e3e4257273cee61ad0d"}, + {file = "pyarrow-18.1.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:da31fbca07c435be88a0c321402c4e31a2ba61593ec7473630769de8346b54ee"}, + {file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:543ad8459bc438efc46d29a759e1079436290bd583141384c6f7a1068ed6f992"}, + {file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0743e503c55be0fdb5c08e7d44853da27f19dc854531c0570f9f394ec9671d54"}, + {file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d4b3d2a34780645bed6414e22dda55a92e0fcd1b8a637fba86800ad737057e33"}, + {file = "pyarrow-18.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c52f81aa6f6575058d8e2c782bf79d4f9fdc89887f16825ec3a66607a5dd8e30"}, + {file = "pyarrow-18.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ad4892617e1a6c7a551cfc827e072a633eaff758fa09f21c4ee548c30bcaf99"}, + {file = "pyarrow-18.1.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:84e314d22231357d473eabec709d0ba285fa706a72377f9cc8e1cb3c8013813b"}, + {file = "pyarrow-18.1.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:f591704ac05dfd0477bb8f8e0bd4b5dc52c1cadf50503858dce3a15db6e46ff2"}, + {file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acb7564204d3c40babf93a05624fc6a8ec1ab1def295c363afc40b0c9e66c191"}, + {file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74de649d1d2ccb778f7c3afff6085bd5092aed4c23df9feeb45dd6b16f3811aa"}, + {file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f96bd502cb11abb08efea6dab09c003305161cb6c9eafd432e35e76e7fa9b90c"}, + {file = "pyarrow-18.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:36ac22d7782554754a3b50201b607d553a8d71b78cdf03b33c1125be4b52397c"}, + {file = "pyarrow-18.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:25dbacab8c5952df0ca6ca0af28f50d45bd31c1ff6fcf79e2d120b4a65ee7181"}, + {file = "pyarrow-18.1.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a276190309aba7bc9d5bd2933230458b3521a4317acfefe69a354f2fe59f2bc"}, + {file = "pyarrow-18.1.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ad514dbfcffe30124ce655d72771ae070f30bf850b48bc4d9d3b25993ee0e386"}, + {file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aebc13a11ed3032d8dd6e7171eb6e86d40d67a5639d96c35142bd568b9299324"}, + {file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6cf5c05f3cee251d80e98726b5c7cc9f21bab9e9783673bac58e6dfab57ecc8"}, + {file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:11b676cd410cf162d3f6a70b43fb9e1e40affbc542a1e9ed3681895f2962d3d9"}, + {file = "pyarrow-18.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:b76130d835261b38f14fc41fdfb39ad8d672afb84c447126b84d5472244cfaba"}, + {file = "pyarrow-18.1.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:0b331e477e40f07238adc7ba7469c36b908f07c89b95dd4bd3a0ec84a3d1e21e"}, + {file = "pyarrow-18.1.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:2c4dd0c9010a25ba03e198fe743b1cc03cd33c08190afff371749c52ccbbaf76"}, + {file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f97b31b4c4e21ff58c6f330235ff893cc81e23da081b1a4b1c982075e0ed4e9"}, + {file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a4813cb8ecf1809871fd2d64a8eff740a1bd3691bbe55f01a3cf6c5ec869754"}, + {file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:05a5636ec3eb5cc2a36c6edb534a38ef57b2ab127292a716d00eabb887835f1e"}, + {file = "pyarrow-18.1.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:73eeed32e724ea3568bb06161cad5fa7751e45bc2228e33dcb10c614044165c7"}, + {file = "pyarrow-18.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:a1880dd6772b685e803011a6b43a230c23b566859a6e0c9a276c1e0faf4f4052"}, + {file = "pyarrow-18.1.0.tar.gz", hash = "sha256:9386d3ca9c145b5539a1cfc75df07757dff870168c959b473a0bccbc3abc8c73"}, ] [package.extras] @@ -2948,62 +2908,82 @@ test = ["cffi", "hypothesis", "pandas", "pytest", "pytz"] [[package]] name = "pycares" -version = "4.4.0" +version = "4.5.0" description = "Python interface for c-ares" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pycares-4.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:24da119850841d16996713d9c3374ca28a21deee056d609fbbed29065d17e1f6"}, - {file = "pycares-4.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8f64cb58729689d4d0e78f0bfb4c25ce2f851d0274c0273ac751795c04b8798a"}, - {file = "pycares-4.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d33e2a1120887e89075f7f814ec144f66a6ce06a54f5722ccefc62fbeda83cff"}, - {file = "pycares-4.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c680fef1b502ee680f8f0b95a41af4ec2c234e50e16c0af5bbda31999d3584bd"}, - {file = "pycares-4.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fff16b09042ba077f7b8aa5868d1d22456f0002574d0ba43462b10a009331677"}, - {file = "pycares-4.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:229a1675eb33bc9afb1fc463e73ee334950ccc485bc83a43f6ae5839fb4d5fa3"}, - {file = "pycares-4.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3aebc73e5ad70464f998f77f2da2063aa617cbd8d3e8174dd7c5b4518f967153"}, - {file = "pycares-4.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6ef64649eba56448f65e26546d85c860709844d2fc22ef14d324fe0b27f761a9"}, - {file = "pycares-4.4.0-cp310-cp310-win32.whl", hash = "sha256:4afc2644423f4eef97857a9fd61be9758ce5e336b4b0bd3d591238bb4b8b03e0"}, - {file = "pycares-4.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5ed4e04af4012f875b78219d34434a6d08a67175150ac1b79eb70ab585d4ba8c"}, - {file = "pycares-4.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bce8db2fc6f3174bd39b81405210b9b88d7b607d33e56a970c34a0c190da0490"}, - {file = "pycares-4.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9a0303428d013ccf5c51de59c83f9127aba6200adb7fd4be57eddb432a1edd2a"}, - {file = "pycares-4.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afb91792f1556f97be7f7acb57dc7756d89c5a87bd8b90363a77dbf9ea653817"}, - {file = "pycares-4.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b61579cecf1f4d616e5ea31a6e423a16680ab0d3a24a2ffe7bb1d4ee162477ff"}, - {file = "pycares-4.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7af06968cbf6851566e806bf3e72825b0e6671832a2cbe840be1d2d65350710"}, - {file = "pycares-4.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ceb12974367b0a68a05d52f4162b29f575d241bd53de155efe632bf2c943c7f6"}, - {file = "pycares-4.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2eeec144bcf6a7b6f2d74d6e70cbba7886a84dd373c886f06cb137a07de4954c"}, - {file = "pycares-4.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e3a6f7cfdfd11eb5493d6d632e582408c8f3b429f295f8799c584c108b28db6f"}, - {file = "pycares-4.4.0-cp311-cp311-win32.whl", hash = "sha256:34736a2ffaa9c08ca9c707011a2d7b69074bbf82d645d8138bba771479b2362f"}, - {file = "pycares-4.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:eb66c30eb11e877976b7ead13632082a8621df648c408b8e15cdb91a452dd502"}, - {file = "pycares-4.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:fd644505a8cfd7f6584d33a9066d4e3d47700f050ef1490230c962de5dfb28c6"}, - {file = "pycares-4.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52084961262232ec04bd75f5043aed7e5d8d9695e542ff691dfef0110209f2d4"}, - {file = "pycares-4.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0c5368206057884cde18602580083aeaad9b860e2eac14fd253543158ce1e93"}, - {file = "pycares-4.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:112a4979c695b1c86f6782163d7dec58d57a3b9510536dcf4826550f9053dd9a"}, - {file = "pycares-4.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d186dafccdaa3409194c0f94db93c1a5d191145a275f19da6591f9499b8e7b8"}, - {file = "pycares-4.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:64965dc19c578a683ea73487a215a8897276224e004d50eeb21f0bc7a0b63c88"}, - {file = "pycares-4.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ed2a38e34bec6f2586435f6ff0bc5fe11d14bebd7ed492cf739a424e81681540"}, - {file = "pycares-4.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:94d6962db81541eb0396d2f0dfcbb18cdb8c8b251d165efc2d974ae652c547d4"}, - {file = "pycares-4.4.0-cp312-cp312-win32.whl", hash = "sha256:1168a48a834813aa80f412be2df4abaf630528a58d15c704857448b20b1675c0"}, - {file = "pycares-4.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:db24c4e7fea4a052c6e869cbf387dd85d53b9736cfe1ef5d8d568d1ca925e977"}, - {file = "pycares-4.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:21a5a0468861ec7df7befa69050f952da13db5427ae41ffe4713bc96291d1d95"}, - {file = "pycares-4.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:22c00bf659a9fa44d7b405cf1cd69b68b9d37537899898d8cbe5dffa4016b273"}, - {file = "pycares-4.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23aa3993a352491a47fcf17867f61472f32f874df4adcbb486294bd9fbe8abee"}, - {file = "pycares-4.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:813d661cbe2e37d87da2d16b7110a6860e93ddb11735c6919c8a3545c7b9c8d8"}, - {file = "pycares-4.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:77cf5a2fd5583c670de41a7f4a7b46e5cbabe7180d8029f728571f4d2e864084"}, - {file = "pycares-4.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3eaa6681c0a3e3f3868c77aca14b7760fed35fdfda2fe587e15c701950e7bc69"}, - {file = "pycares-4.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ad58e284a658a8a6a84af2e0b62f2f961f303cedfe551854d7bd40c3cbb61912"}, - {file = "pycares-4.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bfb89ca9e3d0a9b5332deeb666b2ede9d3469107742158f4aeda5ce032d003f4"}, - {file = "pycares-4.4.0-cp38-cp38-win32.whl", hash = "sha256:f36bdc1562142e3695555d2f4ac0cb69af165eddcefa98efc1c79495b533481f"}, - {file = "pycares-4.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:902461a92b6a80fd5041a2ec5235680c7cc35e43615639ec2a40e63fca2dfb51"}, - {file = "pycares-4.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7bddc6adba8f699728f7fc1c9ce8cef359817ad78e2ed52b9502cb5f8dc7f741"}, - {file = "pycares-4.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cb49d5805cd347c404f928c5ae7c35e86ba0c58ffa701dbe905365e77ce7d641"}, - {file = "pycares-4.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56cf3349fa3a2e67ed387a7974c11d233734636fe19facfcda261b411af14d80"}, - {file = "pycares-4.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bf2eaa83a5987e48fa63302f0fe7ce3275cfda87b34d40fef9ce703fb3ac002"}, - {file = "pycares-4.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82bba2ab77eb5addbf9758d514d9bdef3c1bfe7d1649a47bd9a0d55a23ef478b"}, - {file = "pycares-4.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c6a8bde63106f162fca736e842a916853cad3c8d9d137e11c9ffa37efa818b02"}, - {file = "pycares-4.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f5f646eec041db6ffdbcaf3e0756fb92018f7af3266138c756bb09d2b5baadec"}, - {file = "pycares-4.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9dc04c54c6ea615210c1b9e803d0e2d2255f87a3d5d119b6482c8f0dfa15b26b"}, - {file = "pycares-4.4.0-cp39-cp39-win32.whl", hash = "sha256:97892cced5794d721fb4ff8765764aa4ea48fe8b2c3820677505b96b83d4ef47"}, - {file = "pycares-4.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:917f08f0b5d9324e9a34211e68d27447c552b50ab967044776bbab7e42a553a2"}, - {file = "pycares-4.4.0.tar.gz", hash = "sha256:f47579d508f2f56eddd16ce72045782ad3b1b3b678098699e2b6a1b30733e1c2"}, + {file = "pycares-4.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:13a82fad8239d6fbcf916099bee17d8b5666d0ddb77dace431e0f7961c9427ab"}, + {file = "pycares-4.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fefc7bebbe39b2e3b4b9615471233a8f7356b96129a7db9030313a3ae4ecc42d"}, + {file = "pycares-4.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e322e8ce810026f6e0c7c2a254b9ed02191ab8d42fa2ce6808ede1bdccab8e65"}, + {file = "pycares-4.5.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:723ba0803b016294430e40e544503fed9164949b694342c2552ab189e2b688ef"}, + {file = "pycares-4.5.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e48b20b59cdc929cc712a8b22e89c273256e482b49bb8999af98d2c6fc4563c2"}, + {file = "pycares-4.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de6e55bd9af595b112ac6080ac0a0d52b5853d0d8e6d01ac65ff09e51e62490a"}, + {file = "pycares-4.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a6f4b9063e3dd70460400367917698f209c10aabb68bf70b09e364895444487d"}, + {file = "pycares-4.5.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:95522d4840d702fd766439a7c7cd747935aa54cf0b8675e9fadd8414dd9dd0df"}, + {file = "pycares-4.5.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e4709ce4fd9dbee24b1397f71a2adb3267323bb5ad5e7fde3f87873d172dd156"}, + {file = "pycares-4.5.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8addbf3408af1010f50fd67ef634a6cb239ccb9c534c32a40713f3b8d306a98e"}, + {file = "pycares-4.5.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:d0428ef42fcf575e197047e6a47892404faa34231902a453b3dfed66af4178b3"}, + {file = "pycares-4.5.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:aed5c2732f3a6bdbbfab202267d37044ca1162f690b9d34b7ece97ba43f27453"}, + {file = "pycares-4.5.0-cp310-cp310-win32.whl", hash = "sha256:b1859ea770a7abec40a6d02b5ab03c2396c4900c01f4e50ddb6c0dca4c2a6a7c"}, + {file = "pycares-4.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:9f87d8da20a3a80ab05fe80c14a62bf078bd726ca6af609edbeb376fb97d50ab"}, + {file = "pycares-4.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5ca7a1dba7b88290710db45012e0903c21c839fa0a2b9ddc100bba8e66bfb251"}, + {file = "pycares-4.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:160e92588cdf1a0fa3a7015f47990b508d50efd9109ea4d719dee31c058f0648"}, + {file = "pycares-4.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f38e45d23660ed1dafdb956fd263ae4735530ef1578aa2bf2caabb94cee4523"}, + {file = "pycares-4.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f742acc6d29a99ffc14e3f154b3848ea05c5533b71065e0f0a0fd99c527491b2"}, + {file = "pycares-4.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceaf71bcd7b6447705e689b8fee8836c20c6148511a90122981f524a84bfcca9"}, + {file = "pycares-4.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdc3c0be7b5b83e78e28818fecd0405bd401110dd6e2e66f7f10713c1188362c"}, + {file = "pycares-4.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd458ee69800195247aa19b5675c5914cbc091c5a220e4f0e96777a31bb555c1"}, + {file = "pycares-4.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a6649d713df73266708642fc3d04f110c0a66bee510fbce4cc5fed79df42083"}, + {file = "pycares-4.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ac57d7bda925c10b997434e7ce30a2c3689c2e96bab9fd0a1165d5577378eecd"}, + {file = "pycares-4.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ba17d8e5eeec4b2e0eb1a6a840bae9e62cd1c1c9cbc8dc9db9d1b9fdf33d0b54"}, + {file = "pycares-4.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9e9b7d1a8de703283e4735c0e532ba4bc600e88de872dcd1a9a4950cf74d9f4f"}, + {file = "pycares-4.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4c6922ecbe458c13a4a2c1177bbce38abc44b5f086bc82115a92eab34418915f"}, + {file = "pycares-4.5.0-cp311-cp311-win32.whl", hash = "sha256:1004b8a17614e33410b4b1bb68360977667f1cc9ab2dbcfb27240d6703e4cb6a"}, + {file = "pycares-4.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:2c9c1055c622258a0f315560b2880a372363484b87cbef48af092624804caa72"}, + {file = "pycares-4.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:506efbe5017807747ccd1bdcb3c2f6e64635bc01fee01a50c0b97d649018c162"}, + {file = "pycares-4.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c469ec9fbe0526f45a98f67c1ea55be03abf30809c4f9c9be4bc93fb6806304d"}, + {file = "pycares-4.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:597c0950ede240c3a779f023fcf2442207fc11e570d3ca4ccdbb0db5bbaf2588"}, + {file = "pycares-4.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9aa0da03c4df6ed0f87dd52a293bd0508734515041cc5be0f85d9edc1814914f"}, + {file = "pycares-4.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aea1ebf52767c777d10a1b3d03844b9b05cc892714b3ee177d5d9fbff74fb9fa"}, + {file = "pycares-4.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb20d84269ddffb177b6048e3bc03d0b9ffe17592093d900d5544805958d86b3"}, + {file = "pycares-4.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3125df81b657971ee5c0333f8f560ba0151db1eb7cf04aea7d783bb433b306c1"}, + {file = "pycares-4.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:525c77ea44546c12f379641aee163585d403cf50e29b04a06059d6aac894e956"}, + {file = "pycares-4.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1fd87cb26b317a9988abfcfa4e4dbc55d5f20177e5979ad4d854468a9246c187"}, + {file = "pycares-4.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a90aecd41188884e57ae32507a2c6b010c60b791a253083761bbb37a488ecaed"}, + {file = "pycares-4.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0d3de65cab653979dcc491e03f596566c9d40346c9deb088e0f9fe70600d8737"}, + {file = "pycares-4.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:27a77b43604b3ba24e4fc49fd3ea59f50f7d89c7255f1f1ea46928b26cccacfa"}, + {file = "pycares-4.5.0-cp312-cp312-win32.whl", hash = "sha256:6028cb8766f0fea1d2caa69fac23621fbe2cff9ce6968374e165737258703a33"}, + {file = "pycares-4.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:2ce10672c4cfd1c5fb6718e8b25f0336ca11c89aab88aa6df53dafc4e41df740"}, + {file = "pycares-4.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:011cd670da7caf55664c944abb71ec39af82b837f8d48da7cf0eec80f5682c4c"}, + {file = "pycares-4.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b5c67930497fb2b1dbcaa85f8c4188fc2cb62e41d787deeed2d33cfe9dd6bf52"}, + {file = "pycares-4.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d435a3b8468c656a7e7180dd7c4794510f6c612c33ad61a0fff6e440621f8b5"}, + {file = "pycares-4.5.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8371f5ee1efb33d6276e275d152c9c5605e5f2e58a9e168519ec1f9e13dd95ae"}, + {file = "pycares-4.5.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76a9096fd5dc49c61c5235ea7032e8b43f4382800d64ca1e0e0cda700c082aa"}, + {file = "pycares-4.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b604af76b57469ff68b44e9e4c857eaee43bc5035f4f183f07f4f7149191fe1b"}, + {file = "pycares-4.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c589bd4f9160bfdb2f8080cf564bb120a4312cf091db07fe417f8e58a896a63c"}, + {file = "pycares-4.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:361262805bb09742c364ec0117842043c950339e38561009bcabbb6ac89458ef"}, + {file = "pycares-4.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6d2afb3c0776467055bf33db843ef483d25639be0f32e3a13ef5d4dc64098bf5"}, + {file = "pycares-4.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bc7a1d8ed7c7a4de17706a3c89b305b02eb64c778897e6727c043e5b9dd0d853"}, + {file = "pycares-4.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5703ec878b5c1efacdbf24ceaedfa606112fc67af5564f4db99c2c210f3ffadc"}, + {file = "pycares-4.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d87758e09dbf52c27ed7cf7bc7eaf8b3226217d10c52b03d61a14d59f40fcae1"}, + {file = "pycares-4.5.0-cp313-cp313-win32.whl", hash = "sha256:3316d490b4ce1a69f034881ac1ea7608f5f24ea5293db24ab574ac70b7d7e407"}, + {file = "pycares-4.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:018e700fb0d1a2db5ec96e404ffa85ed97cc96e96d6af0bb9548111e37cf36a3"}, + {file = "pycares-4.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:78c9890d93108c70708babee8a783e6021233f1f0a763d3634add6fd429aae58"}, + {file = "pycares-4.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba69f8123995aa3df99f6ebc726fc6a4b08e467a957b215c0a82749b901d5eed"}, + {file = "pycares-4.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32d33c4ffae31d1b544adebe0b9aee2be1fb18aedd3f4f91e41c495ccbafd6d8"}, + {file = "pycares-4.5.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17a060cfc469828abf7f5945964d505bd8c0a756942fee159538f7885169752e"}, + {file = "pycares-4.5.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c1d0d5e69fa29e41b590a9dd5842454e8f34e2b928c92540aaf87e0161de8120"}, + {file = "pycares-4.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f096699c46f5dde2c7a8d91501a36d2d58500f4d63682e2ec14a0fed7cca6402"}, + {file = "pycares-4.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:429fe2065581a64a5f024f507b5f679bf37ea0ed39c3ba6289dba907e1c8a8f4"}, + {file = "pycares-4.5.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9ea2f6d48e64b413b97b41b47392087b452af9bf9f9d4d6d05305a159f45909f"}, + {file = "pycares-4.5.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:96d3aecd747a3fcd1e12c1ea1481b0813b4e0e80d40f314db7a86dda5bb1bd94"}, + {file = "pycares-4.5.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:32919f6eda7f5ea4df3e64149fc5792b0d455277d23d6d0fc365142062f35d80"}, + {file = "pycares-4.5.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:37add862461f9a3fc7ee4dd8b68465812b39456e21cebd5a33c414131ac05060"}, + {file = "pycares-4.5.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ed1d050d2c6d74a77c1b6c51fd99426cc000b4202a50d28d6ca75f7433099a6b"}, + {file = "pycares-4.5.0-cp39-cp39-win32.whl", hash = "sha256:887ac451ffe6e39ee46d3d0989c7bb829933d77e1dad5776511d825fc7e6a25b"}, + {file = "pycares-4.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:5c8b87c05740595bc8051dc98e51f022f003750e7da90f62f7a9fd50e330b196"}, + {file = "pycares-4.5.0.tar.gz", hash = "sha256:025b6c2ffea4e9fb8f9a097381c2fecb24aff23fbd6906e70da22ec9ba60e19d"}, ] [package.dependencies] @@ -3047,13 +3027,13 @@ files = [ [[package]] name = "pygments" -version = "2.18.0" +version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" files = [ - {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, - {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, ] [package.extras] @@ -3109,15 +3089,18 @@ cli = ["click (>=5.0)"] [[package]] name = "python-json-logger" -version = "2.0.7" -description = "A python library adding a json log formatter" +version = "3.2.1" +description = "JSON Log Formatter for the Python Logging Package" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c"}, - {file = "python_json_logger-2.0.7-py3-none-any.whl", hash = "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd"}, + {file = "python_json_logger-3.2.1-py3-none-any.whl", hash = "sha256:cdc17047eb5374bd311e748b42f99d71223f3b0e186f4206cc5d52aefe85b090"}, + {file = "python_json_logger-3.2.1.tar.gz", hash = "sha256:8eb0554ea17cb75b05d2848bc14fb02fbdbd9d6972120781b974380bfa162008"}, ] +[package.extras] +dev = ["backports.zoneinfo", "black", "build", "freezegun", "mdx_truly_sane_lists", "mike", "mkdocs", "mkdocs-awesome-pages-plugin", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-material (>=8.5)", "mkdocstrings[python]", "msgspec", "msgspec-python313-pre", "mypy", "orjson", "pylint", "pytest", "tzdata", "validate-pyproject[all]"] + [[package]] name = "pytz" version = "2024.2" @@ -3417,101 +3400,114 @@ files = [ [[package]] name = "rpds-py" -version = "0.21.0" +version = "0.22.3" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.9" files = [ - {file = "rpds_py-0.21.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a017f813f24b9df929674d0332a374d40d7f0162b326562daae8066b502d0590"}, - {file = "rpds_py-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:20cc1ed0bcc86d8e1a7e968cce15be45178fd16e2ff656a243145e0b439bd250"}, - {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad116dda078d0bc4886cb7840e19811562acdc7a8e296ea6ec37e70326c1b41c"}, - {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:808f1ac7cf3b44f81c9475475ceb221f982ef548e44e024ad5f9e7060649540e"}, - {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de552f4a1916e520f2703ec474d2b4d3f86d41f353e7680b597512ffe7eac5d0"}, - {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:efec946f331349dfc4ae9d0e034c263ddde19414fe5128580f512619abed05f1"}, - {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b80b4690bbff51a034bfde9c9f6bf9357f0a8c61f548942b80f7b66356508bf5"}, - {file = "rpds_py-0.21.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085ed25baac88953d4283e5b5bd094b155075bb40d07c29c4f073e10623f9f2e"}, - {file = "rpds_py-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:daa8efac2a1273eed2354397a51216ae1e198ecbce9036fba4e7610b308b6153"}, - {file = "rpds_py-0.21.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:95a5bad1ac8a5c77b4e658671642e4af3707f095d2b78a1fdd08af0dfb647624"}, - {file = "rpds_py-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3e53861b29a13d5b70116ea4230b5f0f3547b2c222c5daa090eb7c9c82d7f664"}, - {file = "rpds_py-0.21.0-cp310-none-win32.whl", hash = "sha256:ea3a6ac4d74820c98fcc9da4a57847ad2cc36475a8bd9683f32ab6d47a2bd682"}, - {file = "rpds_py-0.21.0-cp310-none-win_amd64.whl", hash = "sha256:b8f107395f2f1d151181880b69a2869c69e87ec079c49c0016ab96860b6acbe5"}, - {file = "rpds_py-0.21.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5555db3e618a77034954b9dc547eae94166391a98eb867905ec8fcbce1308d95"}, - {file = "rpds_py-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:97ef67d9bbc3e15584c2f3c74bcf064af36336c10d2e21a2131e123ce0f924c9"}, - {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ab2c2a26d2f69cdf833174f4d9d86118edc781ad9a8fa13970b527bf8236027"}, - {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4e8921a259f54bfbc755c5bbd60c82bb2339ae0324163f32868f63f0ebb873d9"}, - {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a7ff941004d74d55a47f916afc38494bd1cfd4b53c482b77c03147c91ac0ac3"}, - {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5145282a7cd2ac16ea0dc46b82167754d5e103a05614b724457cffe614f25bd8"}, - {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de609a6f1b682f70bb7163da745ee815d8f230d97276db049ab447767466a09d"}, - {file = "rpds_py-0.21.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40c91c6e34cf016fa8e6b59d75e3dbe354830777fcfd74c58b279dceb7975b75"}, - {file = "rpds_py-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d2132377f9deef0c4db89e65e8bb28644ff75a18df5293e132a8d67748397b9f"}, - {file = "rpds_py-0.21.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0a9e0759e7be10109645a9fddaaad0619d58c9bf30a3f248a2ea57a7c417173a"}, - {file = "rpds_py-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9e20da3957bdf7824afdd4b6eeb29510e83e026473e04952dca565170cd1ecc8"}, - {file = "rpds_py-0.21.0-cp311-none-win32.whl", hash = "sha256:f71009b0d5e94c0e86533c0b27ed7cacc1239cb51c178fd239c3cfefefb0400a"}, - {file = "rpds_py-0.21.0-cp311-none-win_amd64.whl", hash = "sha256:e168afe6bf6ab7ab46c8c375606298784ecbe3ba31c0980b7dcbb9631dcba97e"}, - {file = "rpds_py-0.21.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:30b912c965b2aa76ba5168fd610087bad7fcde47f0a8367ee8f1876086ee6d1d"}, - {file = "rpds_py-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ca9989d5d9b1b300bc18e1801c67b9f6d2c66b8fd9621b36072ed1df2c977f72"}, - {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f54e7106f0001244a5f4cf810ba8d3f9c542e2730821b16e969d6887b664266"}, - {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fed5dfefdf384d6fe975cc026886aece4f292feaf69d0eeb716cfd3c5a4dd8be"}, - {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:590ef88db231c9c1eece44dcfefd7515d8bf0d986d64d0caf06a81998a9e8cab"}, - {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f983e4c2f603c95dde63df633eec42955508eefd8d0f0e6d236d31a044c882d7"}, - {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b229ce052ddf1a01c67d68166c19cb004fb3612424921b81c46e7ea7ccf7c3bf"}, - {file = "rpds_py-0.21.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ebf64e281a06c904a7636781d2e973d1f0926a5b8b480ac658dc0f556e7779f4"}, - {file = "rpds_py-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:998a8080c4495e4f72132f3d66ff91f5997d799e86cec6ee05342f8f3cda7dca"}, - {file = "rpds_py-0.21.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:98486337f7b4f3c324ab402e83453e25bb844f44418c066623db88e4c56b7c7b"}, - {file = "rpds_py-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a78d8b634c9df7f8d175451cfeac3810a702ccb85f98ec95797fa98b942cea11"}, - {file = "rpds_py-0.21.0-cp312-none-win32.whl", hash = "sha256:a58ce66847711c4aa2ecfcfaff04cb0327f907fead8945ffc47d9407f41ff952"}, - {file = "rpds_py-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:e860f065cc4ea6f256d6f411aba4b1251255366e48e972f8a347cf88077b24fd"}, - {file = "rpds_py-0.21.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ee4eafd77cc98d355a0d02f263efc0d3ae3ce4a7c24740010a8b4012bbb24937"}, - {file = "rpds_py-0.21.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:688c93b77e468d72579351a84b95f976bd7b3e84aa6686be6497045ba84be560"}, - {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c38dbf31c57032667dd5a2f0568ccde66e868e8f78d5a0d27dcc56d70f3fcd3b"}, - {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2d6129137f43f7fa02d41542ffff4871d4aefa724a5fe38e2c31a4e0fd343fb0"}, - {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:520ed8b99b0bf86a176271f6fe23024323862ac674b1ce5b02a72bfeff3fff44"}, - {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aaeb25ccfb9b9014a10eaf70904ebf3f79faaa8e60e99e19eef9f478651b9b74"}, - {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af04ac89c738e0f0f1b913918024c3eab6e3ace989518ea838807177d38a2e94"}, - {file = "rpds_py-0.21.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b9b76e2afd585803c53c5b29e992ecd183f68285b62fe2668383a18e74abe7a3"}, - {file = "rpds_py-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5afb5efde74c54724e1a01118c6e5c15e54e642c42a1ba588ab1f03544ac8c7a"}, - {file = "rpds_py-0.21.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:52c041802a6efa625ea18027a0723676a778869481d16803481ef6cc02ea8cb3"}, - {file = "rpds_py-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee1e4fc267b437bb89990b2f2abf6c25765b89b72dd4a11e21934df449e0c976"}, - {file = "rpds_py-0.21.0-cp313-none-win32.whl", hash = "sha256:0c025820b78817db6a76413fff6866790786c38f95ea3f3d3c93dbb73b632202"}, - {file = "rpds_py-0.21.0-cp313-none-win_amd64.whl", hash = "sha256:320c808df533695326610a1b6a0a6e98f033e49de55d7dc36a13c8a30cfa756e"}, - {file = "rpds_py-0.21.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:2c51d99c30091f72a3c5d126fad26236c3f75716b8b5e5cf8effb18889ced928"}, - {file = "rpds_py-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cbd7504a10b0955ea287114f003b7ad62330c9e65ba012c6223dba646f6ffd05"}, - {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6dcc4949be728ede49e6244eabd04064336012b37f5c2200e8ec8eb2988b209c"}, - {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f414da5c51bf350e4b7960644617c130140423882305f7574b6cf65a3081cecb"}, - {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9afe42102b40007f588666bc7de82451e10c6788f6f70984629db193849dced1"}, - {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b929c2bb6e29ab31f12a1117c39f7e6d6450419ab7464a4ea9b0b417174f044"}, - {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8404b3717da03cbf773a1d275d01fec84ea007754ed380f63dfc24fb76ce4592"}, - {file = "rpds_py-0.21.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e12bb09678f38b7597b8346983d2323a6482dcd59e423d9448108c1be37cac9d"}, - {file = "rpds_py-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:58a0e345be4b18e6b8501d3b0aa540dad90caeed814c515e5206bb2ec26736fd"}, - {file = "rpds_py-0.21.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c3761f62fcfccf0864cc4665b6e7c3f0c626f0380b41b8bd1ce322103fa3ef87"}, - {file = "rpds_py-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c2b2f71c6ad6c2e4fc9ed9401080badd1469fa9889657ec3abea42a3d6b2e1ed"}, - {file = "rpds_py-0.21.0-cp39-none-win32.whl", hash = "sha256:b21747f79f360e790525e6f6438c7569ddbfb1b3197b9e65043f25c3c9b489d8"}, - {file = "rpds_py-0.21.0-cp39-none-win_amd64.whl", hash = "sha256:0626238a43152918f9e72ede9a3b6ccc9e299adc8ade0d67c5e142d564c9a83d"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6b4ef7725386dc0762857097f6b7266a6cdd62bfd209664da6712cb26acef035"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:6bc0e697d4d79ab1aacbf20ee5f0df80359ecf55db33ff41481cf3e24f206919"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da52d62a96e61c1c444f3998c434e8b263c384f6d68aca8274d2e08d1906325c"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:98e4fe5db40db87ce1c65031463a760ec7906ab230ad2249b4572c2fc3ef1f9f"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30bdc973f10d28e0337f71d202ff29345320f8bc49a31c90e6c257e1ccef4333"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:faa5e8496c530f9c71f2b4e1c49758b06e5f4055e17144906245c99fa6d45356"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:32eb88c30b6a4f0605508023b7141d043a79b14acb3b969aa0b4f99b25bc7d4a"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a89a8ce9e4e75aeb7fa5d8ad0f3fecdee813802592f4f46a15754dcb2fd6b061"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:241e6c125568493f553c3d0fdbb38c74babf54b45cef86439d4cd97ff8feb34d"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:3b766a9f57663396e4f34f5140b3595b233a7b146e94777b97a8413a1da1be18"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:af4a644bf890f56e41e74be7d34e9511e4954894d544ec6b8efe1e21a1a8da6c"}, - {file = "rpds_py-0.21.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:3e30a69a706e8ea20444b98a49f386c17b26f860aa9245329bab0851ed100677"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:031819f906bb146561af051c7cef4ba2003d28cff07efacef59da973ff7969ba"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b876f2bc27ab5954e2fd88890c071bd0ed18b9c50f6ec3de3c50a5ece612f7a6"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc5695c321e518d9f03b7ea6abb5ea3af4567766f9852ad1560f501b17588c7b"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4de1da871b5c0fd5537b26a6fc6814c3cc05cabe0c941db6e9044ffbb12f04a"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:878f6fea96621fda5303a2867887686d7a198d9e0f8a40be100a63f5d60c88c9"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8eeec67590e94189f434c6d11c426892e396ae59e4801d17a93ac96b8c02a6c"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ff2eba7f6c0cb523d7e9cff0903f2fe1feff8f0b2ceb6bd71c0e20a4dcee271"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a429b99337062877d7875e4ff1a51fe788424d522bd64a8c0a20ef3021fdb6ed"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:d167e4dbbdac48bd58893c7e446684ad5d425b407f9336e04ab52e8b9194e2ed"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:4eb2de8a147ffe0626bfdc275fc6563aa7bf4b6db59cf0d44f0ccd6ca625a24e"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e78868e98f34f34a88e23ee9ccaeeec460e4eaf6db16d51d7a9b883e5e785a5e"}, - {file = "rpds_py-0.21.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:4991ca61656e3160cdaca4851151fd3f4a92e9eba5c7a530ab030d6aee96ec89"}, - {file = "rpds_py-0.21.0.tar.gz", hash = "sha256:ed6378c9d66d0de903763e7706383d60c33829581f0adff47b6535f1802fa6db"}, + {file = "rpds_py-0.22.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6c7b99ca52c2c1752b544e310101b98a659b720b21db00e65edca34483259967"}, + {file = "rpds_py-0.22.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:be2eb3f2495ba669d2a985f9b426c1797b7d48d6963899276d22f23e33d47e37"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70eb60b3ae9245ddea20f8a4190bd79c705a22f8028aaf8bbdebe4716c3fab24"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4041711832360a9b75cfb11b25a6a97c8fb49c07b8bd43d0d02b45d0b499a4ff"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64607d4cbf1b7e3c3c8a14948b99345eda0e161b852e122c6bb71aab6d1d798c"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e69b0a0e2537f26d73b4e43ad7bc8c8efb39621639b4434b76a3de50c6966e"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc27863442d388870c1809a87507727b799c8460573cfbb6dc0eeaef5a11b5ec"}, + {file = "rpds_py-0.22.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e79dd39f1e8c3504be0607e5fc6e86bb60fe3584bec8b782578c3b0fde8d932c"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e0fa2d4ec53dc51cf7d3bb22e0aa0143966119f42a0c3e4998293a3dd2856b09"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fda7cb070f442bf80b642cd56483b5548e43d366fe3f39b98e67cce780cded00"}, + {file = "rpds_py-0.22.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cff63a0272fcd259dcc3be1657b07c929c466b067ceb1c20060e8d10af56f5bf"}, + {file = "rpds_py-0.22.3-cp310-cp310-win32.whl", hash = "sha256:9bd7228827ec7bb817089e2eb301d907c0d9827a9e558f22f762bb690b131652"}, + {file = "rpds_py-0.22.3-cp310-cp310-win_amd64.whl", hash = "sha256:9beeb01d8c190d7581a4d59522cd3d4b6887040dcfc744af99aa59fef3e041a8"}, + {file = "rpds_py-0.22.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d20cfb4e099748ea39e6f7b16c91ab057989712d31761d3300d43134e26e165f"}, + {file = "rpds_py-0.22.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:68049202f67380ff9aa52f12e92b1c30115f32e6895cd7198fa2a7961621fc5a"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb4f868f712b2dd4bcc538b0a0c1f63a2b1d584c925e69a224d759e7070a12d5"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bc51abd01f08117283c5ebf64844a35144a0843ff7b2983e0648e4d3d9f10dbb"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3cec041684de9a4684b1572fe28c7267410e02450f4561700ca5a3bc6695a2"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7ef9d9da710be50ff6809fed8f1963fecdfecc8b86656cadfca3bc24289414b0"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59f4a79c19232a5774aee369a0c296712ad0e77f24e62cad53160312b1c1eaa1"}, + {file = "rpds_py-0.22.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a60bce91f81ddaac922a40bbb571a12c1070cb20ebd6d49c48e0b101d87300d"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e89391e6d60251560f0a8f4bd32137b077a80d9b7dbe6d5cab1cd80d2746f648"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e3fb866d9932a3d7d0c82da76d816996d1667c44891bd861a0f97ba27e84fc74"}, + {file = "rpds_py-0.22.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1352ae4f7c717ae8cba93421a63373e582d19d55d2ee2cbb184344c82d2ae55a"}, + {file = "rpds_py-0.22.3-cp311-cp311-win32.whl", hash = "sha256:b0b4136a252cadfa1adb705bb81524eee47d9f6aab4f2ee4fa1e9d3cd4581f64"}, + {file = "rpds_py-0.22.3-cp311-cp311-win_amd64.whl", hash = "sha256:8bd7c8cfc0b8247c8799080fbff54e0b9619e17cdfeb0478ba7295d43f635d7c"}, + {file = "rpds_py-0.22.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:27e98004595899949bd7a7b34e91fa7c44d7a97c40fcaf1d874168bb652ec67e"}, + {file = "rpds_py-0.22.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1978d0021e943aae58b9b0b196fb4895a25cc53d3956b8e35e0b7682eefb6d56"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:655ca44a831ecb238d124e0402d98f6212ac527a0ba6c55ca26f616604e60a45"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:feea821ee2a9273771bae61194004ee2fc33f8ec7db08117ef9147d4bbcbca8e"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22bebe05a9ffc70ebfa127efbc429bc26ec9e9b4ee4d15a740033efda515cf3d"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3af6e48651c4e0d2d166dc1b033b7042ea3f871504b6805ba5f4fe31581d8d38"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ba3c290821343c192f7eae1d8fd5999ca2dc99994114643e2f2d3e6138b15"}, + {file = "rpds_py-0.22.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:02fbb9c288ae08bcb34fb41d516d5eeb0455ac35b5512d03181d755d80810059"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f56a6b404f74ab372da986d240e2e002769a7d7102cc73eb238a4f72eec5284e"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0a0461200769ab3b9ab7e513f6013b7a97fdeee41c29b9db343f3c5a8e2b9e61"}, + {file = "rpds_py-0.22.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8633e471c6207a039eff6aa116e35f69f3156b3989ea3e2d755f7bc41754a4a7"}, + {file = "rpds_py-0.22.3-cp312-cp312-win32.whl", hash = "sha256:593eba61ba0c3baae5bc9be2f5232430453fb4432048de28399ca7376de9c627"}, + {file = "rpds_py-0.22.3-cp312-cp312-win_amd64.whl", hash = "sha256:d115bffdd417c6d806ea9069237a4ae02f513b778e3789a359bc5856e0404cc4"}, + {file = "rpds_py-0.22.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:ea7433ce7e4bfc3a85654aeb6747babe3f66eaf9a1d0c1e7a4435bbdf27fea84"}, + {file = "rpds_py-0.22.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6dd9412824c4ce1aca56c47b0991e65bebb7ac3f4edccfd3f156150c96a7bf25"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20070c65396f7373f5df4005862fa162db5d25d56150bddd0b3e8214e8ef45b4"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0b09865a9abc0ddff4e50b5ef65467cd94176bf1e0004184eb915cbc10fc05c5"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3453e8d41fe5f17d1f8e9c383a7473cd46a63661628ec58e07777c2fff7196dc"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f5d36399a1b96e1a5fdc91e0522544580dbebeb1f77f27b2b0ab25559e103b8b"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:009de23c9c9ee54bf11303a966edf4d9087cd43a6003672e6aa7def643d06518"}, + {file = "rpds_py-0.22.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1aef18820ef3e4587ebe8b3bc9ba6e55892a6d7b93bac6d29d9f631a3b4befbd"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f60bd8423be1d9d833f230fdbccf8f57af322d96bcad6599e5a771b151398eb2"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:62d9cfcf4948683a18a9aff0ab7e1474d407b7bab2ca03116109f8464698ab16"}, + {file = "rpds_py-0.22.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9253fc214112405f0afa7db88739294295f0e08466987f1d70e29930262b4c8f"}, + {file = "rpds_py-0.22.3-cp313-cp313-win32.whl", hash = "sha256:fb0ba113b4983beac1a2eb16faffd76cb41e176bf58c4afe3e14b9c681f702de"}, + {file = "rpds_py-0.22.3-cp313-cp313-win_amd64.whl", hash = "sha256:c58e2339def52ef6b71b8f36d13c3688ea23fa093353f3a4fee2556e62086ec9"}, + {file = "rpds_py-0.22.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:f82a116a1d03628a8ace4859556fb39fd1424c933341a08ea3ed6de1edb0283b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3dfcbc95bd7992b16f3f7ba05af8a64ca694331bd24f9157b49dadeeb287493b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59259dc58e57b10e7e18ce02c311804c10c5a793e6568f8af4dead03264584d1"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5725dd9cc02068996d4438d397e255dcb1df776b7ceea3b9cb972bdb11260a83"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:99b37292234e61325e7a5bb9689e55e48c3f5f603af88b1642666277a81f1fbd"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:27b1d3b3915a99208fee9ab092b8184c420f2905b7d7feb4aeb5e4a9c509b8a1"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f612463ac081803f243ff13cccc648578e2279295048f2a8d5eb430af2bae6e3"}, + {file = "rpds_py-0.22.3-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f73d3fef726b3243a811121de45193c0ca75f6407fe66f3f4e183c983573e130"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3f21f0495edea7fdbaaa87e633a8689cd285f8f4af5c869f27bc8074638ad69c"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:1e9663daaf7a63ceccbbb8e3808fe90415b0757e2abddbfc2e06c857bf8c5e2b"}, + {file = "rpds_py-0.22.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a76e42402542b1fae59798fab64432b2d015ab9d0c8c47ba7addddbaf7952333"}, + {file = "rpds_py-0.22.3-cp313-cp313t-win32.whl", hash = "sha256:69803198097467ee7282750acb507fba35ca22cc3b85f16cf45fb01cb9097730"}, + {file = "rpds_py-0.22.3-cp313-cp313t-win_amd64.whl", hash = "sha256:f5cf2a0c2bdadf3791b5c205d55a37a54025c6e18a71c71f82bb536cf9a454bf"}, + {file = "rpds_py-0.22.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:378753b4a4de2a7b34063d6f95ae81bfa7b15f2c1a04a9518e8644e81807ebea"}, + {file = "rpds_py-0.22.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3445e07bf2e8ecfeef6ef67ac83de670358abf2996916039b16a218e3d95e97e"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b2513ba235829860b13faa931f3b6846548021846ac808455301c23a101689d"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eaf16ae9ae519a0e237a0f528fd9f0197b9bb70f40263ee57ae53c2b8d48aeb3"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:583f6a1993ca3369e0f80ba99d796d8e6b1a3a2a442dd4e1a79e652116413091"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4617e1915a539a0d9a9567795023de41a87106522ff83fbfaf1f6baf8e85437e"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c150c7a61ed4a4f4955a96626574e9baf1adf772c2fb61ef6a5027e52803543"}, + {file = "rpds_py-0.22.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fa4331c200c2521512595253f5bb70858b90f750d39b8cbfd67465f8d1b596d"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:214b7a953d73b5e87f0ebece4a32a5bd83c60a3ecc9d4ec8f1dca968a2d91e99"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f47ad3d5f3258bd7058d2d506852217865afefe6153a36eb4b6928758041d831"}, + {file = "rpds_py-0.22.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f276b245347e6e36526cbd4a266a417796fc531ddf391e43574cf6466c492520"}, + {file = "rpds_py-0.22.3-cp39-cp39-win32.whl", hash = "sha256:bbb232860e3d03d544bc03ac57855cd82ddf19c7a07651a7c0fdb95e9efea8b9"}, + {file = "rpds_py-0.22.3-cp39-cp39-win_amd64.whl", hash = "sha256:cfbc454a2880389dbb9b5b398e50d439e2e58669160f27b60e5eca11f68ae17c"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d48424e39c2611ee1b84ad0f44fb3b2b53d473e65de061e3f460fc0be5f1939d"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:24e8abb5878e250f2eb0d7859a8e561846f98910326d06c0d51381fed59357bd"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b232061ca880db21fa14defe219840ad9b74b6158adb52ddf0e87bead9e8493"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac0a03221cdb5058ce0167ecc92a8c89e8d0decdc9e99a2ec23380793c4dcb96"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb0c341fa71df5a4595f9501df4ac5abfb5a09580081dffbd1ddd4654e6e9123"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bf9db5488121b596dbfc6718c76092fda77b703c1f7533a226a5a9f65248f8ad"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b8db6b5b2d4491ad5b6bdc2bc7c017eec108acbf4e6785f42a9eb0ba234f4c9"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b3d504047aba448d70cf6fa22e06cb09f7cbd761939fdd47604f5e007675c24e"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e61b02c3f7a1e0b75e20c3978f7135fd13cb6cf551bf4a6d29b999a88830a338"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:e35ba67d65d49080e8e5a1dd40101fccdd9798adb9b050ff670b7d74fa41c566"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:26fd7cac7dd51011a245f29a2cc6489c4608b5a8ce8d75661bb4a1066c52dfbe"}, + {file = "rpds_py-0.22.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:177c7c0fce2855833819c98e43c262007f42ce86651ffbb84f37883308cb0e7d"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:bb47271f60660803ad11f4c61b42242b8c1312a31c98c578f79ef9387bbde21c"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:70fb28128acbfd264eda9bf47015537ba3fe86e40d046eb2963d75024be4d055"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44d61b4b7d0c2c9ac019c314e52d7cbda0ae31078aabd0f22e583af3e0d79723"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0e260eaf54380380ac3808aa4ebe2d8ca28b9087cf411649f96bad6900c728"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b25bc607423935079e05619d7de556c91fb6adeae9d5f80868dde3468657994b"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb6116dfb8d1925cbdb52595560584db42a7f664617a1f7d7f6e32f138cdf37d"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a63cbdd98acef6570c62b92a1e43266f9e8b21e699c363c0fef13bd530799c11"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b8f60e1b739a74bab7e01fcbe3dddd4657ec685caa04681df9d562ef15b625f"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:2e8b55d8517a2fda8d95cb45d62a5a8bbf9dd0ad39c5b25c8833efea07b880ca"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:2de29005e11637e7a2361fa151f780ff8eb2543a0da1413bb951e9f14b699ef3"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:666ecce376999bf619756a24ce15bb14c5bfaf04bf00abc7e663ce17c3f34fe7"}, + {file = "rpds_py-0.22.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5246b14ca64a8675e0a7161f7af68fe3e910e6b90542b4bfb5439ba752191df6"}, + {file = "rpds_py-0.22.3.tar.gz", hash = "sha256:e32fee8ab45d3c2db6da19a5323bc3362237c8b653c70194414b892fd06a080d"}, ] [[package]] @@ -3530,53 +3526,60 @@ timezone = ["pytz"] [[package]] name = "scipy" -version = "1.14.1" +version = "1.15.1" description = "Fundamental algorithms for scientific computing in Python" optional = false python-versions = ">=3.10" files = [ - {file = "scipy-1.14.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:b28d2ca4add7ac16ae8bb6632a3c86e4b9e4d52d3e34267f6e1b0c1f8d87e389"}, - {file = "scipy-1.14.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d0d2821003174de06b69e58cef2316a6622b60ee613121199cb2852a873f8cf3"}, - {file = "scipy-1.14.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8bddf15838ba768bb5f5083c1ea012d64c9a444e16192762bd858f1e126196d0"}, - {file = "scipy-1.14.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:97c5dddd5932bd2a1a31c927ba5e1463a53b87ca96b5c9bdf5dfd6096e27efc3"}, - {file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ff0a7e01e422c15739ecd64432743cf7aae2b03f3084288f399affcefe5222d"}, - {file = "scipy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e32dced201274bf96899e6491d9ba3e9a5f6b336708656466ad0522d8528f69"}, - {file = "scipy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8426251ad1e4ad903a4514712d2fa8fdd5382c978010d1c6f5f37ef286a713ad"}, - {file = "scipy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:a49f6ed96f83966f576b33a44257d869756df6cf1ef4934f59dd58b25e0327e5"}, - {file = "scipy-1.14.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:2da0469a4ef0ecd3693761acbdc20f2fdeafb69e6819cc081308cc978153c675"}, - {file = "scipy-1.14.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:c0ee987efa6737242745f347835da2cc5bb9f1b42996a4d97d5c7ff7928cb6f2"}, - {file = "scipy-1.14.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3a1b111fac6baec1c1d92f27e76511c9e7218f1695d61b59e05e0fe04dc59617"}, - {file = "scipy-1.14.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8475230e55549ab3f207bff11ebfc91c805dc3463ef62eda3ccf593254524ce8"}, - {file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:278266012eb69f4a720827bdd2dc54b2271c97d84255b2faaa8f161a158c3b37"}, - {file = "scipy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fef8c87f8abfb884dac04e97824b61299880c43f4ce675dd2cbeadd3c9b466d2"}, - {file = "scipy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b05d43735bb2f07d689f56f7b474788a13ed8adc484a85aa65c0fd931cf9ccd2"}, - {file = "scipy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:716e389b694c4bb564b4fc0c51bc84d381735e0d39d3f26ec1af2556ec6aad94"}, - {file = "scipy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:631f07b3734d34aced009aaf6fedfd0eb3498a97e581c3b1e5f14a04164a456d"}, - {file = "scipy-1.14.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:af29a935803cc707ab2ed7791c44288a682f9c8107bc00f0eccc4f92c08d6e07"}, - {file = "scipy-1.14.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:2843f2d527d9eebec9a43e6b406fb7266f3af25a751aa91d62ff416f54170bc5"}, - {file = "scipy-1.14.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:eb58ca0abd96911932f688528977858681a59d61a7ce908ffd355957f7025cfc"}, - {file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30ac8812c1d2aab7131a79ba62933a2a76f582d5dbbc695192453dae67ad6310"}, - {file = "scipy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f9ea80f2e65bdaa0b7627fb00cbeb2daf163caa015e59b7516395fe3bd1e066"}, - {file = "scipy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:edaf02b82cd7639db00dbff629995ef185c8df4c3ffa71a5562a595765a06ce1"}, - {file = "scipy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2ff38e22128e6c03ff73b6bb0f85f897d2362f8c052e3b8ad00532198fbdae3f"}, - {file = "scipy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1729560c906963fc8389f6aac023739ff3983e727b1a4d87696b7bf108316a79"}, - {file = "scipy-1.14.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:4079b90df244709e675cdc8b93bfd8a395d59af40b72e339c2287c91860deb8e"}, - {file = "scipy-1.14.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e0cf28db0f24a38b2a0ca33a85a54852586e43cf6fd876365c86e0657cfe7d73"}, - {file = "scipy-1.14.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0c2f95de3b04e26f5f3ad5bb05e74ba7f68b837133a4492414b3afd79dfe540e"}, - {file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b99722ea48b7ea25e8e015e8341ae74624f72e5f21fc2abd45f3a93266de4c5d"}, - {file = "scipy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5149e3fd2d686e42144a093b206aef01932a0059c2a33ddfa67f5f035bdfe13e"}, - {file = "scipy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e4f5a7c49323533f9103d4dacf4e4f07078f360743dec7f7596949149efeec06"}, - {file = "scipy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:baff393942b550823bfce952bb62270ee17504d02a1801d7fd0719534dfb9c84"}, - {file = "scipy-1.14.1.tar.gz", hash = "sha256:5a275584e726026a5699459aa72f828a610821006228e841b94275c4a7c08417"}, + {file = "scipy-1.15.1-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:c64ded12dcab08afff9e805a67ff4480f5e69993310e093434b10e85dc9d43e1"}, + {file = "scipy-1.15.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5b190b935e7db569960b48840e5bef71dc513314cc4e79a1b7d14664f57fd4ff"}, + {file = "scipy-1.15.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:4b17d4220df99bacb63065c76b0d1126d82bbf00167d1730019d2a30d6ae01ea"}, + {file = "scipy-1.15.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:63b9b6cd0333d0eb1a49de6f834e8aeaefe438df8f6372352084535ad095219e"}, + {file = "scipy-1.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f151e9fb60fbf8e52426132f473221a49362091ce7a5e72f8aa41f8e0da4f25"}, + {file = "scipy-1.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e10b1dd56ce92fba3e786007322542361984f8463c6d37f6f25935a5a6ef52"}, + {file = "scipy-1.15.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5dff14e75cdbcf07cdaa1c7707db6017d130f0af9ac41f6ce443a93318d6c6e0"}, + {file = "scipy-1.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:f82fcf4e5b377f819542fbc8541f7b5fbcf1c0017d0df0bc22c781bf60abc4d8"}, + {file = "scipy-1.15.1-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:5bd8d27d44e2c13d0c1124e6a556454f52cd3f704742985f6b09e75e163d20d2"}, + {file = "scipy-1.15.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:be3deeb32844c27599347faa077b359584ba96664c5c79d71a354b80a0ad0ce0"}, + {file = "scipy-1.15.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:5eb0ca35d4b08e95da99a9f9c400dc9f6c21c424298a0ba876fdc69c7afacedf"}, + {file = "scipy-1.15.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:74bb864ff7640dea310a1377d8567dc2cb7599c26a79ca852fc184cc851954ac"}, + {file = "scipy-1.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:667f950bf8b7c3a23b4199db24cb9bf7512e27e86d0e3813f015b74ec2c6e3df"}, + {file = "scipy-1.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395be70220d1189756068b3173853029a013d8c8dd5fd3d1361d505b2aa58fa7"}, + {file = "scipy-1.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce3a000cd28b4430426db2ca44d96636f701ed12e2b3ca1f2b1dd7abdd84b39a"}, + {file = "scipy-1.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:3fe1d95944f9cf6ba77aa28b82dd6bb2a5b52f2026beb39ecf05304b8392864b"}, + {file = "scipy-1.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c09aa9d90f3500ea4c9b393ee96f96b0ccb27f2f350d09a47f533293c78ea776"}, + {file = "scipy-1.15.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:0ac102ce99934b162914b1e4a6b94ca7da0f4058b6d6fd65b0cef330c0f3346f"}, + {file = "scipy-1.15.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:09c52320c42d7f5c7748b69e9f0389266fd4f82cf34c38485c14ee976cb8cb04"}, + {file = "scipy-1.15.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:cdde8414154054763b42b74fe8ce89d7f3d17a7ac5dd77204f0e142cdc9239e9"}, + {file = "scipy-1.15.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c9d8fc81d6a3b6844235e6fd175ee1d4c060163905a2becce8e74cb0d7554ce"}, + {file = "scipy-1.15.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fb57b30f0017d4afa5fe5f5b150b8f807618819287c21cbe51130de7ccdaed2"}, + {file = "scipy-1.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:491d57fe89927fa1aafbe260f4cfa5ffa20ab9f1435025045a5315006a91b8f5"}, + {file = "scipy-1.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:900f3fa3db87257510f011c292a5779eb627043dd89731b9c461cd16ef76ab3d"}, + {file = "scipy-1.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:100193bb72fbff37dbd0bf14322314fc7cbe08b7ff3137f11a34d06dc0ee6b85"}, + {file = "scipy-1.15.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:2114a08daec64980e4b4cbdf5bee90935af66d750146b1d2feb0d3ac30613692"}, + {file = "scipy-1.15.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:6b3e71893c6687fc5e29208d518900c24ea372a862854c9888368c0b267387ab"}, + {file = "scipy-1.15.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:837299eec3d19b7e042923448d17d95a86e43941104d33f00da7e31a0f715d3c"}, + {file = "scipy-1.15.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82add84e8a9fb12af5c2c1a3a3f1cb51849d27a580cb9e6bd66226195142be6e"}, + {file = "scipy-1.15.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:070d10654f0cb6abd295bc96c12656f948e623ec5f9a4eab0ddb1466c000716e"}, + {file = "scipy-1.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55cc79ce4085c702ac31e49b1e69b27ef41111f22beafb9b49fea67142b696c4"}, + {file = "scipy-1.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:c352c1b6d7cac452534517e022f8f7b8d139cd9f27e6fbd9f3cbd0bfd39f5bef"}, + {file = "scipy-1.15.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0458839c9f873062db69a03de9a9765ae2e694352c76a16be44f93ea45c28d2b"}, + {file = "scipy-1.15.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:af0b61c1de46d0565b4b39c6417373304c1d4f5220004058bdad3061c9fa8a95"}, + {file = "scipy-1.15.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:71ba9a76c2390eca6e359be81a3e879614af3a71dfdabb96d1d7ab33da6f2364"}, + {file = "scipy-1.15.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14eaa373c89eaf553be73c3affb11ec6c37493b7eaaf31cf9ac5dffae700c2e0"}, + {file = "scipy-1.15.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f735bc41bd1c792c96bc426dece66c8723283695f02df61dcc4d0a707a42fc54"}, + {file = "scipy-1.15.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2722a021a7929d21168830790202a75dbb20b468a8133c74a2c0230c72626b6c"}, + {file = "scipy-1.15.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bc7136626261ac1ed988dca56cfc4ab5180f75e0ee52e58f1e6aa74b5f3eacd5"}, + {file = "scipy-1.15.1.tar.gz", hash = "sha256:033a75ddad1463970c96a88063a1df87ccfddd526437136b6ee81ff0312ebdf6"}, ] [package.dependencies] -numpy = ">=1.23.5,<2.3" +numpy = ">=1.23.5,<2.5" [package.extras] dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy (==1.10.0)", "pycodestyle", "pydevtool", "rich-click", "ruff (>=0.0.292)", "types-psutil", "typing_extensions"] -doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.13.1)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<=7.3.7)", "sphinx-design (>=0.4.0)"] -test = ["Cython", "array-api-strict (>=2.0)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] +doc = ["intersphinx_registry", "jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.16.5)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0,<8.0.0)", "sphinx-copybutton", "sphinx-design (>=0.4.0)"] +test = ["Cython", "array-api-strict (>=2.0,<2.1.1)", "asv", "gmpy2", "hypothesis (>=6.30)", "meson", "mpmath", "ninja", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"] [[package]] name = "send2trash" @@ -3596,33 +3599,33 @@ win32 = ["pywin32"] [[package]] name = "setuptools" -version = "75.5.0" +version = "75.8.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" files = [ - {file = "setuptools-75.5.0-py3-none-any.whl", hash = "sha256:87cb777c3b96d638ca02031192d40390e0ad97737e27b6b4fa831bea86f2f829"}, - {file = "setuptools-75.5.0.tar.gz", hash = "sha256:5c4ccb41111392671f02bb5f8436dfc5a9a7185e80500531b133f5775c4163ef"}, + {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"}, + {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.7.0)"] -core = ["importlib-metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] +core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (>=1.12,<1.14)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] @@ -3649,72 +3652,72 @@ files = [ [[package]] name = "sqlalchemy" -version = "2.0.36" +version = "2.0.37" description = "Database Abstraction Library" optional = false python-versions = ">=3.7" files = [ - {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:59b8f3adb3971929a3e660337f5dacc5942c2cdb760afcabb2614ffbda9f9f72"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37350015056a553e442ff672c2d20e6f4b6d0b2495691fa239d8aa18bb3bc908"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8318f4776c85abc3f40ab185e388bee7a6ea99e7fa3a30686580b209eaa35c08"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c245b1fbade9c35e5bd3b64270ab49ce990369018289ecfde3f9c318411aaa07"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:69f93723edbca7342624d09f6704e7126b152eaed3cdbb634cb657a54332a3c5"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f9511d8dd4a6e9271d07d150fb2f81874a3c8c95e11ff9af3a2dfc35fe42ee44"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-win32.whl", hash = "sha256:c3f3631693003d8e585d4200730616b78fafd5a01ef8b698f6967da5c605b3fa"}, - {file = "SQLAlchemy-2.0.36-cp310-cp310-win_amd64.whl", hash = "sha256:a86bfab2ef46d63300c0f06936bd6e6c0105faa11d509083ba8f2f9d237fb5b5"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fd3a55deef00f689ce931d4d1b23fa9f04c880a48ee97af488fd215cf24e2a6c"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4f5e9cd989b45b73bd359f693b935364f7e1f79486e29015813c338450aa5a71"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ddd9db6e59c44875211bc4c7953a9f6638b937b0a88ae6d09eb46cced54eff"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2519f3a5d0517fc159afab1015e54bb81b4406c278749779be57a569d8d1bb0d"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59b1ee96617135f6e1d6f275bbe988f419c5178016f3d41d3c0abb0c819f75bb"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:39769a115f730d683b0eb7b694db9789267bcd027326cccc3125e862eb03bfd8"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-win32.whl", hash = "sha256:66bffbad8d6271bb1cc2f9a4ea4f86f80fe5e2e3e501a5ae2a3dc6a76e604e6f"}, - {file = "SQLAlchemy-2.0.36-cp311-cp311-win_amd64.whl", hash = "sha256:23623166bfefe1487d81b698c423f8678e80df8b54614c2bf4b4cfcd7c711959"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7b64e6ec3f02c35647be6b4851008b26cff592a95ecb13b6788a54ef80bbdd4"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46331b00096a6db1fdc052d55b101dbbfc99155a548e20a0e4a8e5e4d1362855"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdf3386a801ea5aba17c6410dd1dc8d39cf454ca2565541b5ac42a84e1e28f53"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac9dfa18ff2a67b09b372d5db8743c27966abf0e5344c555d86cc7199f7ad83a"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:90812a8933df713fdf748b355527e3af257a11e415b613dd794512461eb8a686"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1bc330d9d29c7f06f003ab10e1eaced295e87940405afe1b110f2eb93a233588"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-win32.whl", hash = "sha256:79d2e78abc26d871875b419e1fd3c0bca31a1cb0043277d0d850014599626c2e"}, - {file = "SQLAlchemy-2.0.36-cp312-cp312-win_amd64.whl", hash = "sha256:b544ad1935a8541d177cb402948b94e871067656b3a0b9e91dbec136b06a2ff5"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5cc79df7f4bc3d11e4b542596c03826063092611e481fcf1c9dfee3c94355ef"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3c01117dd36800f2ecaa238c65365b7b16497adc1522bf84906e5710ee9ba0e8"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc633f4ee4b4c46e7adcb3a9b5ec083bf1d9a97c1d3854b92749d935de40b9b"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e46ed38affdfc95d2c958de328d037d87801cfcbea6d421000859e9789e61c2"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b2985c0b06e989c043f1dc09d4fe89e1616aadd35392aea2844f0458a989eacf"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a121d62ebe7d26fec9155f83f8be5189ef1405f5973ea4874a26fab9f1e262c"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-win32.whl", hash = "sha256:0572f4bd6f94752167adfd7c1bed84f4b240ee6203a95e05d1e208d488d0d436"}, - {file = "SQLAlchemy-2.0.36-cp313-cp313-win_amd64.whl", hash = "sha256:8c78ac40bde930c60e0f78b3cd184c580f89456dd87fc08f9e3ee3ce8765ce88"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:be9812b766cad94a25bc63bec11f88c4ad3629a0cec1cd5d4ba48dc23860486b"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50aae840ebbd6cdd41af1c14590e5741665e5272d2fee999306673a1bb1fdb4d"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4557e1f11c5f653ebfdd924f3f9d5ebfc718283b0b9beebaa5dd6b77ec290971"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07b441f7d03b9a66299ce7ccf3ef2900abc81c0db434f42a5694a37bd73870f2"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:28120ef39c92c2dd60f2721af9328479516844c6b550b077ca450c7d7dc68575"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-win32.whl", hash = "sha256:b81ee3d84803fd42d0b154cb6892ae57ea6b7c55d8359a02379965706c7efe6c"}, - {file = "SQLAlchemy-2.0.36-cp37-cp37m-win_amd64.whl", hash = "sha256:f942a799516184c855e1a32fbc7b29d7e571b52612647866d4ec1c3242578fcb"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3d6718667da04294d7df1670d70eeddd414f313738d20a6f1d1f379e3139a545"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:72c28b84b174ce8af8504ca28ae9347d317f9dba3999e5981a3cd441f3712e24"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b11d0cfdd2b095e7b0686cf5fabeb9c67fae5b06d265d8180715b8cfa86522e3"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e32092c47011d113dc01ab3e1d3ce9f006a47223b18422c5c0d150af13a00687"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:6a440293d802d3011028e14e4226da1434b373cbaf4a4bbb63f845761a708346"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c54a1e53a0c308a8e8a7dffb59097bff7facda27c70c286f005327f21b2bd6b1"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-win32.whl", hash = "sha256:1e0d612a17581b6616ff03c8e3d5eff7452f34655c901f75d62bd86449d9750e"}, - {file = "SQLAlchemy-2.0.36-cp38-cp38-win_amd64.whl", hash = "sha256:8958b10490125124463095bbdadda5aa22ec799f91958e410438ad6c97a7b793"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dc022184d3e5cacc9579e41805a681187650e170eb2fd70e28b86192a479dcaa"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b817d41d692bf286abc181f8af476c4fbef3fd05e798777492618378448ee689"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4e46a888b54be23d03a89be510f24a7652fe6ff660787b96cd0e57a4ebcb46d"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4ae3005ed83f5967f961fd091f2f8c5329161f69ce8480aa8168b2d7fe37f06"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:03e08af7a5f9386a43919eda9de33ffda16b44eb11f3b313e6822243770e9763"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3dbb986bad3ed5ceaf090200eba750b5245150bd97d3e67343a3cfed06feecf7"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-win32.whl", hash = "sha256:9fe53b404f24789b5ea9003fc25b9a3988feddebd7e7b369c8fac27ad6f52f28"}, - {file = "SQLAlchemy-2.0.36-cp39-cp39-win_amd64.whl", hash = "sha256:af148a33ff0349f53512a049c6406923e4e02bf2f26c5fb285f143faf4f0e46a"}, - {file = "SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e"}, - {file = "sqlalchemy-2.0.36.tar.gz", hash = "sha256:7f2767680b6d2398aea7082e45a774b2b0767b5c8d8ffb9c8b683088ea9b29c5"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da36c3b0e891808a7542c5c89f224520b9a16c7f5e4d6a1156955605e54aef0e"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e7402ff96e2b073a98ef6d6142796426d705addd27b9d26c3b32dbaa06d7d069"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6f5d254a22394847245f411a2956976401e84da4288aa70cbcd5190744062c1"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41296bbcaa55ef5fdd32389a35c710133b097f7b2609d8218c0eabded43a1d84"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bedee60385c1c0411378cbd4dc486362f5ee88deceea50002772912d798bb00f"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:6c67415258f9f3c69867ec02fea1bf6508153709ecbd731a982442a590f2b7e4"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-win32.whl", hash = "sha256:650dcb70739957a492ad8acff65d099a9586b9b8920e3507ca61ec3ce650bb72"}, + {file = "SQLAlchemy-2.0.37-cp310-cp310-win_amd64.whl", hash = "sha256:93d1543cd8359040c02b6614421c8e10cd7a788c40047dbc507ed46c29ae5636"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:78361be6dc9073ed17ab380985d1e45e48a642313ab68ab6afa2457354ff692c"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b661b49d0cb0ab311a189b31e25576b7ac3e20783beb1e1817d72d9d02508bf5"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d57bafbab289e147d064ffbd5cca2d7b1394b63417c0636cea1f2e93d16eb9e8"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa2c0913f02341d25fb858e4fb2031e6b0813494cca1ba07d417674128ce11b"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9df21b8d9e5c136ea6cde1c50d2b1c29a2b5ff2b1d610165c23ff250e0704087"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db18ff6b8c0f1917f8b20f8eca35c28bbccb9f83afa94743e03d40203ed83de9"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-win32.whl", hash = "sha256:46954173612617a99a64aee103bcd3f078901b9a8dcfc6ae80cbf34ba23df989"}, + {file = "SQLAlchemy-2.0.37-cp311-cp311-win_amd64.whl", hash = "sha256:7b7e772dc4bc507fdec4ee20182f15bd60d2a84f1e087a8accf5b5b7a0dcf2ba"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2952748ecd67ed3b56773c185e85fc084f6bdcdec10e5032a7c25a6bc7d682ef"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3151822aa1db0eb5afd65ccfafebe0ef5cda3a7701a279c8d0bf17781a793bb4"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eaa8039b6d20137a4e02603aba37d12cd2dde7887500b8855356682fc33933f4"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cdba1f73b64530c47b27118b7053b8447e6d6f3c8104e3ac59f3d40c33aa9fd"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1b2690456528a87234a75d1a1644cdb330a6926f455403c8e4f6cad6921f9098"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf5ae8a9dcf657fd72144a7fd01f243236ea39e7344e579a121c4205aedf07bb"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-win32.whl", hash = "sha256:ea308cec940905ba008291d93619d92edaf83232ec85fbd514dcb329f3192761"}, + {file = "SQLAlchemy-2.0.37-cp312-cp312-win_amd64.whl", hash = "sha256:635d8a21577341dfe4f7fa59ec394b346da12420b86624a69e466d446de16aff"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8c4096727193762e72ce9437e2a86a110cf081241919ce3fab8e89c02f6b6658"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4fb5ac86d8fe8151966814f6720996430462e633d225497566b3996966b9bdb"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e56a139bfe136a22c438478a86f8204c1eb5eed36f4e15c4224e4b9db01cb3e4"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f95fc8e3f34b5f6b3effb49d10ac97c569ec8e32f985612d9b25dd12d0d2e94"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c505edd429abdfe3643fa3b2e83efb3445a34a9dc49d5f692dd087be966020e0"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:12b0f1ec623cccf058cf21cb544f0e74656618165b083d78145cafde156ea7b6"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-win32.whl", hash = "sha256:293f9ade06b2e68dd03cfb14d49202fac47b7bb94bffcff174568c951fbc7af2"}, + {file = "SQLAlchemy-2.0.37-cp313-cp313-win_amd64.whl", hash = "sha256:d70f53a0646cc418ca4853da57cf3ddddbccb8c98406791f24426f2dd77fd0e2"}, + {file = "SQLAlchemy-2.0.37-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:44f569d0b1eb82301b92b72085583277316e7367e038d97c3a1a899d9a05e342"}, + {file = "SQLAlchemy-2.0.37-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2eae3423e538c10d93ae3e87788c6a84658c3ed6db62e6a61bb9495b0ad16bb"}, + {file = "SQLAlchemy-2.0.37-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfff7be361048244c3aa0f60b5e63221c5e0f0e509f4e47b8910e22b57d10ae7"}, + {file = "SQLAlchemy-2.0.37-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:5bc3339db84c5fb9130ac0e2f20347ee77b5dd2596ba327ce0d399752f4fce39"}, + {file = "SQLAlchemy-2.0.37-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:84b9f23b0fa98a6a4b99d73989350a94e4a4ec476b9a7dfe9b79ba5939f5e80b"}, + {file = "SQLAlchemy-2.0.37-cp37-cp37m-win32.whl", hash = "sha256:51bc9cfef83e0ac84f86bf2b10eaccb27c5a3e66a1212bef676f5bee6ef33ebb"}, + {file = "SQLAlchemy-2.0.37-cp37-cp37m-win_amd64.whl", hash = "sha256:8e47f1af09444f87c67b4f1bb6231e12ba6d4d9f03050d7fc88df6d075231a49"}, + {file = "SQLAlchemy-2.0.37-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6b788f14c5bb91db7f468dcf76f8b64423660a05e57fe277d3f4fad7b9dcb7ce"}, + {file = "SQLAlchemy-2.0.37-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521ef85c04c33009166777c77e76c8a676e2d8528dc83a57836b63ca9c69dcd1"}, + {file = "SQLAlchemy-2.0.37-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:75311559f5c9881a9808eadbeb20ed8d8ba3f7225bef3afed2000c2a9f4d49b9"}, + {file = "SQLAlchemy-2.0.37-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cce918ada64c956b62ca2c2af59b125767097ec1dca89650a6221e887521bfd7"}, + {file = "SQLAlchemy-2.0.37-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9d087663b7e1feabea8c578d6887d59bb00388158e8bff3a76be11aa3f748ca2"}, + {file = "SQLAlchemy-2.0.37-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:cf95a60b36997dad99692314c4713f141b61c5b0b4cc5c3426faad570b31ca01"}, + {file = "SQLAlchemy-2.0.37-cp38-cp38-win32.whl", hash = "sha256:d75ead7dd4d255068ea0f21492ee67937bd7c90964c8f3c2bea83c7b7f81b95f"}, + {file = "SQLAlchemy-2.0.37-cp38-cp38-win_amd64.whl", hash = "sha256:74bbd1d0a9bacf34266a7907d43260c8d65d31d691bb2356f41b17c2dca5b1d0"}, + {file = "SQLAlchemy-2.0.37-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:648ec5acf95ad59255452ef759054f2176849662af4521db6cb245263ae4aa33"}, + {file = "SQLAlchemy-2.0.37-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:35bd2df269de082065d4b23ae08502a47255832cc3f17619a5cea92ce478b02b"}, + {file = "SQLAlchemy-2.0.37-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f581d365af9373a738c49e0c51e8b18e08d8a6b1b15cc556773bcd8a192fa8b"}, + {file = "SQLAlchemy-2.0.37-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82df02816c14f8dc9f4d74aea4cb84a92f4b0620235daa76dde002409a3fbb5a"}, + {file = "SQLAlchemy-2.0.37-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94b564e38b344d3e67d2e224f0aec6ba09a77e4582ced41e7bfd0f757d926ec9"}, + {file = "SQLAlchemy-2.0.37-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:955a2a765aa1bd81aafa69ffda179d4fe3e2a3ad462a736ae5b6f387f78bfeb8"}, + {file = "SQLAlchemy-2.0.37-cp39-cp39-win32.whl", hash = "sha256:03f0528c53ca0b67094c4764523c1451ea15959bbf0a8a8a3096900014db0278"}, + {file = "SQLAlchemy-2.0.37-cp39-cp39-win_amd64.whl", hash = "sha256:4b12885dc85a2ab2b7d00995bac6d967bffa8594123b02ed21e8eb2205a7584b"}, + {file = "SQLAlchemy-2.0.37-py3-none-any.whl", hash = "sha256:a8998bf9f8658bd3839cbc44ddbe982955641863da0c1efe5b00c1ab4f5c16b1"}, + {file = "sqlalchemy-2.0.37.tar.gz", hash = "sha256:12b28d99a9c14eaf4055810df1001557176716de0167b91026e648e65229bffb"}, ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} +greenlet = {version = "!=0.4.17", markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} typing-extensions = ">=4.6.0" [package.extras] @@ -3831,13 +3834,43 @@ test = ["pytest", "ruff"] [[package]] name = "tomli" -version = "2.1.0" +version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ - {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, - {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] @@ -3862,20 +3895,20 @@ files = [ [[package]] name = "tqdm" -version = "4.67.0" +version = "4.67.1" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.67.0-py3-none-any.whl", hash = "sha256:0cd8af9d56911acab92182e88d763100d4788bdf421d251616040cc4d44863be"}, - {file = "tqdm-4.67.0.tar.gz", hash = "sha256:fe5a6f95e6fe0b9755e9469b77b9c3cf850048224ecaa8293d7d2d31f97d869a"}, + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] discord = ["requests"] notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] @@ -3913,13 +3946,13 @@ numpy = "*" [[package]] name = "types-python-dateutil" -version = "2.9.0.20241003" +version = "2.9.0.20241206" description = "Typing stubs for python-dateutil" optional = false python-versions = ">=3.8" files = [ - {file = "types-python-dateutil-2.9.0.20241003.tar.gz", hash = "sha256:58cb85449b2a56d6684e41aeefb4c4280631246a0da1a719bdbe6f3fb0317446"}, - {file = "types_python_dateutil-2.9.0.20241003-py3-none-any.whl", hash = "sha256:250e1d8e80e7bbc3a6c99b907762711d1a1cdd00e978ad39cb5940f6f0a87f3d"}, + {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, + {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, ] [[package]] @@ -3960,13 +3993,13 @@ dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake [[package]] name = "urllib3" -version = "2.2.3" +version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, + {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, + {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] [package.extras] @@ -4054,172 +4087,174 @@ files = [ [[package]] name = "wrapt" -version = "1.16.0" +version = "1.17.1" description = "Module for decorators, wrappers and monkey patching." optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "wrapt-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4"}, - {file = "wrapt-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487"}, - {file = "wrapt-1.16.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0"}, - {file = "wrapt-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136"}, - {file = "wrapt-1.16.0-cp310-cp310-win32.whl", hash = "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d"}, - {file = "wrapt-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09"}, - {file = "wrapt-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060"}, - {file = "wrapt-1.16.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956"}, - {file = "wrapt-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d"}, - {file = "wrapt-1.16.0-cp311-cp311-win32.whl", hash = "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362"}, - {file = "wrapt-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b"}, - {file = "wrapt-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809"}, - {file = "wrapt-1.16.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9"}, - {file = "wrapt-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c"}, - {file = "wrapt-1.16.0-cp312-cp312-win32.whl", hash = "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc"}, - {file = "wrapt-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8"}, - {file = "wrapt-1.16.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c"}, - {file = "wrapt-1.16.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e"}, - {file = "wrapt-1.16.0-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465"}, - {file = "wrapt-1.16.0-cp36-cp36m-win32.whl", hash = "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e"}, - {file = "wrapt-1.16.0-cp36-cp36m-win_amd64.whl", hash = "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966"}, - {file = "wrapt-1.16.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5"}, - {file = "wrapt-1.16.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f"}, - {file = "wrapt-1.16.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win32.whl", hash = "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c"}, - {file = "wrapt-1.16.0-cp37-cp37m-win_amd64.whl", hash = "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0"}, - {file = "wrapt-1.16.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e"}, - {file = "wrapt-1.16.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca"}, - {file = "wrapt-1.16.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6"}, - {file = "wrapt-1.16.0-cp38-cp38-win32.whl", hash = "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b"}, - {file = "wrapt-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2"}, - {file = "wrapt-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c"}, - {file = "wrapt-1.16.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f"}, - {file = "wrapt-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537"}, - {file = "wrapt-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3"}, - {file = "wrapt-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35"}, - {file = "wrapt-1.16.0-py3-none-any.whl", hash = "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1"}, - {file = "wrapt-1.16.0.tar.gz", hash = "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d"}, + {file = "wrapt-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9176057c60438c2ce2284cdefc2b3ee5eddc8c87cd6e24c558d9f5c64298fa4a"}, + {file = "wrapt-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e0f0e731e0ca1583befd3af71b9f90d64ded1535da7b80181cb9e907cc10bbae"}, + {file = "wrapt-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:144ed42a4ec3aca5d6f1524f99ee49493bbd0d9c66c24da7ec44b4661dca4dcc"}, + {file = "wrapt-1.17.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8a7b0699a381226d81d75b48ea58414beb5891ba8982bdc8e42912f766de074"}, + {file = "wrapt-1.17.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b20fcef5a3ee410671a5a59472e1ff9dda21cfbe5dfd15e23ee4b99ac455c8e"}, + {file = "wrapt-1.17.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b9a58a1cbdc0588ed4c8ab0c191002d5d831a58c3bad88523fe471ea97eaf57d"}, + {file = "wrapt-1.17.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:50bbfa7a92da7540426c774e09d6901e44d8f9b513b276ebae03ae244f0c6dbf"}, + {file = "wrapt-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:09f5141599eaf36d6cc0b760ad87c2ab6b8618d009b2922639266676775a73a6"}, + {file = "wrapt-1.17.1-cp310-cp310-win32.whl", hash = "sha256:589f24449fd58508533c4a69b2a0f45e9e3419b86b43a0607e2fdb989c6f2552"}, + {file = "wrapt-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:7eca3a1afa9820785b79cb137c68ca38c2f77cfedc3120115da42e1d5800907e"}, + {file = "wrapt-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:da0d0c1c4bd55f9ace919454776dbf0821f537b9a77f739f0c3e34b14728b3b3"}, + {file = "wrapt-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cd7649f0c493d35f9aad9790bbecd7b6fd2e2f7141f6cb1e1e9bb7a681d6d0a4"}, + {file = "wrapt-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0aad4f54b3155d673a5c4706a71a0a84f3d415b2fc8a2a399a964d70f18846a2"}, + {file = "wrapt-1.17.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ebea3ebb6a394f50f150a52e279508e91c8770625ac8fcb5d8cf35995a320f2"}, + {file = "wrapt-1.17.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e2986a65eba7c399d7ad1ccd204562d4ffe6e937344fe5a49eb5a83858f797"}, + {file = "wrapt-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:67c30d3fe245adb0eb1061a0e526905970a0dabe7c5fba5078e0ee9d19f28167"}, + {file = "wrapt-1.17.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6fd88935b12b59a933ef45facb57575095f205d30d0ae8dd1a3b485bc4fa2fbd"}, + {file = "wrapt-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec3e763e7ca8dcba0792fc3e8ff7061186f59e9aafe4438e6bb1f635a6ab0901"}, + {file = "wrapt-1.17.1-cp311-cp311-win32.whl", hash = "sha256:d792631942a102d6d4f71e4948aceb307310ac0a0af054be6d28b4f79583e0f1"}, + {file = "wrapt-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:3dfd4738a630eddfcb7ff6c8e9fe863df3821f9c991dec73821e05450074ae09"}, + {file = "wrapt-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b1a4c8edd038fee0ce67bf119b16eaa45d22a52bbaf7d0a17d2312eb0003b1bb"}, + {file = "wrapt-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:181a844005c9818792212a32e004cb4c6bd8e35cae8e97b1a39a1918d95cef58"}, + {file = "wrapt-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:21ffcf16f5c243a626b0f8da637948e3d5984e3bc0c1bc500ad990e88e974e3b"}, + {file = "wrapt-1.17.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0eb33799b7582bb73787b9903b70595f8eff67eecc9455f668ed01adf53f9eea"}, + {file = "wrapt-1.17.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:57e932ad1908b53e9ad67a746432f02bc8473a9ee16e26a47645a2b224fba5fd"}, + {file = "wrapt-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b8bd35c15bc82c5cbe397e8196fa57a17ce5d3f30e925a6fd39e4c5bb02fdcff"}, + {file = "wrapt-1.17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:93018dbb956e0ad99ea2fa2c3c22f033549dcb1f56ad9f4555dfe25e49688c5d"}, + {file = "wrapt-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5bd9186d52cf3d36bf1823be0e85297e4dbad909bc6dd495ce0d272806d84a7"}, + {file = "wrapt-1.17.1-cp312-cp312-win32.whl", hash = "sha256:d609f0ab0603bbcbf2de906b366b9f9bec75c32b4493550a940de658cc2ce512"}, + {file = "wrapt-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:2c160bb8815787646b27a0c8575a26a4d6bf6abd7c5eb250ad3f2d38b29cb2cb"}, + {file = "wrapt-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:99e544e6ce26f89ad5acc6f407bc4daf7c1d42321e836f5c768f834100bdf35c"}, + {file = "wrapt-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:78da796b74f2c8e0af021ee99feb3bff7cb46f8e658fe25c20e66be1080db4a2"}, + {file = "wrapt-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f1bc359f6c52e53565e7af24b423e7a1eea97d155f38ac9e90e95303514710b"}, + {file = "wrapt-1.17.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cbead724daa13cae46e8ab3bb24938d8514d123f34345535b184f3eb1b7ad717"}, + {file = "wrapt-1.17.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdf7b0e3d3713331c0bb9daac47cd10e5aa60d060e53696f50de4e560bd5617f"}, + {file = "wrapt-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f17e8d926f63aed65ff949682c922f96d00f65c2e852c24272232313fa7823d5"}, + {file = "wrapt-1.17.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9e04f3bd30e0b23c0ca7e1d4084e7d28b6d7d2feb8b7bc69b496fe881280579b"}, + {file = "wrapt-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5660e470edfa15ae7ef407272c642d29e9962777a6b30bfa8fc0da2173dc9afd"}, + {file = "wrapt-1.17.1-cp313-cp313-win32.whl", hash = "sha256:a992f9e019145e84616048556546edeaba68e05e1c1ffbe8391067a63cdadb0c"}, + {file = "wrapt-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:5c2e24ba455af4b0a237a890ea6ed9bafd01fac2c47095f87c53ea3344215d43"}, + {file = "wrapt-1.17.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88623fd957ba500d8bb0f7427a76496d99313ca2f9e932481c0882e034cf1add"}, + {file = "wrapt-1.17.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:162d5f15bdd3b8037e06540902227ef9e0f298496c0afaadd9e2875851446693"}, + {file = "wrapt-1.17.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bb82447ddae4e3d9b51f40c494f66e6cbd8fb0e8e8b993678416535c67f9a0d"}, + {file = "wrapt-1.17.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ce4cff3922707048d754e365c4ebf41a3bcbf29b329349bf85d51873c7c7e9e"}, + {file = "wrapt-1.17.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fdc4e73a3fa0c25eed4d836d9732226f0326957cb075044a7f252b465299433"}, + {file = "wrapt-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:bca1c0824f824bcd97b4b179dd55dcad1dab419252be2b2faebbcacefa3b27b2"}, + {file = "wrapt-1.17.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6d44b14f3a2f6343a07c90344850b7af5515538ce3a5d01f9c87d8bae9bd8724"}, + {file = "wrapt-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:169033329022739c6f0d8cd3031a113953b0ba500f3d5978904bdd40baec4568"}, + {file = "wrapt-1.17.1-cp313-cp313t-win32.whl", hash = "sha256:52f0907287d9104112dbebda46af4db0793fcc4c64c8a867099212d116b6db64"}, + {file = "wrapt-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:7966f98fa36933333d8a1c3d8552aa3d0735001901a4aabcfbd5a502b4ef14fe"}, + {file = "wrapt-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:27a49f217839bf559d436308bae8fc4a9dd0ac98ffdb9d6aeb3f00385b0fb72c"}, + {file = "wrapt-1.17.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:50a4e3b45e62b1ccb96b3fc0e427f1b458ff2e0def34ae084de88418157a09d1"}, + {file = "wrapt-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30c0c08434fe2af6e40c5c75c036d7e3c7e7f499079fc479e740d9586b09fb0d"}, + {file = "wrapt-1.17.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15f96fe5e2efdc613983327240ae89cf6368c07eeb0f194d240e9549aa1ea739"}, + {file = "wrapt-1.17.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14f78f8c313884f889c6696af62aa881af302a989a7c0df398d2b541fa53e8a9"}, + {file = "wrapt-1.17.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d87334b521ab0e2564902c0b10039dee8670485e9d397fe97c34b88801f474f7"}, + {file = "wrapt-1.17.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:97eaff096fcb467e0f486f3bf354c1072245c2045859d71ba71158717ec97dcc"}, + {file = "wrapt-1.17.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:13887d1415dc0e213a9adeb9026ae1f427023f77110d988fbd478643490aa40c"}, + {file = "wrapt-1.17.1-cp38-cp38-win32.whl", hash = "sha256:823a262d967cbdf835787039b873ff551e36c14658bdc2e43267968b67f61f88"}, + {file = "wrapt-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:889587664d245dae75c752b643061f922e8a590d43a4cd088eca415ca83f2d13"}, + {file = "wrapt-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:997e8f9b984e4263993d3baf3329367e7c7673b63789bc761718a6f9ed68653d"}, + {file = "wrapt-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bac64f57a5a7926ebc9ab519fb9eba1fc6dcd1f65d7f45937b2ce38da65c2270"}, + {file = "wrapt-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a7aa07603d67007c15b33d20095cc9276f3e127bfb1b8106b3e84ec6907d137e"}, + {file = "wrapt-1.17.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c53ef8936c4d587cb96bb1cf0d076e822fa38266c2b646837ef60465da8db22e"}, + {file = "wrapt-1.17.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72053cc4706dac537d5a772135dc3e1de5aff52883f49994c1757c1b2dc9db2"}, + {file = "wrapt-1.17.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0ee037e4cc9d039efe712b13c483f4efa2c3499642369e01570b3bb1842eea3f"}, + {file = "wrapt-1.17.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20888d886186d19eab53816db2e615950b1ce7dbd5c239107daf2c8a6a4a03c6"}, + {file = "wrapt-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1c119802ae432b8c5d55dd5253825d09c1dca1c97ffc7b32c53ecdb348712f64"}, + {file = "wrapt-1.17.1-cp39-cp39-win32.whl", hash = "sha256:3260178f3bc006acae93378bfd6dbf33c9249de93cc1b78d8cc7b7416f4ea99a"}, + {file = "wrapt-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:18fb16fb6bb75f4ec6272829007f3129a9a5264d0230372f9651e5f75cfec552"}, + {file = "wrapt-1.17.1-py3-none-any.whl", hash = "sha256:f3117feb1fc479eaf84b549d3f229d5d2abdb823f003bc2a1c6dd70072912fa0"}, + {file = "wrapt-1.17.1.tar.gz", hash = "sha256:16b2fdfa09a74a3930175b6d9d7d008022aa72a4f02de2b3eecafcc1adfd3cfe"}, ] [[package]] name = "yarl" -version = "1.17.1" +version = "1.18.3" description = "Yet another URL library" optional = false python-versions = ">=3.9" files = [ - {file = "yarl-1.17.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b1794853124e2f663f0ea54efb0340b457f08d40a1cef78edfa086576179c91"}, - {file = "yarl-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fbea1751729afe607d84acfd01efd95e3b31db148a181a441984ce9b3d3469da"}, - {file = "yarl-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ee427208c675f1b6e344a1f89376a9613fc30b52646a04ac0c1f6587c7e46ec"}, - {file = "yarl-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b74ff4767d3ef47ffe0cd1d89379dc4d828d4873e5528976ced3b44fe5b0a21"}, - {file = "yarl-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62a91aefff3d11bf60e5956d340eb507a983a7ec802b19072bb989ce120cd948"}, - {file = "yarl-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:846dd2e1243407133d3195d2d7e4ceefcaa5f5bf7278f0a9bda00967e6326b04"}, - {file = "yarl-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e844be8d536afa129366d9af76ed7cb8dfefec99f5f1c9e4f8ae542279a6dc3"}, - {file = "yarl-1.17.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc7c92c1baa629cb03ecb0c3d12564f172218fb1739f54bf5f3881844daadc6d"}, - {file = "yarl-1.17.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ae3476e934b9d714aa8000d2e4c01eb2590eee10b9d8cd03e7983ad65dfbfcba"}, - {file = "yarl-1.17.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c7e177c619342e407415d4f35dec63d2d134d951e24b5166afcdfd1362828e17"}, - {file = "yarl-1.17.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64cc6e97f14cf8a275d79c5002281f3040c12e2e4220623b5759ea7f9868d6a5"}, - {file = "yarl-1.17.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:84c063af19ef5130084db70ada40ce63a84f6c1ef4d3dbc34e5e8c4febb20822"}, - {file = "yarl-1.17.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:482c122b72e3c5ec98f11457aeb436ae4aecca75de19b3d1de7cf88bc40db82f"}, - {file = "yarl-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:380e6c38ef692b8fd5a0f6d1fa8774d81ebc08cfbd624b1bca62a4d4af2f9931"}, - {file = "yarl-1.17.1-cp310-cp310-win32.whl", hash = "sha256:16bca6678a83657dd48df84b51bd56a6c6bd401853aef6d09dc2506a78484c7b"}, - {file = "yarl-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:561c87fea99545ef7d692403c110b2f99dced6dff93056d6e04384ad3bc46243"}, - {file = "yarl-1.17.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:cbad927ea8ed814622305d842c93412cb47bd39a496ed0f96bfd42b922b4a217"}, - {file = "yarl-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fca4b4307ebe9c3ec77a084da3a9d1999d164693d16492ca2b64594340999988"}, - {file = "yarl-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff5c6771c7e3511a06555afa317879b7db8d640137ba55d6ab0d0c50425cab75"}, - {file = "yarl-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b29beab10211a746f9846baa39275e80034e065460d99eb51e45c9a9495bcca"}, - {file = "yarl-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a52a1ffdd824fb1835272e125385c32fd8b17fbdefeedcb4d543cc23b332d74"}, - {file = "yarl-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58c8e9620eb82a189c6c40cb6b59b4e35b2ee68b1f2afa6597732a2b467d7e8f"}, - {file = "yarl-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d216e5d9b8749563c7f2c6f7a0831057ec844c68b4c11cb10fc62d4fd373c26d"}, - {file = "yarl-1.17.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:881764d610e3269964fc4bb3c19bb6fce55422828e152b885609ec176b41cf11"}, - {file = "yarl-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8c79e9d7e3d8a32d4824250a9c6401194fb4c2ad9a0cec8f6a96e09a582c2cc0"}, - {file = "yarl-1.17.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:299f11b44d8d3a588234adbe01112126010bd96d9139c3ba7b3badd9829261c3"}, - {file = "yarl-1.17.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:cc7d768260f4ba4ea01741c1b5fe3d3a6c70eb91c87f4c8761bbcce5181beafe"}, - {file = "yarl-1.17.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:de599af166970d6a61accde358ec9ded821234cbbc8c6413acfec06056b8e860"}, - {file = "yarl-1.17.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2b24ec55fad43e476905eceaf14f41f6478780b870eda5d08b4d6de9a60b65b4"}, - {file = "yarl-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9fb815155aac6bfa8d86184079652c9715c812d506b22cfa369196ef4e99d1b4"}, - {file = "yarl-1.17.1-cp311-cp311-win32.whl", hash = "sha256:7615058aabad54416ddac99ade09a5510cf77039a3b903e94e8922f25ed203d7"}, - {file = "yarl-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:14bc88baa44e1f84164a392827b5defb4fa8e56b93fecac3d15315e7c8e5d8b3"}, - {file = "yarl-1.17.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:327828786da2006085a4d1feb2594de6f6d26f8af48b81eb1ae950c788d97f61"}, - {file = "yarl-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cc353841428d56b683a123a813e6a686e07026d6b1c5757970a877195f880c2d"}, - {file = "yarl-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c73df5b6e8fabe2ddb74876fb82d9dd44cbace0ca12e8861ce9155ad3c886139"}, - {file = "yarl-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bdff5e0995522706c53078f531fb586f56de9c4c81c243865dd5c66c132c3b5"}, - {file = "yarl-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06157fb3c58f2736a5e47c8fcbe1afc8b5de6fb28b14d25574af9e62150fcaac"}, - {file = "yarl-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1654ec814b18be1af2c857aa9000de7a601400bd4c9ca24629b18486c2e35463"}, - {file = "yarl-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f6595c852ca544aaeeb32d357e62c9c780eac69dcd34e40cae7b55bc4fb1147"}, - {file = "yarl-1.17.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:459e81c2fb920b5f5df744262d1498ec2c8081acdcfe18181da44c50f51312f7"}, - {file = "yarl-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e48cdb8226644e2fbd0bdb0a0f87906a3db07087f4de77a1b1b1ccfd9e93685"}, - {file = "yarl-1.17.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d9b6b28a57feb51605d6ae5e61a9044a31742db557a3b851a74c13bc61de5172"}, - {file = "yarl-1.17.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e594b22688d5747b06e957f1ef822060cb5cb35b493066e33ceac0cf882188b7"}, - {file = "yarl-1.17.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5f236cb5999ccd23a0ab1bd219cfe0ee3e1c1b65aaf6dd3320e972f7ec3a39da"}, - {file = "yarl-1.17.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a2a64e62c7a0edd07c1c917b0586655f3362d2c2d37d474db1a509efb96fea1c"}, - {file = "yarl-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d0eea830b591dbc68e030c86a9569826145df485b2b4554874b07fea1275a199"}, - {file = "yarl-1.17.1-cp312-cp312-win32.whl", hash = "sha256:46ddf6e0b975cd680eb83318aa1d321cb2bf8d288d50f1754526230fcf59ba96"}, - {file = "yarl-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:117ed8b3732528a1e41af3aa6d4e08483c2f0f2e3d3d7dca7cf538b3516d93df"}, - {file = "yarl-1.17.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5d1d42556b063d579cae59e37a38c61f4402b47d70c29f0ef15cee1acaa64488"}, - {file = "yarl-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0167540094838ee9093ef6cc2c69d0074bbf84a432b4995835e8e5a0d984374"}, - {file = "yarl-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2f0a6423295a0d282d00e8701fe763eeefba8037e984ad5de44aa349002562ac"}, - {file = "yarl-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5b078134f48552c4d9527db2f7da0b5359abd49393cdf9794017baec7506170"}, - {file = "yarl-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d401f07261dc5aa36c2e4efc308548f6ae943bfff20fcadb0a07517a26b196d8"}, - {file = "yarl-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5f1ac7359e17efe0b6e5fec21de34145caef22b260e978336f325d5c84e6938"}, - {file = "yarl-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f63d176a81555984e91f2c84c2a574a61cab7111cc907e176f0f01538e9ff6e"}, - {file = "yarl-1.17.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e275792097c9f7e80741c36de3b61917aebecc08a67ae62899b074566ff8556"}, - {file = "yarl-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:81713b70bea5c1386dc2f32a8f0dab4148a2928c7495c808c541ee0aae614d67"}, - {file = "yarl-1.17.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:aa46dce75078fceaf7cecac5817422febb4355fbdda440db55206e3bd288cfb8"}, - {file = "yarl-1.17.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1ce36ded585f45b1e9bb36d0ae94765c6608b43bd2e7f5f88079f7a85c61a4d3"}, - {file = "yarl-1.17.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2d374d70fdc36f5863b84e54775452f68639bc862918602d028f89310a034ab0"}, - {file = "yarl-1.17.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2d9f0606baaec5dd54cb99667fcf85183a7477f3766fbddbe3f385e7fc253299"}, - {file = "yarl-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b0341e6d9a0c0e3cdc65857ef518bb05b410dbd70d749a0d33ac0f39e81a4258"}, - {file = "yarl-1.17.1-cp313-cp313-win32.whl", hash = "sha256:2e7ba4c9377e48fb7b20dedbd473cbcbc13e72e1826917c185157a137dac9df2"}, - {file = "yarl-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:949681f68e0e3c25377462be4b658500e85ca24323d9619fdc41f68d46a1ffda"}, - {file = "yarl-1.17.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8994b29c462de9a8fce2d591028b986dbbe1b32f3ad600b2d3e1c482c93abad6"}, - {file = "yarl-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f9cbfbc5faca235fbdf531b93aa0f9f005ec7d267d9d738761a4d42b744ea159"}, - {file = "yarl-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b40d1bf6e6f74f7c0a567a9e5e778bbd4699d1d3d2c0fe46f4b717eef9e96b95"}, - {file = "yarl-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5efe0661b9fcd6246f27957f6ae1c0eb29bc60552820f01e970b4996e016004"}, - {file = "yarl-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5c4804e4039f487e942c13381e6c27b4b4e66066d94ef1fae3f6ba8b953f383"}, - {file = "yarl-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5d6a6c9602fd4598fa07e0389e19fe199ae96449008d8304bf5d47cb745462e"}, - {file = "yarl-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4c9156c4d1eb490fe374fb294deeb7bc7eaccda50e23775b2354b6a6739934"}, - {file = "yarl-1.17.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6324274b4e0e2fa1b3eccb25997b1c9ed134ff61d296448ab8269f5ac068c4c"}, - {file = "yarl-1.17.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d8a8b74d843c2638f3864a17d97a4acda58e40d3e44b6303b8cc3d3c44ae2d29"}, - {file = "yarl-1.17.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:7fac95714b09da9278a0b52e492466f773cfe37651cf467a83a1b659be24bf71"}, - {file = "yarl-1.17.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c180ac742a083e109c1a18151f4dd8675f32679985a1c750d2ff806796165b55"}, - {file = "yarl-1.17.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:578d00c9b7fccfa1745a44f4eddfdc99d723d157dad26764538fbdda37209857"}, - {file = "yarl-1.17.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1a3b91c44efa29e6c8ef8a9a2b583347998e2ba52c5d8280dbd5919c02dfc3b5"}, - {file = "yarl-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a7ac5b4984c468ce4f4a553df281450df0a34aefae02e58d77a0847be8d1e11f"}, - {file = "yarl-1.17.1-cp39-cp39-win32.whl", hash = "sha256:7294e38f9aa2e9f05f765b28ffdc5d81378508ce6dadbe93f6d464a8c9594473"}, - {file = "yarl-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:eb6dce402734575e1a8cc0bb1509afca508a400a57ce13d306ea2c663bad1138"}, - {file = "yarl-1.17.1-py3-none-any.whl", hash = "sha256:f1790a4b1e8e8e028c391175433b9c8122c39b46e1663228158e61e6f915bf06"}, - {file = "yarl-1.17.1.tar.gz", hash = "sha256:067a63fcfda82da6b198fa73079b1ca40b7c9b7994995b6ee38acda728b64d47"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7"}, + {file = "yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc"}, + {file = "yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b"}, + {file = "yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690"}, + {file = "yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6"}, + {file = "yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193"}, + {file = "yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae"}, + {file = "yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e"}, + {file = "yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a"}, + {file = "yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1"}, + {file = "yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576"}, + {file = "yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba"}, + {file = "yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393"}, + {file = "yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285"}, + {file = "yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2"}, + {file = "yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:90adb47ad432332d4f0bc28f83a5963f426ce9a1a8809f5e584e704b82685dcb"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:913829534200eb0f789d45349e55203a091f45c37a2674678744ae52fae23efa"}, + {file = "yarl-1.18.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ef9f7768395923c3039055c14334ba4d926f3baf7b776c923c93d80195624782"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a19f62ff30117e706ebc9090b8ecc79aeb77d0b1f5ec10d2d27a12bc9f66d0"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e17c9361d46a4d5addf777c6dd5eab0715a7684c2f11b88c67ac37edfba6c482"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a74a13a4c857a84a845505fd2d68e54826a2cd01935a96efb1e9d86c728e186"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41f7ce59d6ee7741af71d82020346af364949314ed3d87553763a2df1829cc58"}, + {file = "yarl-1.18.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f52a265001d830bc425f82ca9eabda94a64a4d753b07d623a9f2863fde532b53"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:82123d0c954dc58db301f5021a01854a85bf1f3bb7d12ae0c01afc414a882ca2"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:2ec9bbba33b2d00999af4631a3397d1fd78290c48e2a3e52d8dd72db3a067ac8"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fbd6748e8ab9b41171bb95c6142faf068f5ef1511935a0aa07025438dd9a9bc1"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:877d209b6aebeb5b16c42cbb377f5f94d9e556626b1bfff66d7b0d115be88d0a"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b464c4ab4bfcb41e3bfd3f1c26600d038376c2de3297760dfe064d2cb7ea8e10"}, + {file = "yarl-1.18.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8d39d351e7faf01483cc7ff7c0213c412e38e5a340238826be7e0e4da450fdc8"}, + {file = "yarl-1.18.3-cp313-cp313-win32.whl", hash = "sha256:61ee62ead9b68b9123ec24bc866cbef297dd266175d53296e2db5e7f797f902d"}, + {file = "yarl-1.18.3-cp313-cp313-win_amd64.whl", hash = "sha256:578e281c393af575879990861823ef19d66e2b1d0098414855dd367e234f5b3c"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:61e5e68cb65ac8f547f6b5ef933f510134a6bf31bb178be428994b0cb46c2a04"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe57328fbc1bfd0bd0514470ac692630f3901c0ee39052ae47acd1d90a436719"}, + {file = "yarl-1.18.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a440a2a624683108a1b454705ecd7afc1c3438a08e890a1513d468671d90a04e"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c7907c8548bcd6ab860e5f513e727c53b4a714f459b084f6580b49fa1b9cee"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4f6450109834af88cb4cc5ecddfc5380ebb9c228695afc11915a0bf82116789"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9ca04806f3be0ac6d558fffc2fdf8fcef767e0489d2684a21912cc4ed0cd1b8"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77a6e85b90a7641d2e07184df5557132a337f136250caafc9ccaa4a2a998ca2c"}, + {file = "yarl-1.18.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6333c5a377c8e2f5fae35e7b8f145c617b02c939d04110c76f29ee3676b5f9a5"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0b3c92fa08759dbf12b3a59579a4096ba9af8dd344d9a813fc7f5070d86bbab1"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:4ac515b860c36becb81bb84b667466885096b5fc85596948548b667da3bf9f24"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:045b8482ce9483ada4f3f23b3774f4e1bf4f23a2d5c912ed5170f68efb053318"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:a4bb030cf46a434ec0225bddbebd4b89e6471814ca851abb8696170adb163985"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:54d6921f07555713b9300bee9c50fb46e57e2e639027089b1d795ecd9f7fa910"}, + {file = "yarl-1.18.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1d407181cfa6e70077df3377938c08012d18893f9f20e92f7d2f314a437c30b1"}, + {file = "yarl-1.18.3-cp39-cp39-win32.whl", hash = "sha256:ac36703a585e0929b032fbaab0707b75dc12703766d0b53486eabd5139ebadd5"}, + {file = "yarl-1.18.3-cp39-cp39-win_amd64.whl", hash = "sha256:ba87babd629f8af77f557b61e49e7c7cac36f22f871156b91e10a6e9d4f829e9"}, + {file = "yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b"}, + {file = "yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1"}, ] [package.dependencies] From c7abed43df51691973cccfa57ea9719aa491fd92 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Sat, 11 Jan 2025 12:19:49 +0100 Subject: [PATCH 6/9] Pin poetry version to 1.7.1 --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1b0022ca..5ba15d4f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -69,6 +69,7 @@ jobs: - name: Install Poetry uses: snok/install-poetry@v1 with: + version: 1.7.1 virtualenvs-create: true virtualenvs-in-project: true #---------------------------------------------- From 47ffc41181ff93a3aa7737ad18a6bc94b94dc199 Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Sat, 11 Jan 2025 14:22:55 +0100 Subject: [PATCH 7/9] Fix failing tests --- investing_algorithm_framework/app/app.py | 36 ++++++++++--------- investing_algorithm_framework/create_app.py | 10 ++++-- tests/resources/test_base.py | 2 ++ tests/services/test_portfolio_sync_service.py | 13 ++++--- 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/investing_algorithm_framework/app/app.py b/investing_algorithm_framework/app/app.py index 7b79a35a..0fe5ee79 100644 --- a/investing_algorithm_framework/app/app.py +++ b/investing_algorithm_framework/app/app.py @@ -39,10 +39,9 @@ def on_run(self, app, algorithm: Algorithm): class App: - def __init__(self, state_handler=None, web=False): + def __init__(self, state_handler=None): self._flask_app: Optional[Flask] = None self.container = None - self._web = web self._algorithm: Optional[Algorithm] = None self._started = False self._tasks = [] @@ -134,13 +133,8 @@ def initialize_config(self): config = configuration_service.get_config() - if APP_MODE not in config or config[APP_MODE] is None: - if self._web: - configuration_service.add_value(APP_MODE, AppMode.WEB.value) - else: - configuration_service.add_value( - APP_MODE, AppMode.DEFAULT.value - ) + if APP_MODE not in config: + configuration_service.add_value(APP_MODE, AppMode.DEFAULT.value) def initialize(self): """ @@ -198,10 +192,13 @@ def initialize(self): for strategy in self.algorithm.strategies: - for market_data_source in strategy.market_data_sources: - market_data_source_service.add(market_data_source) + if strategy.market_data_sources is not None: + for market_data_source in strategy.market_data_sources: + market_data_source_service.add(market_data_source) + + config = self.container.configuration_service().get_config() - if self._web: + if config[APP_MODE] == AppMode.WEB.value: self._configuration_service.add_value( APP_MODE, AppMode.WEB.value ) @@ -322,6 +319,15 @@ def _initialize_app_for_backtest( configuration_service.add_value( BACKTESTING_END_DATE, backtest_date_range.end_date ) + configuration_service.add_value( + DATABASE_NAME, "backtest-database.sqlite3" + ) + configuration_service.add_value( + DATABASE_DIRECTORY_PATH, + os.path.join( + configuration_service.config[RESOURCE_DIRECTORY], "backtest_databases" + ) + ) if pending_order_check_interval is not None: configuration_service.add_value( @@ -330,7 +336,7 @@ def _initialize_app_for_backtest( ) # Create resource dir if not exits - self._create_resource_directory_if_not_exists() + self._create_resources_if_not_exists() def _create_backtest_database_if_not_exists(self): """ @@ -644,10 +650,6 @@ def add_portfolio_configuration(self, portfolio_configuration): .portfolio_configuration_service() portfolio_configuration_service.add(portfolio_configuration) - @property - def web(self): - return self._web - @property def running(self): return self.algorithm.running diff --git a/investing_algorithm_framework/create_app.py b/investing_algorithm_framework/create_app.py index 991c32c3..054fd585 100644 --- a/investing_algorithm_framework/create_app.py +++ b/investing_algorithm_framework/create_app.py @@ -3,14 +3,15 @@ from .app import App from .dependency_container import setup_dependency_container +from .domain import APP_MODE, AppMode logger = logging.getLogger("investing_algorithm_framework") def create_app( config: dict = None, - web=False, - state_handler=None + state_handler=None, + web: bool = False ) -> App: """ Factory method to create an app instance. @@ -26,7 +27,7 @@ def create_app( # Load the environment variables load_dotenv() - app = App(web=web, state_handler=state_handler) + app = App(state_handler=state_handler) app = setup_dependency_container( app, ["investing_algorithm_framework"], @@ -38,5 +39,8 @@ def create_app( if config is not None: app.set_config_with_dict(config) + if web: + app.set_config("APP_MODE", AppMode.WEB.value) + logger.info("Investing algoritm framework app created") return app diff --git a/tests/resources/test_base.py b/tests/resources/test_base.py index cdfaf627..485593d0 100644 --- a/tests/resources/test_base.py +++ b/tests/resources/test_base.py @@ -70,6 +70,8 @@ def setUp(self) -> None: for market_credential in self.market_credentials: self.app.add_market_credential(market_credential) + self.app.initialize_config() + if self.initialize: self.app.initialize() diff --git a/tests/services/test_portfolio_sync_service.py b/tests/services/test_portfolio_sync_service.py index 1ba3a387..d03c17d9 100644 --- a/tests/services/test_portfolio_sync_service.py +++ b/tests/services/test_portfolio_sync_service.py @@ -1,8 +1,5 @@ -from datetime import datetime - from investing_algorithm_framework import PortfolioConfiguration, Algorithm, \ - MarketCredential, OperationalException, RESERVED_BALANCES, APP_MODE, \ - Order, SYMBOLS, AppMode + MarketCredential, OperationalException, RESERVED_BALANCES from tests.resources import TestBase @@ -36,6 +33,7 @@ def test_sync_unallocated(self): ) self.market_service.balances = {"EUR": 1000} self.app.add_algorithm(Algorithm()) + self.app.initialize_config() self.app.initialize() portfolio = self.app.container.portfolio_service()\ @@ -76,12 +74,11 @@ def test_sync_unallocated_with_no_balance(self): self.app.add_algorithm(Algorithm()) with self.assertRaises(OperationalException) as context: + self.app.initialize_config() self.app.initialize() self.assertEqual( - "The initial balance of the portfolio configuration is more than" " the available balance on the exchange. Please make sure" - " that the initial balance of the portfolio configuration" - " is less than the available balance on the exchange.", + "The initial balance of the portfolio configuration (1000.0 EUR) is more than the available balance on the exchange. Please make sure that the initial balance of the portfolio configuration is less than the available balance on the exchange 0.0 EUR.", str(context.exception) ) @@ -105,6 +102,7 @@ def test_sync_unallocated_with_reserved(self): ) self.market_service.balances = {"EUR": 1200} self.app.add_algorithm(Algorithm()) + self.app.initialize_config() self.app.initialize() portfolio = self.app.container.portfolio_service() \ @@ -129,6 +127,7 @@ def test_sync_unallocated_with_initial_size(self): ) self.market_service.balances = {"EUR": 1200} self.app.add_algorithm(Algorithm()) + self.app.initialize_config() self.app.initialize() portfolio = self.app.container.portfolio_service() \ From ad6257b1ce428479a565f2797db629ff6e477e6e Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Sat, 11 Jan 2025 14:25:09 +0100 Subject: [PATCH 8/9] Fix flake 8 warnings --- investing_algorithm_framework/app/app.py | 5 ++++- investing_algorithm_framework/create_app.py | 2 +- tests/resources/test_base.py | 4 +++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/investing_algorithm_framework/app/app.py b/investing_algorithm_framework/app/app.py index 0fe5ee79..4201df2e 100644 --- a/investing_algorithm_framework/app/app.py +++ b/investing_algorithm_framework/app/app.py @@ -325,7 +325,10 @@ def _initialize_app_for_backtest( configuration_service.add_value( DATABASE_DIRECTORY_PATH, os.path.join( - configuration_service.config[RESOURCE_DIRECTORY], "backtest_databases" + configuration_service.config[ + RESOURCE_DIRECTORY + ], + "backtest_databases" ) ) diff --git a/investing_algorithm_framework/create_app.py b/investing_algorithm_framework/create_app.py index 054fd585..8f34271a 100644 --- a/investing_algorithm_framework/create_app.py +++ b/investing_algorithm_framework/create_app.py @@ -3,7 +3,7 @@ from .app import App from .dependency_container import setup_dependency_container -from .domain import APP_MODE, AppMode +from .domain import AppMode logger = logging.getLogger("investing_algorithm_framework") diff --git a/tests/resources/test_base.py b/tests/resources/test_base.py index 485593d0..c747b3a9 100644 --- a/tests/resources/test_base.py +++ b/tests/resources/test_base.py @@ -153,7 +153,9 @@ class FlaskTestBase(FlaskTestCase): def create_app(self): self.resource_directory = os.path.dirname(__file__) self.iaf_app: App = create_app( - {RESOURCE_DIRECTORY: self.resource_directory}, web=True + { + RESOURCE_DIRECTORY: self.resource_directory + } ) self.market_service.balances = self.external_balances self.market_service.orders = self.external_orders From 0142def9ba8a736c8ee1b98b6ad72df5b47a1fac Mon Sep 17 00:00:00 2001 From: marcvanduyn Date: Sat, 11 Jan 2025 20:08:42 +0100 Subject: [PATCH 9/9] Fix flask test base --- tests/resources/test_base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/resources/test_base.py b/tests/resources/test_base.py index c747b3a9..f899c0cc 100644 --- a/tests/resources/test_base.py +++ b/tests/resources/test_base.py @@ -155,7 +155,8 @@ def create_app(self): self.iaf_app: App = create_app( { RESOURCE_DIRECTORY: self.resource_directory - } + }, + web=True ) self.market_service.balances = self.external_balances self.market_service.orders = self.external_orders