diff --git a/.flake8 b/.flake8 index 79a16af..0a87c13 100644 --- a/.flake8 +++ b/.flake8 @@ -1,2 +1,8 @@ [flake8] -max-line-length = 120 \ No newline at end of file +max-line-length = 120 +exclude = + .git, + .eggs, + __pycache__, + doc/examples, + **/__init__.py \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..6067dc7 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,26 @@ +name: Run CI make command + +on: + push: + branches: + - "*" + - "!master" + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build + working-directory: . + run: make ci \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..ed6fff5 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,27 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + + + { + "name": "Python Debugger: Current File", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "justMyCode": false + }, + { + "name": "Pytest", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal", + "purpose": ["debug-test"], + "justMyCode": false + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..1fafce7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", + "python.testing.unittestArgs": [ + "-v", + "tests/test_*.py" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "python.terminal.activateEnvironment": true, + "python.terminal.activateEnvInCurrentTerminal": true, + "python.analysis.exclude": [ + "**/.venv", + "**/docker", + "**/mypy_cache", + "**/pytest_cache" + ] +} \ No newline at end of file diff --git a/Makefile b/Makefile index f30cd6d..450ff1d 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,12 @@ SHELL := /bin/bash -e -o pipefail -PROJECT ?= fx-lib +PROJECT ?= pytrade BRANCH_NAME ?= local BUILD_NUMBER ?= 0 IMAGE ?= ${PROJECT}:${BRANCH_NAME}-${BUILD_NUMBER} COMPOSE_FILE=docker/docker-compose.yaml COMPOSE_BASE_FILE=docker/docker-compose.base.yaml DC=docker compose -p ${PROJECT} -f ${COMPOSE_FILE} -f ${COMPOSE_BASE_FILE} -SERVICE := fx_lib +SERVICE := pytrade POETRY ?= "poetry" VERSION := $(shell head VERSION | grep -Eo "\d+.\d+.\d+") diff --git a/docker/Dockerfile b/docker/Dockerfile index 303f885..6cd6273 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -42,7 +42,7 @@ RUN apt-get update \ curl \ build-essential -ENV PATH="/app/${PROJECT_DIR}/.venv/bin:$PATH" +ENV PATH="/app/.venv/bin:$PATH" ENV APP_USER app ENV APP_GROUP app RUN groupadd ${APP_GROUP} @@ -55,7 +55,7 @@ COPY --from=base /app . WORKDIR /app/${PROJECT_DIR} USER ${APP_USER} -ENTRYPOINT ["/app/${PROJECT_DIR}/scripts/entrypoint.sh"] +ENTRYPOINT ["/app/scripts/entrypoint.sh"] ################################################## ## DEVELOPMENT ## @@ -63,7 +63,7 @@ ENTRYPOINT ["/app/${PROJECT_DIR}/scripts/entrypoint.sh"] FROM base as development -ENV PATH="/app/${PROJECT_DIR}/.venv/bin:$PATH" +ENV PATH="/app/.venv/bin:$PATH" ENV APP_USER app ENV APP_GROUP app RUN groupadd ${APP_GROUP} @@ -81,4 +81,4 @@ RUN poetry install RUN chown ${APP_USER}:${APP_GROUP} . USER ${APP_USER} -ENTRYPOINT ["/app/${PROJECT_DIR}/scripts/entrypoint.sh"] \ No newline at end of file +ENTRYPOINT ["/app/scripts/entrypoint.sh"] \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index b1ce8d2..291dd73 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -44,4 +44,4 @@ services: extends: file: docker-compose.base.yaml service: base - entrypoint: ["/app/fx-lib/scripts/test.sh"] \ No newline at end of file + entrypoint: ["/app/scripts/test.sh"] \ No newline at end of file diff --git a/fx_lib/_util.py b/fx_lib/_util.py deleted file mode 100644 index 5a9b454..0000000 --- a/fx_lib/_util.py +++ /dev/null @@ -1,207 +0,0 @@ -import warnings -from numbers import Number -from typing import Dict, List, Optional, Sequence, Union, cast - -import numpy as np -import pandas as pd - - -def try_(lazy_func, default=None, exception=Exception): - try: - return lazy_func() - except exception: - return default - - -def _as_str(value) -> str: - if isinstance(value, (Number, str)): - return str(value) - if isinstance(value, pd.DataFrame): - return "df" - name = str(getattr(value, "name", "") or "") - if name in ("Open", "High", "Low", "Close", "Volume"): - return name[:1] - if callable(value): - name = getattr(value, "__name__", value.__class__.__name__).replace( - "", "λ" - ) - if len(name) > 10: - name = name[:9] + "…" - return name - - -def _as_list(value) -> List: - if isinstance(value, Sequence) and not isinstance(value, str): - return list(value) - return [value] - - -def _data_period(index) -> Union[pd.Timedelta, Number]: - """Return data index period as pd.Timedelta""" - values = pd.Series(index[-100:]) - return values.diff().dropna().median() - - -class _Array(np.ndarray): - """ - ndarray extended to supply .name and other arbitrary properties - in ._opts dict. - """ - - def __new__(cls, array, *, name=None, **kwargs): - obj = np.asarray(array).view(cls) - obj.name = name or array.name - obj._opts = kwargs - return obj - - def __array_finalize__(self, obj): - if obj is not None: - self.name = getattr(obj, "name", "") - self._opts = getattr(obj, "_opts", {}) - - # Make sure properties name and _opts are carried over - # when (un-)pickling. - def __reduce__(self): - value = super().__reduce__() - return value[:2] + (value[2] + (self.__dict__,),) - - def __setstate__(self, state): - self.__dict__.update(state[-1]) - super().__setstate__(state[:-1]) - - def __bool__(self): - try: - return bool(self[-1]) - except IndexError: - return super().__bool__() - - def __float__(self): - try: - return float(self[-1]) - except IndexError: - return super().__float__() - - def to_series(self): - warnings.warn( - "`.to_series()` is deprecated. For pd.Series conversion, use accessor `.s`" - ) - return self.s - - @property - def s(self) -> pd.Series: - values = np.atleast_2d(self) - index = self._opts["index"][: values.shape[1]] - return pd.Series(values[0], index=index, name=self.name) - - @property - def df(self) -> pd.DataFrame: - values = np.atleast_2d(np.asarray(self)) - index = self._opts["index"][: values.shape[1]] - df = pd.DataFrame(values.T, index=index, columns=[self.name] * len(values)) - return df - - -class _Indicator(_Array): - pass - - -class _Data: - """ - A data array accessor. Provides access to OHLCV "columns" - as a standard `pd.DataFrame` would, except it's not a DataFrame - and the returned "series" are _not_ `pd.Series` but `np.ndarray` - for performance reasons. - """ - - def __init__(self, df: pd.DataFrame): - self.__df = df - self.__i = len(df) - self.__pip: Optional[float] = None - self.__cache: Dict[str, _Array] = {} - self.__arrays: Dict[str, _Array] = {} - self._update() - - def __getitem__(self, item): - return self.__get_array(item) - - def __getattr__(self, item): - try: - return self.__get_array(item) - except KeyError: - raise AttributeError(f"Column '{item}' not in data") from None - - def _set_length(self, i): - self.__i = i - self.__cache.clear() - - def _update(self): - index = self.__df.index.copy() - self.__arrays = { - col: _Array(arr, index=index) for col, arr in self.__df.items() - } - # Leave index as Series because pd.Timestamp nicer API to work with - self.__arrays["__index"] = index - - def __repr__(self): - i = min(self.__i, len(self.__df)) - 1 - index = self.__arrays["__index"][i] - items = ", ".join(f"{k}={v}" for k, v in self.__df.iloc[i].items()) - return f"" - - def __len__(self): - return self.__i - - @property - def df(self) -> pd.DataFrame: - return self.__df.iloc[: self.__i] if self.__i < len(self.__df) else self.__df - - @property - def pip(self) -> float: - if self.__pip is None: - self.__pip = float( - 10 - ** -np.median( - [ - len(s.partition(".")[-1]) - for s in self.__arrays["Close"].astype(str) - ] - ) - ) - return self.__pip - - def __get_array(self, key) -> _Array: - arr = self.__cache.get(key) - if arr is None: - arr = self.__cache[key] = cast(_Array, self.__arrays[key][: self.__i]) - return arr - - @property - def Open(self) -> _Array: - return self.__get_array("Open") - - @property - def High(self) -> _Array: - return self.__get_array("High") - - @property - def Low(self) -> _Array: - return self.__get_array("Low") - - @property - def Close(self) -> _Array: - return self.__get_array("Close") - - @property - def Volume(self) -> _Array: - return self.__get_array("Volume") - - @property - def index(self) -> _Array: - return self.__get_array("__index") - - # Make pickling in Backtest.optimize() work with our catch-all __getattr__ - def __getstate__(self): - return self.__dict__ - - def __setstate__(self, state): - self.__dict__ = state diff --git a/fx_lib/broker.py b/fx_lib/broker.py deleted file mode 100644 index cccb60d..0000000 --- a/fx_lib/broker.py +++ /dev/null @@ -1,22 +0,0 @@ -from fx_lib.interfaces.broker import IBroker - - -class Broker(IBroker): - - def __init__(self, client): - self.client = client - - def new_order(self, size, limit, stop, sl, tp, tag): - raise NotImplementedError - - @property - def equity(self) -> float: - raise NotImplementedError - # return self._cash + sum(trade.pl for trade in self.trades) - - @property - def margin_available(self) -> float: - raise NotImplementedError - # From https://github.com/QuantConnect/Lean/pull/3768 - # margin_used = sum(trade.value / self._leverage for trade in self.trades) - # return max(0, self.equity - margin_used) diff --git a/fx_lib/models/granularity.py b/fx_lib/models/granularity.py deleted file mode 100644 index 8c6b8b0..0000000 --- a/fx_lib/models/granularity.py +++ /dev/null @@ -1,7 +0,0 @@ -from enum import Enum - - -class Granularity(Enum): - M1 = "M1" - M5 = "M5" - M15 = "M15" diff --git a/fx_lib/models/granularity.pyi b/fx_lib/models/granularity.pyi deleted file mode 100644 index eccd692..0000000 --- a/fx_lib/models/granularity.pyi +++ /dev/null @@ -1,6 +0,0 @@ -from enum import Enum - -class Granularity(Enum): - M1: str - M5: str - M15: str diff --git a/fx_lib/models/instruments.py b/fx_lib/models/instruments.py deleted file mode 100644 index 808b1a7..0000000 --- a/fx_lib/models/instruments.py +++ /dev/null @@ -1,24 +0,0 @@ -from enum import Enum - - -class Instrument(Enum): - AUDJPY = "AUD/JPY" - AUDNZD = "AUD/NZD" - AUDUSD = "AUD/USD" - CADJPY = "CAD/JPY" - CHFJPY = "CHF/JPY" - EURCHF = "EUR/CHF" - EURGBP = "EUR/GBP" - EURJPY = "EUR/JPY" - EURPLN = "EUR/PLN" - EURUSD = "EUR/USD" - GBPJPY = "GBP/JPY" - GBPUSD = "GBP/USD" - NZDUSD = "NZD/USD" - USDCAD = "USD/CAD" - USDCHF = "USD/CHF" - USDJPY = "USD/JPY" - USDMXN = "USD/MXN" - USDRUB = "USD/RUB" - USDTRY = "USD/TRY" - USDZAR = "USD/ZAR" diff --git a/fx_lib/models/instruments.pyi b/fx_lib/models/instruments.pyi deleted file mode 100644 index 0eba412..0000000 --- a/fx_lib/models/instruments.pyi +++ /dev/null @@ -1,23 +0,0 @@ -from enum import Enum - -class Instrument(Enum): - AUDJPY: str - AUDNZD: str - AUDUSD: str - CADJPY: str - CHFJPY: str - EURCHF: str - EURGBP: str - EURJPY: str - EURPLN: str - EURUSD: str - GBPJPY: str - GBPUSD: str - NZDUSD: str - USDCAD: str - USDCHF: str - USDJPY: str - USDMXN: str - USDRUB: str - USDTRY: str - USDZAR: str diff --git a/fx_lib/strategy.py b/fx_lib/strategy.py deleted file mode 100644 index f707ed8..0000000 --- a/fx_lib/strategy.py +++ /dev/null @@ -1,295 +0,0 @@ -import sys -from abc import ABCMeta, abstractmethod -from itertools import chain -from typing import Callable, Optional - -import numpy as np -import pandas as pd - -from fx_lib.broker import Broker - -from ._util import _as_str, _Data, _Indicator, try_ - - -class Strategy(metaclass=ABCMeta): - """ - A trading strategy base class. Extend this class and - override methods - `backtesting.backtesting.Strategy.init` and - `backtesting.backtesting.Strategy.next` to define - your own strategy. - """ - - def __init__(self, broker, data, params): - self._indicators = [] - self._broker: Broker = broker - self._data: _Data = data - self._params = self._check_params(params) - - def __repr__(self): - return "" - - def __str__(self): - params = ",".join( - f"{i[0]}={i[1]}" - for i in zip(self._params.keys(), map(_as_str, self._params.values())) - ) - if params: - params = "(" + params + ")" - return f"{self.__class__.__name__}{params}" - - def _check_params(self, params): - for k, v in params.items(): - if not hasattr(self, k): - raise AttributeError( - f"Strategy '{self.__class__.__name__}' is missing parameter '{k}'." - "Strategy class should define parameters as class variables before they " - "can be optimized or run with." - ) - setattr(self, k, v) - return params - - def I( # noqa: E743 - self, - func: Callable, - *args, - name=None, - plot=True, - overlay=None, - color=None, - scatter=False, - **kwargs, - ) -> np.ndarray: - """ - Declare an indicator. An indicator is just an array of values, - but one that is revealed gradually in - `backtesting.backtesting.Strategy.next` much like - `backtesting.backtesting.Strategy.data` is. - Returns `np.ndarray` of indicator values. - - `func` is a function that returns the indicator array(s) of - same length as `backtesting.backtesting.Strategy.data`. - - In the plot legend, the indicator is labeled with - function name, unless `name` overrides it. - - If `plot` is `True`, the indicator is plotted on the resulting - `backtesting.backtesting.Backtest.plot`. - - If `overlay` is `True`, the indicator is plotted overlaying the - price candlestick chart (suitable e.g. for moving averages). - If `False`, the indicator is plotted standalone below the - candlestick chart. By default, a heuristic is used which decides - correctly most of the time. - - `color` can be string hex RGB triplet or X11 color name. - By default, the next available color is assigned. - - If `scatter` is `True`, the plotted indicator marker will be a - circle instead of a connected line segment (default). - - Additional `*args` and `**kwargs` are passed to `func` and can - be used for parameters. - - For example, using simple moving average function from TA-Lib: - - def init(): - self.sma = self.I(ta.SMA, self.data.Close, self.n_sma) - """ - if name is None: - params = ",".join(filter(None, map(_as_str, chain(args, kwargs.values())))) - func_name = _as_str(func) - name = f"{func_name}({params})" if params else f"{func_name}" - else: - name = name.format( - *map(_as_str, args), - **dict(zip(kwargs.keys(), map(_as_str, kwargs.values()))), - ) - - try: - value = func(*args, **kwargs) - except Exception as e: - raise RuntimeError(f'Indicator "{name}" error') from e - - if isinstance(value, pd.DataFrame): - value = value.values.T - - if value is not None: - value = try_(lambda: np.asarray(value, order="C"), None) - is_arraylike = bool(value is not None and value.shape) - - # Optionally flip the array if the user returned e.g. `df.values` - if is_arraylike and np.argmax(value.shape) == 0: - value = value.T - - if ( - not is_arraylike - or not 1 <= value.ndim <= 2 - or value.shape[-1] != len(self._data.Close) - ): - raise ValueError( - "Indicators must return (optionally a tuple of) numpy.arrays of same " - f'length as `data` (data shape: {self._data.Close.shape}; indicator "{name}" ' - f'shape: {getattr(value, "shape", "")}, returned value: {value})' - ) - - if plot and overlay is None and np.issubdtype(value.dtype, np.number): - x = value / self._data.Close - # By default, overlay if strong majority of indicator values - # is within 30% of Close - with np.errstate(invalid="ignore"): - overlay = ((x < 1.4) & (x > 0.6)).mean() > 0.6 - - value = _Indicator( - value, - name=name, - plot=plot, - overlay=overlay, - color=color, - scatter=scatter, - # _Indicator.s Series accessor uses this: - index=self.data.index, - ) - self._indicators.append(value) - return value - - @abstractmethod - def init(self): - """ - Initialize the strategy. - Override this method. - Declare indicators (with `backtesting.backtesting.Strategy.I`). - Precompute what needs to be precomputed or can be precomputed - in a vectorized fashion before the strategy starts. - - If you extend composable strategies from `backtesting.lib`, - make sure to call: - - super().init() - """ - - @abstractmethod - def next(self): - """ - Main strategy runtime method, called as each new - `backtesting.backtesting.Strategy.data` - instance (row; full candlestick bar) becomes available. - This is the main method where strategy decisions - upon data precomputed in `backtesting.backtesting.Strategy.init` - take place. - - If you extend composable strategies from `backtesting.lib`, - make sure to call: - - super().next() - """ - - class __FULL_EQUITY(float): # noqa: N801 - def __repr__(self): - return ".9999" - - _FULL_EQUITY = __FULL_EQUITY(1 - sys.float_info.epsilon) - - def buy( - self, - *, - size: float = _FULL_EQUITY, - limit: Optional[float] = None, - stop: Optional[float] = None, - sl: Optional[float] = None, - tp: Optional[float] = None, - tag: object = None, - ): - """ - Place a new long order. For explanation of parameters, see `Order` and its properties. - - See `Position.close()` and `Trade.close()` for closing existing positions. - - See also `Strategy.sell()`. - """ - size_valid = 0 < size < 1 or round(size) == size - if not size_valid: - raise RuntimeError( - "size must be a positive fraction of equity, or a positive whole number of units" - ) - return self._broker.new_order(size, limit, stop, sl, tp, tag) - - def sell( - self, - *, - size: float = _FULL_EQUITY, - limit: Optional[float] = None, - stop: Optional[float] = None, - sl: Optional[float] = None, - tp: Optional[float] = None, - tag: object = None, - ): - """ - Place a new short order. For explanation of parameters, see `Order` and its properties. - - See also `Strategy.buy()`. - - .. note:: - If you merely want to close an existing long position, - use `Position.close()` or `Trade.close()`. - """ - size_valid = 0 < size < 1 or round(size) == size - if not size_valid: - raise RuntimeError( - "size must be a positive fraction of equity, or a positive whole number of units" - ) - return self._broker.new_order(-size, limit, stop, sl, tp, tag) - - @property - def equity(self) -> float: - """Current account equity (cash plus assets).""" - return self._broker.equity - - @property - def data(self) -> _Data: - """ - Price data, roughly as passed into - `backtesting.backtesting.Backtest.__init__`, - but with two significant exceptions: - - * `data` is _not_ a DataFrame, but a custom structure - that serves customized numpy arrays for reasons of performance - and convenience. Besides OHLCV columns, `.index` and length, - it offers `.pip` property, the smallest price unit of change. - * Within `backtesting.backtesting.Strategy.init`, `data` arrays - are available in full length, as passed into - `backtesting.backtesting.Backtest.__init__` - (for precomputing indicators and such). However, within - `backtesting.backtesting.Strategy.next`, `data` arrays are - only as long as the current iteration, simulating gradual - price point revelation. In each call of - `backtesting.backtesting.Strategy.next` (iteratively called by - `backtesting.backtesting.Backtest` internally), - the last array value (e.g. `data.Close[-1]`) - is always the _most recent_ value. - * If you need data arrays (e.g. `data.Close`) to be indexed - **Pandas series**, you can call their `.s` accessor - (e.g. `data.Close.s`). If you need the whole of data - as a **DataFrame**, use `.df` accessor (i.e. `data.df`). - """ - return self._data - - # @property - # def position(self) -> Position: - # """Instance of `backtesting.backtesting.Position`.""" - # return self._broker.position - - # @property - # def orders(self) -> Tuple[Order, ...]: - # """List of orders (see `Order`) waiting for execution.""" - # return _Orders(self._broker.orders) - - # @property - # def trades(self) -> Tuple[Trade, ...]: - # """List of active trades (see `Trade`).""" - # return tuple(self._broker.trades) - - # @property - # def closed_trades(self) -> Tuple[Trade, ...]: - # """List of settled trades (see `Trade`).""" - # return tuple(self._broker.closed_trades) diff --git a/makefiles/development.mk b/makefiles/development.mk index 6ad5571..858926e 100644 --- a/makefiles/development.mk +++ b/makefiles/development.mk @@ -7,10 +7,10 @@ venv: buid: ##@development Build the docker images build: prod_image ?= ${PROJECT}:${BRANCH_NAME}-${BUILD_NUMBER} build: dev_image ?= ${PROJECT}_development:${BRANCH_NAME}-${BUILD_NUMBER} -build: args ?= -f docker/Dockerfile --build-arg PROJECT_DIR=${PROJECT} --network=host --build-arg BUILDKIT_INLINE_CACHE=1 +build: args ?= -f docker/Dockerfile --build-arg PROJECT_DIR=. --network=host --build-arg BUILDKIT_INLINE_CACHE=1 build: - DOCKER_BUILDKIT=1 docker build --progress=plain --target production -t ${prod_image} ${args} .. - DOCKER_BUILDKIT=1 docker build --progress=plain --target development -t ${dev_image} --cache-from ${prod_image} ${args} .. + DOCKER_BUILDKIT=1 docker build --progress=plain --target production -t ${prod_image} ${args} . + DOCKER_BUILDKIT=1 docker build --progress=plain --target development -t ${dev_image} --cache-from ${prod_image} ${args} . .PHONY: infrastructure infrastructure: ##@development Set up infrastructure for tests diff --git a/poetry.lock b/poetry.lock index 34b05b8..a6731e4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,14 +1,34 @@ # This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +[[package]] +name = "backtesting" +version = "0.3.3" +description = "Backtest trading strategies in Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "Backtesting-0.3.3.tar.gz", hash = "sha256:b2511993ae16596c06d3cfd3d42a662ca69d51a95a604b85705fb1a900e3a798"}, +] + +[package.dependencies] +bokeh = ">=1.4.0" +numpy = ">=1.17.0" +pandas = ">0.25.0" + +[package.extras] +dev = ["coverage", "flake8", "mypy"] +doc = ["ipykernel", "jupyter_client", "jupytext (>=1.3)", "nbconvert", "pdoc3"] +test = ["matplotlib", "scikit-learn", "scikit-optimize", "seaborn"] + [[package]] name = "bandit" -version = "1.7.8" +version = "1.7.9" description = "Security oriented static analyser for python code." optional = false python-versions = ">=3.8" files = [ - {file = "bandit-1.7.8-py3-none-any.whl", hash = "sha256:509f7af645bc0cd8fd4587abc1a038fc795636671ee8204d502b933aee44f381"}, - {file = "bandit-1.7.8.tar.gz", hash = "sha256:36de50f720856ab24a24dbaa5fee2c66050ed97c1477e0a1159deab1775eab6b"}, + {file = "bandit-1.7.9-py3-none-any.whl", hash = "sha256:52077cb339000f337fb25f7e045995c4ad01511e716e5daac37014b9752de8ec"}, + {file = "bandit-1.7.9.tar.gz", hash = "sha256:7c395a436743018f7be0a4cbb0a4ea9b902b6d87264ddecf8cfdc73b4f78ff61"}, ] [package.dependencies] @@ -26,33 +46,33 @@ yaml = ["PyYAML"] [[package]] name = "black" -version = "24.3.0" +version = "24.4.2" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"}, - {file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"}, - {file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"}, - {file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"}, - {file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"}, - {file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"}, - {file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"}, - {file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"}, - {file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"}, - {file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"}, - {file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"}, - {file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"}, - {file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"}, - {file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"}, - {file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"}, - {file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"}, - {file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"}, - {file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"}, - {file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"}, - {file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"}, - {file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"}, - {file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"}, + {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, + {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, + {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, + {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, + {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, + {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, + {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, + {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, + {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, + {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, + {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, + {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, + {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, + {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, + {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, + {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, + {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, + {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, + {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, + {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, + {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, + {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, ] [package.dependencies] @@ -70,6 +90,28 @@ d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "bokeh" +version = "3.5.0" +description = "Interactive plots and applications in the browser from Python" +optional = false +python-versions = ">=3.10" +files = [ + {file = "bokeh-3.5.0-py3-none-any.whl", hash = "sha256:1a1c7d35aa9aba1ae86916e92c1d9f19d706ba1c4929e0ff8647902b32e25689"}, + {file = "bokeh-3.5.0.tar.gz", hash = "sha256:65e89addbe900c37af25a2052ae174b6d3ba1ef08c91fd5ab6dfd712e184c399"}, +] + +[package.dependencies] +contourpy = ">=1.2" +Jinja2 = ">=2.9" +numpy = ">=1.16" +packaging = ">=16.8" +pandas = ">=1.2" +pillow = ">=7.1.0" +PyYAML = ">=3.10" +tornado = ">=6.2" +xyzservices = ">=2021.09.1" + [[package]] name = "click" version = "8.1.7" @@ -95,65 +137,128 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "contourpy" +version = "1.2.1" +description = "Python library for calculating contours of 2D quadrilateral grids" +optional = false +python-versions = ">=3.9" +files = [ + {file = "contourpy-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd7c23df857d488f418439686d3b10ae2fbf9bc256cd045b37a8c16575ea1040"}, + {file = "contourpy-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b9eb0ca724a241683c9685a484da9d35c872fd42756574a7cfbf58af26677fd"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c75507d0a55378240f781599c30e7776674dbaf883a46d1c90f37e563453480"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11959f0ce4a6f7b76ec578576a0b61a28bdc0696194b6347ba3f1c53827178b9"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb3315a8a236ee19b6df481fc5f997436e8ade24a9f03dfdc6bd490fea20c6da"}, + {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39f3ecaf76cd98e802f094e0d4fbc6dc9c45a8d0c4d185f0f6c2234e14e5f75b"}, + {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:94b34f32646ca0414237168d68a9157cb3889f06b096612afdd296003fdd32fd"}, + {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:457499c79fa84593f22454bbd27670227874cd2ff5d6c84e60575c8b50a69619"}, + {file = "contourpy-1.2.1-cp310-cp310-win32.whl", hash = "sha256:ac58bdee53cbeba2ecad824fa8159493f0bf3b8ea4e93feb06c9a465d6c87da8"}, + {file = "contourpy-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9cffe0f850e89d7c0012a1fb8730f75edd4320a0a731ed0c183904fe6ecfc3a9"}, + {file = "contourpy-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6022cecf8f44e36af10bd9118ca71f371078b4c168b6e0fab43d4a889985dbb5"}, + {file = "contourpy-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef5adb9a3b1d0c645ff694f9bca7702ec2c70f4d734f9922ea34de02294fdf72"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6150ffa5c767bc6332df27157d95442c379b7dce3a38dff89c0f39b63275696f"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c863140fafc615c14a4bf4efd0f4425c02230eb8ef02784c9a156461e62c965"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00e5388f71c1a0610e6fe56b5c44ab7ba14165cdd6d695429c5cd94021e390b2"}, + {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4492d82b3bc7fbb7e3610747b159869468079fe149ec5c4d771fa1f614a14df"}, + {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49e70d111fee47284d9dd867c9bb9a7058a3c617274900780c43e38d90fe1205"}, + {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b59c0ffceff8d4d3996a45f2bb6f4c207f94684a96bf3d9728dbb77428dd8cb8"}, + {file = "contourpy-1.2.1-cp311-cp311-win32.whl", hash = "sha256:7b4182299f251060996af5249c286bae9361fa8c6a9cda5efc29fe8bfd6062ec"}, + {file = "contourpy-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2855c8b0b55958265e8b5888d6a615ba02883b225f2227461aa9127c578a4922"}, + {file = "contourpy-1.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:62828cada4a2b850dbef89c81f5a33741898b305db244904de418cc957ff05dc"}, + {file = "contourpy-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:309be79c0a354afff9ff7da4aaed7c3257e77edf6c1b448a779329431ee79d7e"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e785e0f2ef0d567099b9ff92cbfb958d71c2d5b9259981cd9bee81bd194c9a4"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cac0a8f71a041aa587410424ad46dfa6a11f6149ceb219ce7dd48f6b02b87a7"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af3f4485884750dddd9c25cb7e3915d83c2db92488b38ccb77dd594eac84c4a0"}, + {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce6889abac9a42afd07a562c2d6d4b2b7134f83f18571d859b25624a331c90b"}, + {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a1eea9aecf761c661d096d39ed9026574de8adb2ae1c5bd7b33558af884fb2ce"}, + {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:187fa1d4c6acc06adb0fae5544c59898ad781409e61a926ac7e84b8f276dcef4"}, + {file = "contourpy-1.2.1-cp312-cp312-win32.whl", hash = "sha256:c2528d60e398c7c4c799d56f907664673a807635b857df18f7ae64d3e6ce2d9f"}, + {file = "contourpy-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:1a07fc092a4088ee952ddae19a2b2a85757b923217b7eed584fdf25f53a6e7ce"}, + {file = "contourpy-1.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bb6834cbd983b19f06908b45bfc2dad6ac9479ae04abe923a275b5f48f1a186b"}, + {file = "contourpy-1.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1d59e739ab0e3520e62a26c60707cc3ab0365d2f8fecea74bfe4de72dc56388f"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd3db01f59fdcbce5b22afad19e390260d6d0222f35a1023d9adc5690a889364"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a12a813949e5066148712a0626895c26b2578874e4cc63160bb007e6df3436fe"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe0ccca550bb8e5abc22f530ec0466136379c01321fd94f30a22231e8a48d985"}, + {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1d59258c3c67c865435d8fbeb35f8c59b8bef3d6f46c1f29f6123556af28445"}, + {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f32c38afb74bd98ce26de7cc74a67b40afb7b05aae7b42924ea990d51e4dac02"}, + {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d31a63bc6e6d87f77d71e1abbd7387ab817a66733734883d1fc0021ed9bfa083"}, + {file = "contourpy-1.2.1-cp39-cp39-win32.whl", hash = "sha256:ddcb8581510311e13421b1f544403c16e901c4e8f09083c881fab2be80ee31ba"}, + {file = "contourpy-1.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:10a37ae557aabf2509c79715cd20b62e4c7c28b8cd62dd7d99e5ed3ce28c3fd9"}, + {file = "contourpy-1.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a31f94983fecbac95e58388210427d68cd30fe8a36927980fab9c20062645609"}, + {file = "contourpy-1.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef2b055471c0eb466033760a521efb9d8a32b99ab907fc8358481a1dd29e3bd3"}, + {file = "contourpy-1.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b33d2bc4f69caedcd0a275329eb2198f560b325605810895627be5d4b876bf7f"}, + {file = "contourpy-1.2.1.tar.gz", hash = "sha256:4d8908b3bee1c889e547867ca4cdc54e5ab6be6d3e078556814a22457f49423c"}, +] + +[package.dependencies] +numpy = ">=1.20" + +[package.extras] +bokeh = ["bokeh", "selenium"] +docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] +mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.8.0)", "types-Pillow"] +test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] +test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] + [[package]] name = "coverage" -version = "7.4.4" +version = "7.5.4" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.8" files = [ - {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, - {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, - {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, - {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, - {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, - {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, - {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, - {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, - {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, - {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, - {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, - {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, - {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, - {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, - {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, - {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, - {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, - {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, - {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, - {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, - {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, - {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, - {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, - {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, - {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, - {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, - {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, - {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, - {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, - {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, - {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, - {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, + {file = "coverage-7.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cfb5a4f556bb51aba274588200a46e4dd6b505fb1a5f8c5ae408222eb416f99"}, + {file = "coverage-7.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2174e7c23e0a454ffe12267a10732c273243b4f2d50d07544a91198f05c48f47"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2214ee920787d85db1b6a0bd9da5f8503ccc8fcd5814d90796c2f2493a2f4d2e"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1137f46adb28e3813dec8c01fefadcb8c614f33576f672962e323b5128d9a68d"}, + {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b385d49609f8e9efc885790a5a0e89f2e3ae042cdf12958b6034cc442de428d3"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4a474f799456e0eb46d78ab07303286a84a3140e9700b9e154cfebc8f527016"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5cd64adedf3be66f8ccee418473c2916492d53cbafbfcff851cbec5a8454b136"}, + {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e564c2cf45d2f44a9da56f4e3a26b2236504a496eb4cb0ca7221cd4cc7a9aca9"}, + {file = "coverage-7.5.4-cp310-cp310-win32.whl", hash = "sha256:7076b4b3a5f6d2b5d7f1185fde25b1e54eb66e647a1dfef0e2c2bfaf9b4c88c8"}, + {file = "coverage-7.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:018a12985185038a5b2bcafab04ab833a9a0f2c59995b3cec07e10074c78635f"}, + {file = "coverage-7.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db14f552ac38f10758ad14dd7b983dbab424e731588d300c7db25b6f89e335b5"}, + {file = "coverage-7.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3257fdd8e574805f27bb5342b77bc65578e98cbc004a92232106344053f319ba"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a6612c99081d8d6134005b1354191e103ec9705d7ba2754e848211ac8cacc6b"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d45d3cbd94159c468b9b8c5a556e3f6b81a8d1af2a92b77320e887c3e7a5d080"}, + {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a892be37ca35eb5019ec85402c3371b0f7cda5ab5056023a7f13da0961e60da"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8192794d120167e2a64721d88dbd688584675e86e15d0569599257566dec9bf0"}, + {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:820bc841faa502e727a48311948e0461132a9c8baa42f6b2b84a29ced24cc078"}, + {file = "coverage-7.5.4-cp311-cp311-win32.whl", hash = "sha256:6aae5cce399a0f065da65c7bb1e8abd5c7a3043da9dceb429ebe1b289bc07806"}, + {file = "coverage-7.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2e344d6adc8ef81c5a233d3a57b3c7d5181f40e79e05e1c143da143ccb6377d"}, + {file = "coverage-7.5.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:54317c2b806354cbb2dc7ac27e2b93f97096912cc16b18289c5d4e44fc663233"}, + {file = "coverage-7.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:042183de01f8b6d531e10c197f7f0315a61e8d805ab29c5f7b51a01d62782747"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bb74ed465d5fb204b2ec41d79bcd28afccf817de721e8a807d5141c3426638"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3d45ff86efb129c599a3b287ae2e44c1e281ae0f9a9bad0edc202179bcc3a2e"}, + {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5013ed890dc917cef2c9f765c4c6a8ae9df983cd60dbb635df8ed9f4ebc9f555"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1014fbf665fef86cdfd6cb5b7371496ce35e4d2a00cda501cf9f5b9e6fced69f"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3684bc2ff328f935981847082ba4fdc950d58906a40eafa93510d1b54c08a66c"}, + {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:581ea96f92bf71a5ec0974001f900db495488434a6928a2ca7f01eee20c23805"}, + {file = "coverage-7.5.4-cp312-cp312-win32.whl", hash = "sha256:73ca8fbc5bc622e54627314c1a6f1dfdd8db69788f3443e752c215f29fa87a0b"}, + {file = "coverage-7.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:cef4649ec906ea7ea5e9e796e68b987f83fa9a718514fe147f538cfeda76d7a7"}, + {file = "coverage-7.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdd31315fc20868c194130de9ee6bfd99755cc9565edff98ecc12585b90be882"}, + {file = "coverage-7.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:02ff6e898197cc1e9fa375581382b72498eb2e6d5fc0b53f03e496cfee3fac6d"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05c16cf4b4c2fc880cb12ba4c9b526e9e5d5bb1d81313d4d732a5b9fe2b9d53"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5986ee7ea0795a4095ac4d113cbb3448601efca7f158ec7f7087a6c705304e4"}, + {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df54843b88901fdc2f598ac06737f03d71168fd1175728054c8f5a2739ac3e4"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ab73b35e8d109bffbda9a3e91c64e29fe26e03e49addf5b43d85fc426dde11f9"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:aea072a941b033813f5e4814541fc265a5c12ed9720daef11ca516aeacd3bd7f"}, + {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:16852febd96acd953b0d55fc842ce2dac1710f26729b31c80b940b9afcd9896f"}, + {file = "coverage-7.5.4-cp38-cp38-win32.whl", hash = "sha256:8f894208794b164e6bd4bba61fc98bf6b06be4d390cf2daacfa6eca0a6d2bb4f"}, + {file = "coverage-7.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:e2afe743289273209c992075a5a4913e8d007d569a406ffed0bd080ea02b0633"}, + {file = "coverage-7.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088"}, + {file = "coverage-7.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8"}, + {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c"}, + {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7"}, + {file = "coverage-7.5.4-cp39-cp39-win32.whl", hash = "sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace"}, + {file = "coverage-7.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d"}, + {file = "coverage-7.5.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5"}, + {file = "coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353"}, ] [package.dependencies] @@ -164,13 +269,13 @@ toml = ["tomli"] [[package]] name = "exceptiongroup" -version = "1.2.0" +version = "1.2.1" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, - {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, ] [package.extras] @@ -178,18 +283,18 @@ test = ["pytest (>=6)"] [[package]] name = "flake8" -version = "7.0.0" +version = "7.1.0" description = "the modular source code checker: pep8 pyflakes and co" optional = false python-versions = ">=3.8.1" files = [ - {file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"}, - {file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"}, + {file = "flake8-7.1.0-py2.py3-none-any.whl", hash = "sha256:2e416edcc62471a64cea09353f4e7bdba32aeb079b6e360554c659a122b1bc6a"}, + {file = "flake8-7.1.0.tar.gz", hash = "sha256:48a07b626b55236e0fb4784ee69a465fbf59d79eec1f5b4785c3d3bc57d17aa5"}, ] [package.dependencies] mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.11.0,<2.12.0" +pycodestyle = ">=2.12.0,<2.13.0" pyflakes = ">=3.2.0,<3.3.0" [[package]] @@ -217,6 +322,23 @@ files = [ [package.extras] colors = ["colorama (>=0.4.6)"] +[[package]] +name = "jinja2" +version = "3.1.4" +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"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -241,6 +363,75 @@ profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + [[package]] name = "mccabe" version = "0.7.0" @@ -263,40 +454,51 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "multimethod" +version = "1.12" +description = "Multiple argument dispatching." +optional = false +python-versions = ">=3.9" +files = [ + {file = "multimethod-1.12-py3-none-any.whl", hash = "sha256:fd0c473c43558908d97cc06e4d68e8f69202f167db46f7b4e4058893e7dbdf60"}, + {file = "multimethod-1.12.tar.gz", hash = "sha256:8db8ef2a8d2a247e3570cc23317680892fdf903d84c8c1053667c8e8f7671a67"}, +] + [[package]] name = "mypy" -version = "1.9.0" +version = "1.10.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, - {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, - {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, - {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, - {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, - {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, - {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, - {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, - {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, - {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, - {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, - {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, - {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, - {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, - {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, - {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, - {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, - {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, - {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, - {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, - {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, - {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, - {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, + {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"}, + {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"}, + {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"}, + {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"}, + {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"}, + {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"}, + {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"}, + {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"}, + {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"}, + {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"}, + {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"}, + {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"}, + {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"}, + {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"}, + {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"}, + {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"}, + {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"}, + {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"}, + {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"}, + {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"}, + {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"}, + {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"}, + {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"}, ] [package.dependencies] @@ -368,13 +570,13 @@ files = [ [[package]] name = "packaging" -version = "24.0" +version = "24.1" description = "Core utilities for Python packages" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, - {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, ] [[package]] @@ -490,45 +692,154 @@ files = [ {file = "pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9"}, ] +[[package]] +name = "pillow" +version = "10.4.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e"}, + {file = "pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b"}, + {file = "pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e"}, + {file = "pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46"}, + {file = "pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984"}, + {file = "pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141"}, + {file = "pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c"}, + {file = "pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe"}, + {file = "pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d"}, + {file = "pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696"}, + {file = "pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496"}, + {file = "pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91"}, + {file = "pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94"}, + {file = "pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef"}, + {file = "pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b"}, + {file = "pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9"}, + {file = "pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42"}, + {file = "pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a"}, + {file = "pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3"}, + {file = "pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0"}, + {file = "pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a"}, + {file = "pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309"}, + {file = "pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060"}, + {file = "pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea"}, + {file = "pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736"}, + {file = "pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b"}, + {file = "pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84"}, + {file = "pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0"}, + {file = "pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e"}, + {file = "pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d"}, + {file = "pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b"}, + {file = "pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1"}, + {file = "pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df"}, + {file = "pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef"}, + {file = "pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5"}, + {file = "pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885"}, + {file = "pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27"}, + {file = "pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3"}, + {file = "pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=7.3)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + [[package]] name = "platformdirs" -version = "4.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +version = "4.2.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, ] [package.extras] docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] [[package]] name = "pluggy" -version = "1.4.0" +version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ - {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, - {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "py-cpuinfo" +version = "9.0.0" +description = "Get CPU info with pure Python" +optional = false +python-versions = "*" +files = [ + {file = "py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690"}, + {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, +] + [[package]] name = "pycodestyle" -version = "2.11.1" +version = "2.12.0" description = "Python style guide checker" optional = false python-versions = ">=3.8" files = [ - {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, - {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, + {file = "pycodestyle-2.12.0-py2.py3-none-any.whl", hash = "sha256:949a39f6b86c3e1515ba1787c2022131d165a8ad271b11370a8819aa070269e4"}, + {file = "pycodestyle-2.12.0.tar.gz", hash = "sha256:442f950141b4f43df752dd303511ffded3a04c2b6fb7f65980574f0c31e6e79c"}, ] [[package]] @@ -544,28 +855,27 @@ files = [ [[package]] name = "pygments" -version = "2.17.2" +version = "2.18.0" description = "Pygments is a syntax highlighting package written in Python." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, - {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, + {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, + {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, ] [package.extras] -plugins = ["importlib-metadata"] windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" -version = "8.1.1" +version = "8.2.2" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, - {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, + {file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"}, + {file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"}, ] [package.dependencies] @@ -573,11 +883,49 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" -pluggy = ">=1.4,<2.0" +pluggy = ">=1.5,<2.0" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.23.7" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"}, + {file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-benchmark" +version = "4.0.0" +description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-benchmark-4.0.0.tar.gz", hash = "sha256:fb0785b83efe599a6a956361c0691ae1dbb5318018561af10f3e915caa0048d1"}, + {file = "pytest_benchmark-4.0.0-py3-none-any.whl", hash = "sha256:fdb7db64e31c8b277dff9850d2a2556d8b60bcb0ea6524e36e28ffd7c87f71d6"}, +] + +[package.dependencies] +py-cpuinfo = "*" +pytest = ">=3.8" + +[package.extras] +aspect = ["aspectlib"] +elasticsearch = ["elasticsearch"] +histogram = ["pygal", "pygaljs"] [[package]] name = "pytest-cov" @@ -736,6 +1084,26 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "tornado" +version = "6.4.1" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">=3.8" +files = [ + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:163b0aafc8e23d8cdc3c9dfb24c5368af84a81e3364745ccb4427669bf84aec8"}, + {file = "tornado-6.4.1-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:6d5ce3437e18a2b66fbadb183c1d3364fb03f2be71299e7d10dbeeb69f4b2a14"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e20b9113cd7293f164dc46fffb13535266e713cdb87bd2d15ddb336e96cfc4"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ae50a504a740365267b2a8d1a90c9fbc86b780a39170feca9bcc1787ff80842"}, + {file = "tornado-6.4.1-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:613bf4ddf5c7a95509218b149b555621497a6cc0d46ac341b30bd9ec19eac7f3"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25486eb223babe3eed4b8aecbac33b37e3dd6d776bc730ca14e1bf93888b979f"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:454db8a7ecfcf2ff6042dde58404164d969b6f5d58b926da15e6b23817950fc4"}, + {file = "tornado-6.4.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a02a08cc7a9314b006f653ce40483b9b3c12cda222d6a46d4ac63bb6c9057698"}, + {file = "tornado-6.4.1-cp38-abi3-win32.whl", hash = "sha256:d9a566c40b89757c9aa8e6f032bcdb8ca8795d7c1a9762910c722b1635c9de4d"}, + {file = "tornado-6.4.1-cp38-abi3-win_amd64.whl", hash = "sha256:b24b8982ed444378d7f21d563f4180a2de31ced9d8d84443907a0a64da2072e7"}, + {file = "tornado-6.4.1.tar.gz", hash = "sha256:92d3ab53183d8c50f8204a51e6f91d18a15d5ef261e84d452800d4ff6fc504e9"}, +] + [[package]] name = "types-pytz" version = "2024.1.0.20240417" @@ -749,13 +1117,13 @@ files = [ [[package]] name = "typing-extensions" -version = "4.10.0" +version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, - {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] [[package]] @@ -769,7 +1137,18 @@ files = [ {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, ] +[[package]] +name = "xyzservices" +version = "2024.6.0" +description = "Source of XYZ tiles providers" +optional = false +python-versions = ">=3.8" +files = [ + {file = "xyzservices-2024.6.0-py3-none-any.whl", hash = "sha256:fecb2508f0f2b71c819aecf5df2c03cef001c56a4b49302e640f3b34710d25e4"}, + {file = "xyzservices-2024.6.0.tar.gz", hash = "sha256:58c1bdab4257d2551b9ef91cd48571f77b7c4d2bc45bf5e3c05ac97b3a4d7282"}, +] + [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "c59975abad77e3662c6c8337791765e273fc5e5ecf16e8d2ecbc0ba2057ac12a" +content-hash = "78f86e6e0a5d5c90ea2c1a0d288a723d8c8a99b665798a13e377a8eed9d70227" diff --git a/pyproject.toml b/pyproject.toml index 15b4344..08e1356 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "fx-lib" +name = "pytrade" version = "0.1.0" description = "" authors = ["Kyle Widmann "] @@ -11,6 +11,8 @@ pytest = "^8.1.1" numpy = "^1.26.4" pandas = "^2.2.2" pandas-stubs = "^2.2.2.240603" +backtesting = "^0.3.3" +multimethod = "^1.12" [tool.poetry.group.dev.dependencies] @@ -21,6 +23,8 @@ flake8 = "^7.0.0" black = "^24.3.0" isort = "^5.13.2" mypy = "^1.9.0" +pytest-benchmark = "^4.0.0" +pytest-asyncio = "^0.23.7" [build-system] requires = ["poetry-core"] @@ -48,7 +52,7 @@ exclude_also = [ "@(abc\\.)?abstractmethod", ] omit = [ - "fx_lib/interfaces/*", - "fx_lib/models/*" + "pytrade/interfaces/*", + "pytrade/models/*" ] diff --git a/fx_lib/__init__.py b/pytrade/__init__.py similarity index 100% rename from fx_lib/__init__.py rename to pytrade/__init__.py diff --git a/pytrade/broker.py b/pytrade/broker.py new file mode 100644 index 0000000..48dcda3 --- /dev/null +++ b/pytrade/broker.py @@ -0,0 +1,42 @@ +from typing import Callable, List + +from pytrade.interfaces.broker import IBroker +from pytrade.interfaces.client import IClient +from pytrade.models.instruments import Candlestick, Granularity, Instrument +from pytrade.models.order import OrderRequest + + +class FxBroker(IBroker): + + def __init__(self, client: IClient): + self.client = client + self._pending_orders: List[OrderRequest] = [] + + @property + def equity(self) -> float: + raise NotImplementedError + # return self._cash + sum(trade.pl for trade in self.trades) + + @property + def margin_available(self) -> float: + raise NotImplementedError + # From https://github.com/QuantConnect/Lean/pull/3768 + # margin_used = sum(trade.value / self._leverage for trade in self.trades) + # return max(0, self.equity - margin_used) + + def order(self, order: OrderRequest): + self._pending_orders.append(order) + + def process_orders(self): + for order in self._pending_orders: + self.client.order(order) + + self._pending_orders.clear() + + def subscribe( + self, + instrument: Instrument, + granularity: Granularity, + callback: Callable[[Candlestick], None], + ): + self.client.subscribe(instrument, granularity, callback) diff --git a/pytrade/events/candlestick_event.py b/pytrade/events/candlestick_event.py new file mode 100644 index 0000000..a060a65 --- /dev/null +++ b/pytrade/events/candlestick_event.py @@ -0,0 +1,6 @@ +from pytrade.events.typed_event import TypedEvent +from pytrade.models.instruments import Candlestick + + +class CandlestickEvent(TypedEvent[Candlestick]): + pass diff --git a/pytrade/events/event.py b/pytrade/events/event.py new file mode 100644 index 0000000..d4d2501 --- /dev/null +++ b/pytrade/events/event.py @@ -0,0 +1,23 @@ +from typing import Callable + + +class Event: + + def __init__(self): + self.__callbacks: list[Callable[[], None]] = [] + + @property + def _callbacks(self): + return self.__callbacks + + def __iadd__(self, callback: Callable[[], None]): + self.__callbacks.append(callback) + return self + + def __isub__(self, callback: Callable[[], None]): + self.__callbacks.remove(callback) + return self + + def __call__(self, *args, **kwargs): + for callback in self._callbacks: + callback(*args, **kwargs) diff --git a/pytrade/events/typed_event.py b/pytrade/events/typed_event.py new file mode 100644 index 0000000..2d77038 --- /dev/null +++ b/pytrade/events/typed_event.py @@ -0,0 +1,25 @@ +from typing import Callable, Generic, TypeVar + +T = TypeVar("T") + + +class TypedEvent(Generic[T]): + + def __init__(self): + self.__callbacks: list[Callable[[T], None]] = [] + + @property + def _callbacks(self): + return self.__callbacks + + def __iadd__(self, callback: Callable[[T], None]): + self.__callbacks.append(callback) + return self + + def __isub__(self, callback: Callable[[T], None]): + self.__callbacks.remove(callback) + return self + + def __call__(self, *args, **kwargs): + for callback in self._callbacks: + callback(*args, **kwargs) diff --git a/fx_lib/interfaces/__init__.py b/pytrade/interfaces/__init__.py similarity index 100% rename from fx_lib/interfaces/__init__.py rename to pytrade/interfaces/__init__.py diff --git a/fx_lib/interfaces/broker.py b/pytrade/interfaces/broker.py similarity index 53% rename from fx_lib/interfaces/broker.py rename to pytrade/interfaces/broker.py index e840f91..c022716 100644 --- a/fx_lib/interfaces/broker.py +++ b/pytrade/interfaces/broker.py @@ -1,4 +1,8 @@ import abc +from typing import Callable + +from pytrade.models.instruments import Candlestick, Granularity, Instrument +from pytrade.models.order import OrderRequest class IBroker(metaclass=abc.ABCMeta): @@ -6,27 +10,36 @@ class IBroker(metaclass=abc.ABCMeta): @classmethod def __subclasshook__(cls, subclass): return ( - hasattr(subclass, "new_order") - and callable(subclass.new_order) + hasattr(subclass, "order") + and callable(subclass.order) and hasattr(subclass, "equity") and hasattr(subclass, "margin_available") or NotImplemented ) - @abc.abstractmethod - def new_order(self): - raise NotImplementedError - @property @abc.abstractmethod def equity(self) -> float: - raise NotImplementedError + raise NotImplementedError() return self._cash + sum(trade.pl for trade in self.trades) @property @abc.abstractmethod def margin_available(self) -> float: - raise NotImplementedError + raise NotImplementedError() # From https://github.com/QuantConnect/Lean/pull/3768 margin_used = sum(trade.value / self._leverage for trade in self.trades) return max(0, self.equity - margin_used) + + @abc.abstractmethod + def order(self, order: OrderRequest): + raise NotImplementedError() + + @abc.abstractmethod + def subscribe( + self, + instrument: Instrument, + granularity: Granularity, + callback: Callable[[Candlestick], None], + ): + raise NotImplementedError() diff --git a/pytrade/interfaces/client.py b/pytrade/interfaces/client.py new file mode 100644 index 0000000..0674e24 --- /dev/null +++ b/pytrade/interfaces/client.py @@ -0,0 +1,49 @@ +import abc +from typing import Callable + +from multimethod import multimethod + +from pytrade.models.instruments import Candlestick, Granularity, Instrument +from pytrade.models.order import MarketOrderRequest + + +class IClient: + + @classmethod + def __subclasshook__(cls, subclass): + return ( + hasattr(subclass, "order") + and callable(subclass.order) + and hasattr(subclass, "get_candles") + and callable(subclass.get_candles) + and hasattr(subclass, "get_candle") + and callable(subclass.get_candle) + and hasattr(subclass, "subscribe") + and callable(subclass.subscribe) + ) or NotImplemented + + @abc.abstractmethod + @multimethod + def order(self, order: MarketOrderRequest): + raise NotImplementedError() + + @abc.abstractmethod + def get_candles( + self, instrument: Instrument, granularity: Granularity, count: int + ) -> list[Candlestick]: + raise NotImplementedError() + + @abc.abstractmethod + def get_candle( + self, instrument: Instrument, granularity: Granularity + ) -> Candlestick: + raise NotImplementedError() + + @abc.abstractmethod + def subscribe( + self, + instrument: Instrument, + granularity: Granularity, + callback: Callable[[Candlestick], None], + ): + raise NotImplementedError() diff --git a/pytrade/interfaces/data.py b/pytrade/interfaces/data.py new file mode 100644 index 0000000..21cba8a --- /dev/null +++ b/pytrade/interfaces/data.py @@ -0,0 +1,39 @@ +from abc import abstractmethod + +import pandas as pd + +from pytrade.events.event import Event + + +class IInstrumentData: + + @property + @abstractmethod + def df(self) -> pd.DataFrame: + raise NotImplementedError() + + @property + @abstractmethod + def on_update(self) -> Event: + raise NotImplementedError() + + @on_update.setter + @abstractmethod + def on_update(self, value: Event): + raise NotImplementedError() + + @property + def Open(self): + return self.df.Open + + @property + def High(self): + return self.df.High + + @property + def Low(self): + return self.df.Low + + @property + def Close(self): + return self.df.Close diff --git a/fx_lib/models/__init__.py b/pytrade/logging.py similarity index 100% rename from fx_lib/models/__init__.py rename to pytrade/logging.py diff --git a/fx_lib/models/__init__.pyi b/pytrade/models/__init__.py similarity index 100% rename from fx_lib/models/__init__.pyi rename to pytrade/models/__init__.py diff --git a/pytrade/models/broker.py b/pytrade/models/broker.py new file mode 100644 index 0000000..de962f2 --- /dev/null +++ b/pytrade/models/broker.py @@ -0,0 +1,6 @@ +class Trade: + pass + + +class Position: + pass diff --git a/pytrade/models/indicator.py b/pytrade/models/indicator.py new file mode 100644 index 0000000..0bcd170 --- /dev/null +++ b/pytrade/models/indicator.py @@ -0,0 +1,123 @@ +from abc import abstractmethod + +import numpy as np + +from pytrade.interfaces.data import IInstrumentData + + +class _Array(np.ndarray): + """ + ndarray extended to supply .name and other arbitrary properties + in ._opts dict. + """ + + def __new__(cls, array=[], *, name=None, **kwargs): + obj = np.asarray(array).view(cls) + # obj.name = name or array.name + # obj._opts = kwargs + return obj + + def __array_finalize__(self, obj): + pass + # if obj is not None: + # self.name = getattr(obj, "name", "") + # self._opts = getattr(obj, "_opts", {}) + + # Make sure properties name and _opts are carried over + # when (un-)pickling. + def __reduce__(self): + value = super().__reduce__() + return value[:2] + (value[2] + (self.__dict__,),) + + def __setstate__(self, state): + self.__dict__.update(state[-1]) + super().__setstate__(state[:-1]) + + def __bool__(self): + try: + return bool(self[-1]) + except IndexError: + return super().__bool__() + + def __float__(self): + try: + return float(self[-1]) + except IndexError: + return super().__float__() + + # def to_series(self): + # warnings.warn( + # "`.to_series()` is deprecated. For pd.Series conversion, use accessor `.s`" + # ) + # return self.s + + # @property + # def s(self) -> pd.Series: + # values = np.atleast_2d(self) + # index = self._opts["index"][: values.shape[1]] + # return pd.Series(values[0], index=index, name=self.name) + + # @property + # def df(self) -> pd.DataFrame: + # values = np.atleast_2d(np.asarray(self)) + # index = self._opts["index"][: values.shape[1]] + # df = pd.DataFrame(values.T, index=index, columns=[self.name] * len(values)) + # return df + + +class Indicator: + + def __init__(self, data: IInstrumentData, *args, **kwargs): + self._data = data + data.on_update += self._update + self._args = args + self._kwargs = kwargs + self._values = self._run(self._args, self._kwargs) + + def _update(self): + self._values = self._run(self._args, self._kwargs) + + @abstractmethod + def _run(self, *args, **kwargs) -> np.ndarray: + raise NotImplementedError() + + @property + def value(self): + return self._values[-1] if len(self._values) > 0 else None + + @property + def to_array(self): + return self._values + + def __eq__(self, other): + result = False + if isinstance(other, Indicator): + result = self.value == other.value + else: + result = self.value == other + + return result + + def __gt__(self, other): + result = False + if isinstance(other, Indicator): + result = self.value > other.value + else: + result = self.value > other + + return result + + def __lt__(self, other): + result = False + if isinstance(other, Indicator): + result = self.value < other.value + else: + result = self.value < other + + return result + + def __bool__(self): + return bool(self.value) + + def __float__(self): + return float(self.value) diff --git a/pytrade/models/instruments.py b/pytrade/models/instruments.py new file mode 100644 index 0000000..fdf06ab --- /dev/null +++ b/pytrade/models/instruments.py @@ -0,0 +1,243 @@ +from datetime import datetime, timedelta +from enum import Enum +from typing import Optional + +import pandas as pd +import pytz +from pandas import Timestamp + +from pytrade.events.event import Event +from pytrade.interfaces.data import IInstrumentData + + +class Granularity(Enum): + M1 = "M1" + M5 = "M5" + M15 = "M15" + H1 = "H1" + H4 = "H4" + + +MINUTES_MAP = { + Granularity.M1: 1, + Granularity.M5: 5, + Granularity.M15: 15, + Granularity.H1: 60, + Granularity.H4: 240, +} + + +class Instrument(Enum): + AUDJPY = "AUD/JPY" + AUDNZD = "AUD/NZD" + AUDUSD = "AUD/USD" + CADJPY = "CAD/JPY" + CHFJPY = "CHF/JPY" + EURCHF = "EUR/CHF" + EURGBP = "EUR/GBP" + EURJPY = "EUR/JPY" + EURPLN = "EUR/PLN" + EURUSD = "EUR/USD" + GBPJPY = "GBP/JPY" + GBPUSD = "GBP/USD" + NZDUSD = "NZD/USD" + USDCAD = "USD/CAD" + USDCHF = "USD/CHF" + USDJPY = "USD/JPY" + USDMXN = "USD/MXN" + USDRUB = "USD/RUB" + USDTRY = "USD/TRY" + USDZAR = "USD/ZAR" + + +instrument_lookup = {m.value: m for m in Instrument} + + +class CandleSubscription: + + def __init__(self, instrument: Instrument, granularity: Granularity): + self._instrument = instrument + self._granularity = granularity + + @property + def granularity(self) -> Granularity: + return self._granularity + + @property + def instrument(self) -> Instrument: + return self._instrument + + def __hash__(self): + return hash((self.instrument, self.granularity)) + + def __gt__(self, other): + return ( + isinstance(other, CandleSubscription) + and self.instrument.value < other.instrument.value + and self.granularity.value > other.granularity.value + ) + + def __lt__(self, other): + return ( + isinstance(other, CandleSubscription) + and self.instrument.value > other.instrument.value + and self.granularity.value < other.granularity.value + ) + + def __eq__(self, other): + return ( + isinstance(other, CandleSubscription) + and self.instrument.value == other.instrument.value + and self.granularity.value == other.granularity.value + ) + + +class Candlestick: + + def __init__( + self, + instrument: Instrument, + granularity: Granularity, + open: float, + high: float, + low: float, + close: float, + timestamp: Timestamp, + ): + self.instrument = instrument + self.granularity = granularity + self.open = open + self.high = high + self.low = low + self.close = close + self.timestamp = timestamp + + def to_dict(self): + return { + "Timestamp": self.timestamp, + "Instrument": self.instrument, + "Open": self.open, + "High": self.open, + "Low": self.open, + "Close": self.open, + } + + +class TickData: + + instrument: Instrument + timestamp: datetime + bid: float + ask: float + + def __init__(self, instrument: str, timestamp: str, bid: str, ask: str): + self.instrument = instrument_lookup[instrument] + tz = pytz.timezone("UTC") + self.timestamp = datetime.strptime(timestamp, "%Y%m%d %H:%M:%S.%f").astimezone( + tz + ) + self.bid = float(bid) + self.ask = float(ask) + + +COLUMNS = ["Timestamp", "Instrument", "Open", "High", "Low", "Close"] + +INDEX = COLUMNS[0] + + +class InstrumentCandles(IInstrumentData): + + def __init__( + self, data: Optional[pd.DataFrame] = None, max_size: Optional[int] = None + ): + self._data: pd.DataFrame = ( + data if data is not None else pd.DataFrame(columns=COLUMNS) + ) + if not isinstance(self._data.index, pd.DatetimeIndex): + if INDEX in self._data.columns: + self._data.set_index([INDEX], inplace=True) + else: + raise RuntimeError( + "Dataframe does not have a datetime index and does not have a 'Timestamp' column" + ) + self._max_size: Optional[int] = max_size + self.__instrument: Optional[Instrument] = None + self.__granularity: Optional[Granularity] = None + self.__update_event = Event() + + @property + def df(self): + return self._data + + @property + def on_update(self): + return self.__update_event + + @on_update.setter + def on_update(self, value: Event): + self.__update_event = value + + def update(self, candlestick: Candlestick): + if not self.__instrument: + self.__instrument = candlestick.instrument + self.__granularity = candlestick.granularity + + if candlestick.instrument != self.__instrument: + raise RuntimeError( + f"Received {candlestick.instrument} for history[{self.__instrument}]" + ) + + if candlestick.granularity != self.__granularity: + raise RuntimeError( + f"Received {candlestick.granularity} for history[{self.__granularity}]" + ) + + if self._max_size and len(self._data) >= self._max_size: + _delta = self._data.index[-1] - self._data.index[-2] + if not isinstance(_delta, timedelta): + raise RuntimeError( + "Expected dataframe to have DatetimeInex. Unable to caluclate timedelta from index." + ) + self._data.index = self._data.index.shift( # type: ignore + int(_delta.seconds / 60), freq="min" + ) + self._data = self._data.shift(-1) + + self._data.loc[candlestick.timestamp] = [ # type: ignore + candlestick.instrument, + candlestick.open, + candlestick.high, + candlestick.low, + candlestick.close, + ] + self.__update_event() + + +class CandleData: + + def __init__(self, max_size=1000): + self._data: dict[tuple[Instrument, Granularity], InstrumentCandles] = {} + self._max_size = max_size + + def __new__(cls, *args, **kwargs): + if not hasattr(cls, "instance"): + cls.instance = super().__new__(cls) + # Need to handle case where instantiatied and different max size is provided + return cls.instance + + def get(self, instrument: Instrument, granularity: Granularity): + key = (instrument, granularity) + instrument_candles: InstrumentCandles = self._data.get( + (instrument, granularity), InstrumentCandles(max_size=self._max_size) + ) + self._data[key] = instrument_candles + return instrument_candles + + def update(self, candle: Candlestick): + key = (candle.instrument, candle.granularity) + instrument_candles: InstrumentCandles = self._data.get( + key, + InstrumentCandles(max_size=self._max_size), + ) + self._data[key] = instrument_candles + instrument_candles.update(candle) diff --git a/pytrade/models/order.py b/pytrade/models/order.py new file mode 100644 index 0000000..5106f1c --- /dev/null +++ b/pytrade/models/order.py @@ -0,0 +1,321 @@ +from enum import Enum +from typing import Optional + +from pytrade.models.instruments import Instrument +from pytrade.models.trade import Trade + + +class TimeInForce(Enum): + + GOOD_TILL_CANCELLED = "GTC" + GOOD_TILL_DATE = "GTD" + GOOD_FOR_DAY = "GFD" + FILL_OR_KILL = "FOK" + PARTIAL_OR_KILL = "IOC" + + +class OrderType(Enum): + + MARKET = "MARKET" + LIMIT = "LIMIT" + STOP = "STOP" + + +class Order: + """ + Place new orders through `Strategy.buy()` and `Strategy.sell()`. + Query existing orders through `Strategy.orders`. + + When an order is executed or [filled], it results in a `Trade`. + + If you wish to modify aspects of a placed but not yet filled order, + cancel it and place a new one instead. + + All placed orders are [Good 'Til Canceled]. + + [filled]: https://www.investopedia.com/terms/f/fill.asp + [Good 'Til Canceled]: https://www.investopedia.com/terms/g/gtc.asp + """ + + def __init__( + self, + size: float, + limit_price: Optional[float] = None, + stop_price: Optional[float] = None, + sl_price: Optional[float] = None, + tp_price: Optional[float] = None, + parent_trade: Optional["Trade"] = None, + tag: object = None, + ): + if size == 0: + raise RuntimeError("Invalid size provided for order.") + + self.__size = size + self.__limit_price = limit_price + self.__stop_price = stop_price + self.__sl_price = sl_price + self.__tp_price = tp_price + self.__parent_trade = parent_trade + self.__tag = tag + + def _replace(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, f"_{self.__class__.__qualname__}__{k}", v) + return self + + def __repr__(self): + return "".format( + ", ".join( + f"{param}={round(value, 5)}" + for param, value in ( + ("size", self.__size), + ("limit", self.__limit_price), + ("stop", self.__stop_price), + ("sl", self.__sl_price), + ("tp", self.__tp_price), + ("contingent", self.is_contingent), + ("tag", self.__tag), + ) + if value is not None + ) + ) + + # Fields getters + + @property + def size(self) -> float: + """ + Order size (negative for short orders). + + If size is a value between 0 and 1, it is interpreted as a fraction of current + available liquidity (cash plus `Position.pl` minus used margin). + A value greater than or equal to 1 indicates an absolute number of units. + """ + return self.__size + + @property + def limit(self) -> Optional[float]: + """ + Order limit price for [limit orders], or None for [market orders], + which are filled at next available price. + + [limit orders]: https://www.investopedia.com/terms/l/limitorder.asp + [market orders]: https://www.investopedia.com/terms/m/marketorder.asp + """ + return self.__limit_price + + @property + def stop(self) -> Optional[float]: + """ + Order stop price for [stop-limit/stop-market][_] order, + otherwise None if no stop was set, or the stop price has already been hit. + + [_]: https://www.investopedia.com/terms/s/stoporder.asp + """ + return self.__stop_price + + @property + def sl(self) -> Optional[float]: + """ + A stop-loss price at which, if set, a new contingent stop-market order + will be placed upon the `Trade` following this order's execution. + See also `Trade.sl`. + """ + return self.__sl_price + + @property + def tp(self) -> Optional[float]: + """ + A take-profit price at which, if set, a new contingent limit order + will be placed upon the `Trade` following this order's execution. + See also `Trade.tp`. + """ + return self.__tp_price + + @property + def parent_trade(self): + return self.__parent_trade + + @property + def tag(self): + """ + Arbitrary value (such as a string) which, if set, enables tracking + of this order and the associated `Trade` (see `Trade.tag`). + """ + return self.__tag + + # Extra properties + + @property + def is_long(self): + """True if the order is long (order size is positive).""" + return self.__size > 0 + + @property + def is_short(self): + """True if the order is short (order size is negative).""" + return self.__size < 0 + + @property + def is_contingent(self): + """ + True for [contingent] orders, i.e. [OCO] stop-loss and take-profit bracket orders + placed upon an active trade. Remaining contingent orders are canceled when + their parent `Trade` is closed. + + You can modify contingent orders through `Trade.sl` and `Trade.tp`. + + [contingent]: https://www.investopedia.com/terms/c/contingentorder.asp + [OCO]: https://www.investopedia.com/terms/o/oco.asp + """ + return bool(self.__parent_trade) + + def cancel(self): + trade = self.__parent_trade + if trade: + if self is trade._sl_order: + trade._replace(sl_order=None) + elif self is trade._tp_order: + trade._replace(tp_order=None) + else: + raise RuntimeError() + + +class OrderRequest(dict): + + def __init__( + self, + instrument: Instrument, + units: int, + time_in_force: TimeInForce, + take_profit_on_fill: Optional[float] = None, + stop_loss_on_fill: Optional[float] = None, + trailing_stop_loss_on_fill: Optional[float] = None, + ): + # super().__init__( + # instrument=instrument, + # units=units, + # price=price, + # time_in_force=time_in_force, + # take_profit_on_fill=take_profit_on_fill, + # stop_loss_on_fill=stop_loss_on_fill, + # trailing_stop_loss_on_fill=trailing_stop_loss_on_fill + # ) + self._instrument: Instrument = instrument + self._units: int = units + self._time_in_force: Optional[TimeInForce] = time_in_force + self._take_profit_on_fill: Optional[float] = take_profit_on_fill + self._stop_loss_on_fill: Optional[float] = stop_loss_on_fill + self._trailing_stop_loss_on_fill: Optional[float] = trailing_stop_loss_on_fill + + @property + def instrument(self) -> Instrument: + return self._instrument + + @property + def units(self) -> int: + return self._units + + @property + def time_in_force(self) -> TimeInForce: + return ( + self._time_in_force + if self._time_in_force + else TimeInForce.GOOD_TILL_CANCELLED + ) + + @property + def take_profit_on_fill(self) -> Optional[float]: + return self._take_profit_on_fill + + @property + def stop_loss_on_fill(self) -> Optional[float]: + return self._stop_loss_on_fill + + @property + def trailing_stop_loss_on_fill(self) -> Optional[float]: + return self._trailing_stop_loss_on_fill + + +class MarketOrderRequest(OrderRequest): + + def __init__( + self, + instrument: Instrument, + units: int, + time_in_force: TimeInForce = TimeInForce.FILL_OR_KILL, + take_profit_on_fill: Optional[float] = None, + stop_loss_on_fill: Optional[float] = None, + trailing_stop_loss_on_fill: Optional[float] = None, + price_bound: Optional[float] = None, + ): + super().__init__( + instrument, + units, + time_in_force, + take_profit_on_fill, + stop_loss_on_fill, + trailing_stop_loss_on_fill, + ) + self._price_bound = price_bound + + @property + def type(self): + return OrderType.MARKET + + @property + def pricebound(self) -> Optional[float]: + return self._price_bound + + +class LimitOrderRequest(OrderRequest): + + def __init__( + self, + instrument: Instrument, + units: int, + price: float, + time_in_force: TimeInForce, + take_profit_on_fill: Optional[float], + stop_loss_on_fill: Optional[float], + trailing_stop_loss_on_fill: Optional[float], + ): + self._price = price + + @property + def type(self): + return OrderType.LIMIT + + @property + def price(self) -> float: + return self._price + + +class StopOrderRequest(OrderRequest): + + def __init__( + self, + instrument: Instrument, + units: int, + price: float, + time_in_force: TimeInForce, + position_fill: float, + take_profit_on_fill: float, + stop_loss_on_fill: float, + trailing_stop_loss_on_fill: float, + price_bound: Optional[float], + ): + self._price = price + self._price_bound = price_bound + + @property + def type(self): + return OrderType.STOP + + @property + def price(self) -> float: + return self._price + + @property + def pricebound(self) -> Optional[float]: + return self._price_bound diff --git a/pytrade/models/position.py b/pytrade/models/position.py new file mode 100644 index 0000000..0828c58 --- /dev/null +++ b/pytrade/models/position.py @@ -0,0 +1,59 @@ +import numpy as np + +from pytrade.models.trade import Trade + + +class Position: + """ + Currently held asset position, available as + `backtesting.backtesting.Strategy.position` within + `backtesting.backtesting.Strategy.next`. + Can be used in boolean contexts, e.g. + + if self.position: + ... # we have a position, either long or short + """ + + def __init__(self, trades: list[Trade]): + self._trades = trades + + def __bool__(self): + return self.size != 0 + + @property + def size(self) -> float: + """Position size in units of asset. Negative if position is short.""" + return sum(trade.size for trade in self._trades) + + @property + def pl(self) -> float: + """Profit (positive) or loss (negative) of the current position in cash units.""" + return sum(trade.pl for trade in self._trades) + + @property + def pl_pct(self) -> float: + """Profit (positive) or loss (negative) of the current position in percent.""" + weights = np.abs([trade.size for trade in self._trades]) + weights = weights / weights.sum() + pl_pcts = np.array([trade.pl_pct for trade in self._trades]) + return (pl_pcts * weights).sum() + + @property + def is_long(self) -> bool: + """True if the position is long (position size is positive).""" + return self.size > 0 + + @property + def is_short(self) -> bool: + """True if the position is short (position size is negative).""" + return self.size < 0 + + def close(self, portion: float = 1.0): + """ + Close portion of position by closing `portion` of each active trade. See `Trade.close`. + """ + for trade in self._trades: + trade.close(portion) + + def __repr__(self): + return f"" diff --git a/pytrade/models/trade.py b/pytrade/models/trade.py new file mode 100644 index 0000000..9a5fb84 --- /dev/null +++ b/pytrade/models/trade.py @@ -0,0 +1,193 @@ +from copy import copy +from math import copysign +from typing import Optional, Union + +import numpy as np +import pandas as pd + +import pytrade.models.order as order + + +class Trade: + """ + When an `"Order"` is filled, it results in an active `Trade`. + Find active trades in `Strategy.trades` and closed, settled trades in `Strategy.closed_trades`. + """ + + def __init__(self, size: int, entry_price: float, entry_bar: int, tag): + self.__size = size + self.__entry_price = entry_price + self.__exit_price: Optional[float] = None + self.__entry_bar: int = entry_bar + self.__exit_bar: Optional[int] = None + self.__entry_time = None + self.__exit_time = None + self.__sl_order: Optional["order.Order"] = None + self.__tp_order: Optional["order.Order"] = None + self.__tag = tag + + def __repr__(self): + return ( + f'' + ) + + def _replace(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, f"_{self.__class__.__qualname__}__{k}", v) + return self + + def _copy(self, **kwargs): + return copy(self)._replace(**kwargs) + + def close(self, portion: float = 1.0): + """Place new `"Order"` to close `portion` of the trade at next market price.""" + if not 0 < portion <= 1: + raise RuntimeError( + f"Invalid portion for trade ({portion}). Must be a fraction between 0 and 1." + ) + + size = copysign(max(1, round(abs(self.__size) * portion)), -self.__size) + return order.Order(size, parent_trade=self, tag=self.__tag) + + # Fields getters + + @property + def size(self): + """Trade size (volume; negative for short trades).""" + return self.__size + + @property + def entry_price(self) -> float: + """Trade entry price.""" + return self.__entry_price + + @property + def exit_price(self) -> Optional[float]: + """Trade exit price (or None if the trade is still active).""" + return self.__exit_price + + @property + def entry_bar(self) -> int: + """Candlestick bar index of when the trade was entered.""" + return self.__entry_bar + + @property + def exit_bar(self) -> Optional[int]: + """ + Candlestick bar index of when the trade was exited + (or None if the trade is still active). + """ + return self.__exit_bar + + @property + def tag(self): + """ + A tag value inherited from the `"Order"` that opened + this trade. + + This can be used to track trades and apply conditional + logic / subgroup analysis. + + See also `"Order".tag`. + """ + return self.__tag + + @property + def _sl_order(self): + return self.__sl_order + + @property + def _tp_order(self): + return self.__tp_order + + # Extra properties + + @property + def entry_time(self) -> Union[pd.Timestamp, int]: + """Datetime of when the trade was entered.""" + return self.__entry_time # type: ignore + + @property + def exit_time(self) -> Optional[Union[pd.Timestamp, int]]: + """Datetime of when the trade was exited.""" + return self.__exit_time + + @property + def is_long(self): + """True if the trade is long (trade size is positive).""" + return self.__size > 0 + + @property + def is_short(self): + """True if the trade is short (trade size is negative).""" + return not self.is_long + + @property + def pl(self): + """Trade profit (positive) or loss (negative) in cash units.""" + price = self.__exit_price + return self.__size * (price - self.__entry_price) + + @property + def pl_pct(self): + """Trade profit (positive) or loss (negative) in percent.""" + price = self.__exit_price + return copysign(1, self.__size) * (price / self.__entry_price - 1) + + @property + def value(self): + """Trade total value in cash (volume × price).""" + price = self.__exit_price + return abs(self.__size) * price + + # SL/TP management API + + @property + def sl(self): + """ + Stop-loss price at which to close the trade. + + This variable is writable. By assigning it a new price value, + you create or modify the existing SL order. + By assigning it `None`, you cancel it. + """ + return self.__sl_order and self.__sl_order.stop + + @sl.setter + def sl(self, price: float): + self.__set_contingent("sl", price) + + @property + def tp(self): + """ + Take-profit price at which to close the trade. + + This property is writable. By assigning it a new price value, + you create or modify the existing TP order. + By assigning it `None`, you cancel it. + """ + return self.__tp_order and self.__tp_order.limit + + @tp.setter + def tp(self, price: float): + self.__set_contingent("tp", price) + + def __set_contingent(self, type, price): + if type not in ("sl", "tp"): + raise RuntimeError(f"Invalid type supplied for trade {type}") + + if not (price is None or 0 < price < np.inf): + raise RuntimeError(f"Invalid price ({price}) provided for trade.") + + attr = f"_{self.__class__.__qualname__}__{type}_order" + order: "order.Order" = getattr(self, attr) + if order: + order.cancel() + if price: + kwargs = {"stop": price} if type == "sl" else {"limit": price} + order = self.__broker.new_order( + -self.size, trade=self, tag=self.tag, **kwargs + ) + setattr(self, attr, order) diff --git a/fx_lib/py.typed b/pytrade/py.typed similarity index 100% rename from fx_lib/py.typed rename to pytrade/py.typed diff --git a/pytrade/strategy.py b/pytrade/strategy.py new file mode 100644 index 0000000..80787bf --- /dev/null +++ b/pytrade/strategy.py @@ -0,0 +1,99 @@ +import asyncio +from abc import abstractmethod + +from pytrade.interfaces.broker import IBroker +from pytrade.models.instruments import ( + MINUTES_MAP, + CandleData, + Candlestick, + CandleSubscription, + Granularity, + Instrument, + InstrumentCandles, +) + + +class FxStrategy: + + def __init__(self, broker: IBroker, data_context: CandleData): + self.broker = broker + self._updates_complete = asyncio.Event() + self._data_context = data_context + self._pending_updates: list[CandleSubscription] = [] + + def init(self) -> None: + self._caluclate_updates() + self._monitor_instruments() + self._init() + + def _caluclate_updates(self) -> None: + self._required_updates: list[CandleSubscription] = [] + max_interval = 0 + for subscription in self.subscriptions: + max_interval = max(max_interval, MINUTES_MAP[subscription.granularity]) + + for subscription in self.subscriptions: + expected_update_count = int( + max_interval / MINUTES_MAP[subscription.granularity] + ) + self._required_updates += [ + subscription for _ in range(expected_update_count) + ] + + self._pending_updates = self._required_updates.copy() + + @property + @abstractmethod + def subscriptions(self) -> list[CandleSubscription]: + """ + Declare the `InstrumentSubscription`s this strategy should use + for its signals + """ + raise NotImplementedError() + + def _monitor_instruments(self) -> None: + for subscription in self.subscriptions: + self.broker.subscribe( + subscription.instrument, + subscription.granularity, + self._update_instrument, + ) + + def _update_instrument(self, candle: Candlestick) -> None: + self._data_context.update(candle) + self._pending_updates.remove( + CandleSubscription(candle.instrument, candle.granularity) + ) + # Filter out update from pending + if not self._pending_updates: + self._updates_complete.set() + + async def next(self) -> None: + await self._updates_complete.wait() + self._next() + self._pending_updates = self._required_updates.copy() + + def get_data( + self, instrument: Instrument, granularity: Granularity + ) -> InstrumentCandles: + return self._data_context.get(instrument, granularity) + + @abstractmethod + def _init(self) -> None: + """ + Create indicators to be used for signals in the `_next` method. + """ + raise NotImplementedError() + + @abstractmethod + def _next(self) -> None: + """ + Evaluate indicators and submit orders to the broker + """ + raise NotImplementedError() + + def buy(self, size, tp=None, sl=None) -> None: + pass + + def sell(self, size, tp=None, sl=None) -> None: + pass diff --git a/scripts/test.sh b/scripts/test.sh index 9682105..eaedea2 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,3 +1,3 @@ #!/bin/bash -poetry run pytest -s --tb=native --durations=5 --cov=fx_lib --cov-report=html tests -poetry run coverage report --fail-under=0 \ No newline at end of file +poetry run pytest -s --tb=native --durations=5 --cov=pytrade --cov-report=html tests +poetry run coverage report --fail-under=90 \ No newline at end of file diff --git a/tests/unit/test_broker.py b/tests/unit/test_broker.py new file mode 100644 index 0000000..d85c217 --- /dev/null +++ b/tests/unit/test_broker.py @@ -0,0 +1,53 @@ +from unittest.mock import Mock + +from pytrade.broker import FxBroker +from pytrade.models.instruments import Instrument +from pytrade.models.order import OrderRequest, TimeInForce + + +def test_buy_order(): + client = Mock() + broker = FxBroker(client) + + assert len(broker._pending_orders) == 0 + + broker.order(OrderRequest(Instrument.GBPUSD, 10, TimeInForce.GOOD_TILL_CANCELLED)) + + assert len(broker._pending_orders) == 1 + + +def test_sell_order(): + client = Mock() + broker = FxBroker(client) + + assert len(broker._pending_orders) == 0 + + broker.order(OrderRequest(Instrument.GBPUSD, -10, TimeInForce.GOOD_TILL_CANCELLED)) + + assert len(broker._pending_orders) == 1 + + +def test_process_orders(): + client = Mock() + broker = FxBroker(client) + + broker.order(OrderRequest(Instrument.GBPUSD, 10, TimeInForce.GOOD_TILL_CANCELLED)) + + assert len(broker._pending_orders) == 1 + + broker.process_orders() + + assert len(broker._pending_orders) == 0 + assert client.order.call_count == 1 + + +def test_buy(): + pass + + +def test_sell(): + pass + + +def test_get_data(): + pass diff --git a/tests/unit/test_default.py b/tests/unit/test_default.py deleted file mode 100644 index 47d4f19..0000000 --- a/tests/unit/test_default.py +++ /dev/null @@ -1,2 +0,0 @@ -def test_default(): - assert True diff --git a/tests/unit/test_indicator.py b/tests/unit/test_indicator.py new file mode 100644 index 0000000..b26b1b4 --- /dev/null +++ b/tests/unit/test_indicator.py @@ -0,0 +1,189 @@ +import random +from datetime import datetime, timedelta + +import pandas as pd + +from pytrade.models.indicator import Indicator +from pytrade.models.instruments import ( + MINUTES_MAP, + Candlestick, + Granularity, + Instrument, + InstrumentCandles, +) + + +def get_candles( + count: int, instrument: Instrument, granularity: Granularity, end_time: datetime +) -> list[Candlestick]: + _delta = MINUTES_MAP[granularity] + return [ + Candlestick( + instrument, + granularity, + random.uniform(0, 10), + random.uniform(0, 10), + random.uniform(0, 10), + random.uniform(0, 10), + end_time - timedelta(minutes=_delta * i), + ) + for i in range(count) + ] + + +class BoolIndicator(Indicator): + + def _run(self, *args, **kwargs): + return self._data.Open.astype(bool) + + +class SquareIndicator(Indicator): + + def _run(self, *args, **kwargs): + return self._data.Open**2 + + +class StaticIndicator(Indicator): + + def _run(self, *args, **kwargs): + return self._data.Open + + +class AddIndicator(Indicator): + + def _run(self, *args, **kwargs): + return self._data.Open + 1 + + +class SubtractIndicator(Indicator): + + def _run(self, *args, **kwargs): + return self._data.Open - 1 + + +def test_update(): + test_series = pd.Series([0, 1, 0, 0, 1]) + data = InstrumentCandles() + candles = get_candles( + len(test_series), Instrument.EURUSD, Granularity.M1, datetime.now() + ) + indicator = BoolIndicator(data) + for idx, candle in enumerate(candles): + value = test_series[idx] + candle.open = value + data.update(candle) + assert bool(indicator) == bool(value) + + assert len(indicator._values) == len(test_series) + assert indicator.value == test_series.iloc[-1] + + +def test_primitive_equality(): + test_series = pd.Series([0, 1, 2, 3, 4]) + data = InstrumentCandles() + candles = get_candles( + len(test_series), Instrument.EURUSD, Granularity.M1, datetime.now() + ) + indicator = SquareIndicator(data) + for idx, candle in enumerate(candles): + value = test_series[idx] + candle.open = value + data.update(candle) + assert indicator == value**2 + + assert len(indicator._values) == len(test_series) + assert indicator.value == test_series.iloc[-1] ** 2 + + +def test_primitive_greater(): + test_series = pd.Series([0, 1, 2, 3, 4]) + data = InstrumentCandles() + candles = get_candles( + len(test_series), Instrument.EURUSD, Granularity.M1, datetime.now() + ) + indicator = StaticIndicator(data) + for idx, candle in enumerate(candles): + value = test_series[idx] + candle.open = value + data.update(candle) + assert indicator > value - 1 + + assert len(indicator._values) == len(test_series) + assert indicator.value == test_series.iloc[-1] + + +def test_primitive_less(): + test_series = pd.Series([0, 1, 2, 3, 4]) + data = InstrumentCandles() + candles = get_candles( + len(test_series), Instrument.EURUSD, Granularity.M1, datetime.now() + ) + indicator = StaticIndicator(data) + for idx, candle in enumerate(candles): + value = test_series[idx] + candle.open = value + data.update(candle) + assert indicator < value + 1 + + assert len(indicator._values) == len(test_series) + assert indicator.value == test_series.iloc[-1] + + +def test_indicator_equality(): + test_series = pd.Series([0, 1, 2, 3, 4]) + data = InstrumentCandles() + candles = get_candles( + len(test_series), Instrument.EURUSD, Granularity.M1, datetime.now() + ) + indicator = StaticIndicator(data) + indicator2 = StaticIndicator(data) + for idx, candle in enumerate(candles): + value = test_series[idx] + candle.open = value + data.update(candle) + assert indicator == indicator2 + + assert len(indicator._values) == len(test_series) + assert indicator.value == test_series.iloc[-1] + assert len(indicator2._values) == len(test_series) + assert indicator2.value == test_series.iloc[-1] + + +def test_indicator_greater(): + test_series = pd.Series([0, 1, 2, 3, 4]) + data = InstrumentCandles() + candles = get_candles( + len(test_series), Instrument.EURUSD, Granularity.M1, datetime.now() + ) + indicator = StaticIndicator(data) + indicator2 = SubtractIndicator(data) + for idx, candle in enumerate(candles): + value = test_series[idx] + candle.open = value + data.update(candle) + assert indicator > indicator2 + + assert len(indicator._values) == len(test_series) + assert indicator.value == test_series.iloc[-1] + assert len(indicator2._values) == len(test_series) + assert indicator2.value == test_series.iloc[-1] - 1 + + +def test_indicator_less(): + test_series = pd.Series([0, 1, 2, 3, 4]) + data = InstrumentCandles() + candles = get_candles( + len(test_series), Instrument.EURUSD, Granularity.M1, datetime.now() + ) + indicator = StaticIndicator(data) + indicator2 = AddIndicator(data) + for idx, candle in enumerate(candles): + value = test_series[idx] + candle.open = value + data.update(candle) + assert indicator < indicator2 + + assert len(indicator._values) == len(test_series) + assert indicator.value == test_series.iloc[-1] + assert len(indicator2._values) == len(test_series) + assert indicator2.value == test_series.iloc[-1] + 1 diff --git a/tests/unit/test_instrument_history.py b/tests/unit/test_instrument_history.py new file mode 100644 index 0000000..f731005 --- /dev/null +++ b/tests/unit/test_instrument_history.py @@ -0,0 +1,144 @@ +import random +import time +from datetime import datetime, timedelta + +import pandas as pd +import pytest + +from pytrade.models.instruments import ( + COLUMNS, + INDEX, + MINUTES_MAP, + Candlestick, + Granularity, + Instrument, + InstrumentCandles, +) + + +def get_candles(count: int, granularity: Granularity): + start_time = datetime.now() + _delta = MINUTES_MAP[granularity] + return [ + Candlestick( + Instrument.EURUSD, + granularity, + random.uniform(0, 10), + random.uniform(0, 10), + random.uniform(0, 10), + random.uniform(0, 10), + start_time + timedelta(minutes=_delta * i), + ) + for i in range(count) + ] + + +def candles_to_data(candles: list[Candlestick]): + return [ + [ + candlestick.timestamp, + candlestick.instrument, + candlestick.open, + candlestick.high, + candlestick.low, + candlestick.close, + ] + for candlestick in candles + ] + + +def test_max_history(): + max_history = 10 + + history = InstrumentCandles(max_size=max_history) + + dummy_candles = get_candles(max_history + 1, Granularity.M5) + final_df = pd.DataFrame(candles_to_data(dummy_candles[1:]), columns=COLUMNS) + final_df.set_index(INDEX, inplace=True) + for candle in dummy_candles[:max_history]: + history.update(candle) + + assert len(history.df) == max_history + history.update(dummy_candles[-1]) + assert len(history.df) == max_history + + +def test_fifo(): + max_history = 10 + + history = InstrumentCandles(max_size=max_history) + + dummy_candles = get_candles(max_history + 1, Granularity.M5) + final_df = pd.DataFrame(candles_to_data(dummy_candles[1:]), columns=COLUMNS) + final_df.set_index(INDEX, inplace=True) + for candle in dummy_candles[:max_history]: + history.update(candle) + + history.update(dummy_candles[-1]) + assert history.df.equals(final_df) + + +@pytest.mark.parametrize( + "granularity", [Granularity.M1, Granularity.M5, Granularity.H1, Granularity.H4] +) +def test_shift_freq(granularity: Granularity): + max_history = 10 + + history = InstrumentCandles(max_size=max_history) + + dummy_candles = get_candles(max_history + 1, granularity) + final_df = pd.DataFrame(candles_to_data(dummy_candles[1:]), columns=COLUMNS) + final_df.set_index(INDEX, inplace=True) + for candle in dummy_candles[:max_history]: + history.update(candle) + + history.update(dummy_candles[-1]) + assert history.df.equals(final_df) + + +@pytest.mark.parametrize("scale", [10, 1000, 10000]) +def test_update_execution_time(scale: int): + + dummy_candles = get_candles(scale + 1, Granularity.M1) + final_df = pd.DataFrame(candles_to_data(dummy_candles[1:]), columns=COLUMNS) + final_df.set_index(INDEX, inplace=True) + + initial_df = pd.DataFrame.from_records( + [c.to_dict() for c in dummy_candles[:scale]], columns=COLUMNS + ) + history = InstrumentCandles(data=initial_df, max_size=scale) + + start_time = time.time() + history.update(dummy_candles[-1]) + run_time = time.time() - start_time + assert run_time < 0.003 + + +def test_update_wrong_instrument(): + dummy_candles = get_candles(2, Granularity.M1) + + with pytest.raises(RuntimeError): + history = InstrumentCandles(max_size=10) + first_candle = dummy_candles[0] + second_candle = dummy_candles[1] + second_candle.instrument = Instrument.GBPUSD + + assert first_candle.instrument != second_candle.instrument + + history.update(first_candle) + history.update(second_candle) + + +def test_update_wrong_granularity(): + dummy_candles = get_candles(2, Granularity.M1) + + with pytest.raises(RuntimeError): + history = InstrumentCandles(max_size=10) + first_candle = dummy_candles[0] + second_candle = dummy_candles[1] + second_candle.granularity = Granularity.H1 + + assert first_candle.granularity != second_candle.granularity + + history.update(first_candle) + history.update(second_candle) diff --git a/tests/unit/test_strategy.py b/tests/unit/test_strategy.py new file mode 100644 index 0000000..5cc2049 --- /dev/null +++ b/tests/unit/test_strategy.py @@ -0,0 +1,165 @@ +import asyncio +import random +from datetime import datetime, timedelta +from typing import List +from unittest.mock import MagicMock, call, patch + +import pytest + +from pytrade.models.instruments import ( + MINUTES_MAP, + CandleData, + Candlestick, + CandleSubscription, + Granularity, + Instrument, +) +from pytrade.strategy import FxStrategy + +TEST_SUBCRIPTIONS = [ + CandleSubscription(Instrument.EURUSD, Granularity.M5), + CandleSubscription(Instrument.EURUSD, Granularity.M15), + CandleSubscription(Instrument.GBPUSD, Granularity.M5), + CandleSubscription(Instrument.GBPUSD, Granularity.M15), +] + +TEST_UPDATES = [ + CandleSubscription(Instrument.EURUSD, Granularity.M5), + CandleSubscription(Instrument.EURUSD, Granularity.M5), + CandleSubscription(Instrument.EURUSD, Granularity.M5), + CandleSubscription(Instrument.EURUSD, Granularity.M15), + CandleSubscription(Instrument.GBPUSD, Granularity.M5), + CandleSubscription(Instrument.GBPUSD, Granularity.M5), + CandleSubscription(Instrument.GBPUSD, Granularity.M5), + CandleSubscription(Instrument.GBPUSD, Granularity.M15), +] + + +def get_candles( + count: int, instrument: Instrument, granularity: Granularity, end_time: datetime +) -> list[Candlestick]: + _delta = MINUTES_MAP[granularity] + return [ + Candlestick( + instrument, + granularity, + random.uniform(0, 10), + random.uniform(0, 10), + random.uniform(0, 10), + random.uniform(0, 10), + end_time - timedelta(minutes=_delta * i), + ) + for i in range(count) + ] + + +def get_updates(): + end_time = datetime.now() + update_candles: list[Candlestick] = [] + max_interval = max(MINUTES_MAP[sub.granularity] for sub in TEST_SUBCRIPTIONS) + for subscription in TEST_SUBCRIPTIONS: + num_candles = int(max_interval / MINUTES_MAP[subscription.granularity]) + update_candles += get_candles( + num_candles, subscription.instrument, subscription.granularity, end_time + ) + + update_candles.sort(key=lambda c: c.timestamp) + + return update_candles + + +def send_strategy_updates(strategy: FxStrategy): + update_candles = get_updates() + + updates = TEST_UPDATES.copy() + for candle in update_candles: + strategy._update_instrument(candle) + updates.remove(CandleSubscription(candle.instrument, candle.granularity)) + assert len(strategy._pending_updates) == len(updates) + + +class _TestStrategy(FxStrategy): + + @property + def subscriptions(self) -> List[CandleSubscription]: + return TEST_SUBCRIPTIONS + + def _init(self): + pass + + def _next(self): + pass + + +def test_monitor_instruments(): + broker = MagicMock() + data_context = CandleData() + + strategy = _TestStrategy(broker, data_context) + strategy.init() + + subscribe_calls = [ + call(s.instrument, s.granularity, strategy._update_instrument) + for s in TEST_SUBCRIPTIONS + ] + + broker.subscribe.assert_has_calls(subscribe_calls) + + +def test_instrument_updates(): + broker = MagicMock() + data_context = CandleData() + + strategy = _TestStrategy(broker, data_context) + with patch.object(strategy, "_updates_complete", MagicMock()) as mock_updates_event: + strategy.init() + + assert strategy._pending_updates == strategy._required_updates + + send_strategy_updates(strategy) + + assert strategy._pending_updates == [] + assert mock_updates_event.set.called + + +@pytest.mark.asyncio +async def test_strategy_next(): + broker = MagicMock() + data_context = CandleData() + + strategy = _TestStrategy(broker, data_context) + with patch.object(strategy, "_next", MagicMock()) as mock_next: + strategy.init() + + assert not mock_next.called + + send_strategy_updates(strategy) + + await strategy.next() + assert len(strategy._pending_updates) == len(TEST_UPDATES) + assert strategy._pending_updates == strategy._required_updates + + +@pytest.mark.asyncio +async def test_strategy_waits_for_updates(): + broker = MagicMock() + data_context = CandleData() + + strategy = _TestStrategy(broker, data_context) + iterations = 2 + with patch.object(strategy, "_next", MagicMock()) as mock_next: + strategy.init() + for i in range(iterations): + + next_task = asyncio.create_task(strategy.next()) + assert not mock_next.called + assert not next_task.done() + + send_strategy_updates(strategy) + + await asyncio.wait_for(next_task, timeout=0.2) + assert mock_next.called + assert next_task.done() + assert len(strategy._pending_updates) == len(TEST_UPDATES) + assert strategy._pending_updates == strategy._required_updates + mock_next.reset_mock()