diff --git a/notebooks/nn_response_evaluation.ipynb b/notebooks/nn_response_evaluation.ipynb new file mode 100644 index 0000000..7689c1f --- /dev/null +++ b/notebooks/nn_response_evaluation.ipynb @@ -0,0 +1,1671 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "cellId": "5nmr5qtyi4l6dv4003f5hk", + "execution_id": "b4c3a56f-dd79-4dc8-9c15-e451a726c6d8" + }, + "source": [ + "# RS evaluation with a custom response model" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cellId": "a23y5u7l746q73y0tqxss", + "execution_id": "b0740c39-9cda-411d-b832-490de8695080" + }, + "source": [ + "## Imports and preparations" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2\n", + "# !pip install jupyter-black\n", + "%load_ext jupyter_black" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import sys\n", + "\n", + "os.environ[\"JAVA_HOME\"] = \"/usr\"\n", + "os.environ[\"PYSPARK_PYTHON\"] = sys.executable\n", + "os.environ[\"PYSPARK_DRIVER_PYTHON\"] = sys.executable\n", + "os.environ[\"CUDA_LAUNCH_BLOCKING\"] = \"1\"\n", + "os.environ[\"OMP_NUM_THREADS\"] = \"32\"\n", + "os.environ[\"NUMBA_NUM_THREADS\"] = \"4\"" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "cellId": "jlz0twee16goe1fisulv5" + }, + "outputs": [], + "source": [ + "import random\n", + "import time\n", + "import scipy\n", + "import torch\n", + "import numpy as np\n", + "import pandas as pd\n", + "import warnings\n", + "\n", + "from pyspark import SparkConf, StorageLevel\n", + "from pyspark.ml import Pipeline\n", + "from pyspark.sql import SparkSession\n", + "import pyspark.sql.functions as sf\n", + "from replay.models import UCB\n", + "from tqdm.auto import tqdm\n", + "\n", + "from experiments.response_models.utils import plot_metric, calc_metric\n", + "from pyspark.sql.functions import col" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "cellId": "jlz0twee16goe1fisulv5" + }, + "outputs": [], + "source": [ + "from sim4rec.modules import RealDataGenerator\n", + "from sim4rec.modules import Simulator\n", + "from sim4rec.response import BernoulliResponse, ActionModelTransformer\n", + "\n", + "\n", + "def warn(*args, **kwargs):\n", + " pass\n", + "\n", + "\n", + "warnings.warn = warn" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "# path to spark tmp folder and simulator checkpoints\n", + "SPARK_LOCAL_DIR = \"./tmp/task_1\"\n", + "CHECKPOINT_DIR = \"./tmp/task_1_checkpoints\"" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "cellId": "ku55idy38nqtlrc187ucq" + }, + "outputs": [], + "source": [ + "%%bash -s \"$CHECKPOINT_DIR\" \"$SPARK_LOCAL_DIR\"\n", + "# simulator saves the interaction history between users and recommender system\n", + "# to rerun the simulation cycle or begin a new one, clear the directory or use another CHECKPOINT_DIR\n", + "rm -rf $1 $2" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "cellId": "v1uc08ym8h8d7qxpmsop4" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "WARNING: An illegal reflective access operation has occurred\n", + "WARNING: Illegal reflective access by org.apache.spark.unsafe.Platform (file:/root/.cache/pypoetry/virtualenvs/sim4rec-lKa0R1gD-py3.9/lib/python3.9/site-packages/pyspark/jars/spark-unsafe_2.12-3.1.3.jar) to constructor java.nio.DirectByteBuffer(long,int)\n", + "WARNING: Please consider reporting this to the maintainers of org.apache.spark.unsafe.Platform\n", + "WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations\n", + "WARNING: All illegal access operations will be denied in a future release\n", + "24/11/24 21:38:08 WARN NativeCodeLoader: Unable to load native-hadoop library for your platform... using builtin-java classes where applicable\n", + "Using Spark's default log4j profile: org/apache/spark/log4j-defaults.properties\n", + "Setting default log level to \"WARN\".\n", + "To adjust logging level use sc.setLogLevel(newLevel). For SparkR, use setLogLevel(newLevel).\n", + "24/11/24 21:38:09 WARN SparkConf: Note that spark.local.dir will be overridden by the value set by the cluster manager (via SPARK_LOCAL_DIRS in mesos/standalone/kubernetes and LOCAL_DIRS in YARN).\n" + ] + } + ], + "source": [ + "NUM_THREADS = 4\n", + "\n", + "spark = (\n", + " SparkSession.builder.appName(\"simulator\")\n", + " .master(f\"local[{NUM_THREADS}]\")\n", + " .config(\"spark.sql.shuffle.partitions\", f\"{NUM_THREADS * 3}\")\n", + " .config(\"spark.default.parallelism\", f\"{NUM_THREADS * 3}\")\n", + " .config(\"spark.driver.memory\", \"4G\")\n", + " .config(\"spark.executor.memory\", \"4G\")\n", + " .config(\"spark.driver.extraJavaOptions\", \"-XX:+UseG1GC\")\n", + " .config(\"spark.executor.extraJavaOptions\", \"-XX:+UseG1GC\")\n", + " .config(\"spark.local.dir\", SPARK_LOCAL_DIR)\n", + " .getOrCreate()\n", + ")\n", + "\n", + "spark.sparkContext.setLogLevel(\"ERROR\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "cellId": "9vf3sqixr4vgau073549pv" + }, + "outputs": [], + "source": [ + "K = 10 # number of iterations\n", + "NUM_ITER = 10\n", + "SEED = 1234\n", + "np.random.seed(SEED)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cellId": "ffl9l89trncxzwbk5zq7gn", + "execution_id": "cbbf5328-af84-4368-957e-3c000bd80332" + }, + "source": [ + "## Data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Load ContentWise in tabular form. Uncomment for the first run.\n", + "\n", + "This preprocessing is similat to preprocessing in old `ContentWise` class: \n", + "join interactions with impressions, binarize clicks, etc.\n", + "\n", + "ContentWise consists of following files:\n", + "1. interactions.csv.gz:\n", + " * `utc_ts_milliseconds` -- interaction timestamp \n", + " * `user_id` -- user id\n", + " * `item_id`, `series_id` -- video id, video series id. As long as only series were recommended, we threat 'series' as primary items\n", + " * `recommendation_id` -- key to match with recommendations\n", + " * `episode_number`, `series_length`, `item_type`, `interaction_type`, `vision_factor`, `explicit_rating` are ignored\n", + "\n", + "2. impressions-direct-link.csv.gz:\n", + " * `recommendation_id` -- impression id to match with responses, `-1` for unknown recommendation\n", + " * `recommendation_list_length`\n", + " * `recommended_series_list` -- string representation of python list of series ids\n", + " * `row_position` -- a position of recommendation slate on the site" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "# cw_impr = \"data/ContentWiseImpressions/CW10M-CSV/impressions-direct-link.csv\"\n", + "# impressions = spark.read.option(\"header\", \"true\").csv(cw_impr)\n", + "# impressions = impressions.filter(col(\"recommendation_list_length\") == K)\n", + "# impressions = impressions.withColumn(\n", + "# \"tmp\", sf.regexp_replace(\"recommended_series_list\", r\"\\[\\s*\", \"\")\n", + "# )\n", + "# impressions = impressions.withColumn(\"tmp\", sf.regexp_replace(\"tmp\", r\"\\s*\\]\", \"\"))\n", + "# impressions = impressions.withColumn(\"tmp\", sf.regexp_replace(\"tmp\", r\"\\s+\", \" \"))\n", + "# impressions = impressions.withColumn(\"tmp\", sf.split(\"tmp\", \" \"))\n", + "# impressions = impressions.select(\n", + "# \"recommendation_id\", sf.posexplode(\"tmp\").alias(\"slate_pos\", \"item_id\")\n", + "# ).drop(\"tmp\", \"recommendation_list_length\", \"recommended_series_list\", \"row_position\")\n", + "# cw_inter = \"data/ContentWiseImpressions/CW10M-CSV/interactions.csv\"\n", + "# interactions = spark.read.option(\"header\", \"true\").csv(cw_inter)\n", + "# interactions = interactions.filter(col(\"recommendation_id\") != -1)\n", + "# interactions = interactions.drop(\n", + "# \"item_id\",\n", + "# \"episode_number\",\n", + "# \"series_length\",\n", + "# \"item_type\",\n", + "# \"interaction_type\",\n", + "# \"vision_factor\",\n", + "# \"explicit_rating\",\n", + "# )\n", + "# interactions = interactions.withColumnRenamed(\"series_id\", \"item_id\")\n", + "\n", + "# recommendation_metadata = interactions.groupBy(\"recommendation_id\").agg(\n", + "# sf.min(\"utc_ts_milliseconds\").alias(\"recommendation_timestamp\"),\n", + "# sf.first(\"user_id\").alias(\"user_id\"),\n", + "# )\n", + "# # click om item = first click on item\n", + "# interactions = interactions.drop(\"user_id\")\n", + "# interactions = interactions.groupBy(\"recommendation_id\", \"item_id\").agg(\n", + "# sf.min(\"utc_ts_milliseconds\").alias(\"response_timestamp\")\n", + "# )\n", + "# # add response\n", + "# interactions = interactions.withColumn(\"response\", sf.lit(1))\n", + "# content_wise_all = impressions.join(\n", + "# interactions, how=\"left\", on=[\"recommendation_id\", \"item_id\"]\n", + "# ).join(recommendation_metadata, on=\"recommendation_id\")\n", + "# content_wise_all = content_wise_all.withColumn(\n", + "# \"response\", sf.coalesce(\"response\", sf.lit(0))\n", + "# )\n", + "\n", + "# # split train-val-test by global time:\n", + "# total_cnt = content_wise_all.count()\n", + "# train_cnt = int(0.6 * total_cnt)\n", + "# test_cnt = int(0.2 * total_cnt)\n", + "# val_cnt = int(0.2 * total_cnt)\n", + "# sorted_df = content_wise_all.orderBy(\"recommendation_timestamp\")\n", + "# train = sorted_df.limit(train_cnt)\n", + "# val = sorted_df.subtract(train).limit(val_cnt)\n", + "# test = sorted_df.subtract(train).subtract(val)\n", + "# MAX_TIME = content_wise_all.agg(sf.max(\"recommendation_timestamp\")).collect()[0][0]\n", + "# MAX_TIME" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "# def content_wise_to_simlog_format(df: \"DataFrame\"):\n", + "# \"\"\"Convert ContentWise dataset to math sim4rec log schema\"\"\"\n", + "# df = df.drop(\"response_timestamp\")\n", + "# df = df.withColumn(\"response_proba\", sf.col(\"response\").cast(\"float\"))\n", + "# df = df.withColumnRenamed(\"item_id\", \"item_idx\")\n", + "# df = df.withColumnRenamed(\"user_id\", \"user_idx\")\n", + "# # place all historical interactions BEFORE the simulated\n", + "# df = df.withColumn(\n", + "# \"__iter\",\n", + "# (sf.col(\"recommendation_timestamp\").cast(\"bigint\") - int(MAX_TIME)) / 1000,\n", + "# ).drop(\"recommendation_timestamp\", \"recommendation_id\")\n", + "# return df\n", + "\n", + "\n", + "# train = content_wise_to_simlog_format(train)\n", + "# test = content_wise_to_simlog_format(test)\n", + "# val = content_wise_to_simlog_format(val)\n", + "# val.show(10)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "# from sim4rec.response.nn_response import SIM_LOG_SCHEMA\n", + "# print(SIM_LOG_SCHEMA, train.schema)\n", + "\n", + "# # cast types to match simlog schema\n", + "# train = train.withColumn(\"relevance\", 1 / (sf.col(\"slate_pos\") + 1))\n", + "# val = train.withColumn(\"relevance\", 1 / (sf.col(\"slate_pos\") + 1))\n", + "# test = train.withColumn(\"relevance\", 1 / (sf.col(\"slate_pos\") + 1))\n", + "\n", + "# for field in SIM_LOG_SCHEMA:\n", + "# train = train.withColumn(field.name, train[field.name].cast(field.dataType))\n", + "# val = val.withColumn(field.name, val[field.name].cast(field.dataType))\n", + "# test = test.withColumn(field.name, test[field.name].cast(field.dataType))" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "# # this takes some time\n", + "# !rm -rdf parquet && mkdir parquet\n", + "# train.write.partitionBy(\"user_idx\").parquet(\"parquet/train\")\n", + "# val.write.partitionBy(\"user_idx\").parquet(\"parquet/val\")\n", + "# test.write.partitionBy(\"user_idx\").parquet(\"parquet/test\")" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + } + ], + "source": [ + "# re-read:\n", + "train = spark.read.parquet(\"../parquet/train\")\n", + "val = spark.read.parquet(\"../parquet/val\")\n", + "test = spark.read.parquet(\"../parquet/test\")" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+--------+---------+--------+--------------+--------+-------------------+--------+\n", + "|item_idx|slate_pos|response|response_proba| __iter| relevance|user_idx|\n", + "+--------+---------+--------+--------------+--------+-------------------+--------+\n", + "| 25508| 0| 0| 0.0|-8211853| 1.0| 35783|\n", + "| 9245| 8| 0| 0.0|-8211853| 0.1111111111111111| 35783|\n", + "| 5368| 6| 1| 1.0|-8211853|0.14285714285714285| 35783|\n", + "| 5939| 4| 0| 0.0|-8211853| 0.2| 35783|\n", + "| 10685| 5| 0| 0.0|-8211853|0.16666666666666666| 35783|\n", + "| 21058| 3| 0| 0.0|-8211853| 0.25| 35783|\n", + "| 5018| 9| 0| 0.0|-8211853| 0.1| 35783|\n", + "| 22613| 7| 0| 0.0|-8211853| 0.125| 35783|\n", + "| 17175| 2| 0| 0.0|-8211853| 0.3333333333333333| 35783|\n", + "| 7618| 1| 0| 0.0|-8211853| 0.5| 35783|\n", + "+--------+---------+--------+--------------+--------+-------------------+--------+\n", + "only showing top 10 rows\n", + "\n" + ] + } + ], + "source": [ + "train.show(10)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "cellId": "zh9qtijcn5143mi4887od" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "data": { + "text/plain": [ + "(2658, 19205)" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "users = [\n", + " [int(row[\"user_idx\"])] for row in train.select(\"user_idx\").distinct().collect()\n", + "]\n", + "NUM_USERS = len(users)\n", + "users = spark.createDataFrame(users, schema=[\"user_idx\"])\n", + "\n", + "items = [\n", + " [int(row[\"item_idx\"])] for row in train.select(\"item_idx\").distinct().collect()\n", + "]\n", + "NUM_ITEMS = len(items)\n", + "items = spark.createDataFrame(items, schema=[\"item_idx\"])\n", + "\n", + "NUM_ITEMS, NUM_USERS" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "cellId": "3btpzal8hblukqhq46asd" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "data": { + "text/plain": [ + "2658" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "item_generator = RealDataGenerator(label=\"items_real\", seed=SEED)\n", + "user_generator = RealDataGenerator(label=\"users_real\", seed=SEED)\n", + "\n", + "item_generator.fit(items)\n", + "user_generator.fit(users)\n", + "\n", + "item_generator.generate(NUM_ITEMS)\n", + "user_generator.generate(NUM_USERS)\n", + "item_generator.getDataSize()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cellId": "1crcvslqi2qm4dnavh3obq", + "execution_id": "2c640f43-9dd5-445d-9511-d0d100fc830c" + }, + "source": [ + "# One iteration of simulation cycle step by step" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cellId": "q2oio9pylodwwu9qptdom", + "execution_id": "0f252faf-52c4-4a99-ac83-b425c14d055e" + }, + "source": [ + "## (1) Choise of users" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": { + "cellId": "erkzmjzvo06nl5bok1qv5n" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "1964" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "user_generator.sample(0.1).count()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cellId": "tnbcus0l6hcbvoht2rnge", + "execution_id": "a45bc1e9-f284-4702-bfeb-d525f9349d20" + }, + "source": [ + "## (2) Choise of items\n", + "During the simulation cycle all 5276 items will be available at each iteration." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cellId": "4kfl2on9vg46431mkek4ot", + "execution_id": "85a06070-5f73-407a-ad84-ff781e7d04fb" + }, + "source": [ + "## (3) Initialization of recommender model" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cellId": "p7qktnabe0pupwrpgo1vu", + "execution_id": "4f4957f4-cea9-47fe-9140-95dafb48e159" + }, + "source": [ + "### Baseline" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": { + "cellId": "kck71xz164nwlq1tlo81d" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
user_idxitem_idxrelevance
0232163300.000376
1232179150.000376
223288175990.000376
323288146270.000376
\n", + "
" + ], + "text/plain": [ + " user_idx item_idx relevance\n", + "0 232 16330 0.000376\n", + "1 232 17915 0.000376\n", + "2 23288 17599 0.000376\n", + "3 23288 14627 0.000376" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# RePlay model should be fitted on historic data before it can be refitted in cycle\n", + "# Let's assume we had one interaction for simplicity\n", + "model = UCB(sample=True, seed=SEED, exploration_coef=0.1)\n", + "model.fit(\n", + " log=users.limit(1).crossJoin(items.limit(1)).withColumn(\"relevance\", sf.lit(1))\n", + ")\n", + "pred = model.predict(log=None, users=users.limit(2), items=items, k=2)\n", + "pred.toPandas()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cellId": "7m11axprgbnc8wgrnr20km", + "execution_id": "1bf409d8-8df8-4ff6-a96d-eab7b07f54d8" + }, + "source": [ + "The response function in this tutorial is very simple. The response is binary, response == 1 means the user bought the item. \n", + "The probability of response is one tenth of the last digit of an `item_idx`. The response function is constant over time.\n", + "\n", + "Response function gets dataframe with columns `user_idx`, `item_idx`, `relevance`, and returns dataframe with columns `user_idx`, `item_idx`, `relevance`, `response`, adding the response column to the initial ones. If the initial dataframe had some other columns, these will also be returned." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Example how the response is generated for user-item pair" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## (4) Neural response function" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [], + "source": [ + "from sim4rec.response import NNResponseEstimator, NNResponseTransformer\n", + "\n", + "proba_model = NNResponseEstimator(\n", + " outputCol=\"response_proba\",\n", + " log_dir=f\"{CHECKPOINT_DIR}/pipeline\",\n", + " model_name=\"SlatewiseTransformer\",\n", + " hist_data_dir=\"../parquet/train\",\n", + " val_data_dir=\"../parquet/val\",\n", + " batch_size=256,\n", + " lr=1e-3,\n", + " num_epochs=5,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'1.9.1+cu102'" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "torch.__version__" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c92619a38c4448efbf86acaa2e3404b6", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "epoch: 0%| | 0/5 [00:00 (0 + 1) / 1]\r" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+--------+--------+--------------------+-------------------+\n", + "|user_idx|item_idx| relevance| response_proba|\n", + "+--------+--------+--------------------+-------------------+\n", + "| 23288| 17599|3.762227238525207E-4|0.19280970096588135|\n", + "| 23288| 14627|3.762227238525207E-4| 0.1980724036693573|\n", + "| 232| 16330|3.762227238525207E-4| 0.1340920478105545|\n", + "| 232| 17915|3.762227238525207E-4|0.07575525343418121|\n", + "+--------+--------+--------------------+-------------------+\n", + "\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + } + ], + "source": [ + "test_response = proba_model.transform(pred)\n", + "test_response.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Pipelines work as well" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [], + "source": [ + "br = BernoulliResponse(inputCol=\"response_proba\", outputCol=\"response\", seed=SEED)\n", + "response_pipeline = Pipeline(stages=[proba_model, br])\n", + "response_pipeline = response_pipeline.fit(items)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + } + ], + "source": [ + "test_response = response_pipeline.transform(pred)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
user_idxitem_idxrelevanceresponse_probaresponse
023288175990.0003760.1928100
123288146270.0003760.1980720
2232163300.0003760.1340920
3232179150.0003760.0757550
\n", + "
" + ], + "text/plain": [ + " user_idx item_idx relevance response_proba response\n", + "0 23288 17599 0.000376 0.192810 0\n", + "1 23288 14627 0.000376 0.198072 0\n", + "2 232 16330 0.000376 0.134092 0\n", + "3 232 17915 0.000376 0.075755 0" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "test_response.toPandas()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cellId": "oklaxbiawagxloskdgy6g", + "execution_id": "418b174f-f4be-49fe-88aa-585826871197" + }, + "source": [ + "## (5) Fitting of new recommender model" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After receiving responses to recommendations, we can use this information to update the recommender model. To do this, we should rename column `response` to `relevance`." + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
user_idxitem_idxrelevance
023288175990
123288146270
2232163300
3232179150
\n", + "
" + ], + "text/plain": [ + " user_idx item_idx relevance\n", + "0 23288 17599 0\n", + "1 23288 14627 0\n", + "2 232 16330 0\n", + "3 232 17915 0" + ] + }, + "execution_count": 29, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "new_log = test_response.drop(\"relevance\", \"response_proba\").withColumnRenamed(\n", + " \"response\", \"relevance\"\n", + ")\n", + "new_log.limit(5).toPandas()" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
item_idxrelevance
0163300.37233
1179150.37233
2146270.37233
3175990.37233
\n", + "
" + ], + "text/plain": [ + " item_idx relevance\n", + "0 16330 0.37233\n", + "1 17915 0.37233\n", + "2 14627 0.37233\n", + "3 17599 0.37233" + ] + }, + "execution_count": 30, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "model.fit(log=new_log)\n", + "model.item_popularity.limit(5).toPandas()" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cellId": "ytr06uktdlrtba7yym35z", + "execution_id": "875247d7-4576-4f2e-8524-57a90ec7b530" + }, + "source": [ + "## (6) Quality of recommendations" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cellId": "rlp6n6ghul6do1y9tnfs3", + "execution_id": "c66c5616-7b51-46fb-bf64-ff3aae3560a3" + }, + "source": [ + "Quality is the mean number of positive responses per user." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": { + "cellId": "ggmpn0ggz9nq4qt06ubae" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + }, + { + "data": { + "text/plain": [ + "0.0" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "calc_metric(test_response)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cellId": "o0o3kq6tjrs7tuvbdikavd", + "execution_id": "7c137458-8817-41ef-aa39-c255fe3286f6" + }, + "source": [ + "# Training the model in the simulator" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": { + "cellId": "kkg4h3xdwqhgpk4gs9y37m" + }, + "outputs": [], + "source": [ + "%%bash -s \"$CHECKPOINT_DIR\"\n", + "rm -rf $1" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cellId": "wkff236bnjdemsw693fj6s", + "execution_id": "2f344684-ba2b-4d19-9079-527e6e2d7969" + }, + "source": [ + "## Simulator initialization" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": { + "cellId": "9eplt0wb7cnd273dkt5n6" + }, + "outputs": [], + "source": [ + "user_generator.initSeedSequence(SEED)\n", + "item_generator.initSeedSequence(SEED)\n", + "\n", + "sim = Simulator(\n", + " user_gen=user_generator,\n", + " item_gen=item_generator,\n", + " data_dir=f\"{CHECKPOINT_DIR}/pipeline\",\n", + " user_key_col=\"user_idx\",\n", + " item_key_col=\"item_idx\",\n", + " spark_session=spark,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cellId": "32xq7dw4q0uk4bwf04bzac", + "execution_id": "5a37dcac-ecb3-4c8a-b613-96059682e31d" + }, + "source": [ + "### Check that the format is correct" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": { + "cellId": "mz1tro3j75j8a8hcuvjvl7" + }, + "outputs": [], + "source": [ + "pred = model.predict(log=None, users=users.limit(10), items=items, k=K)\n", + "\n", + "assert pred.columns == [\"user_idx\", \"item_idx\", \"relevance\"]\n", + "assert (\n", + " pred.groupBy(\"user_idx\")\n", + " .agg(sf.countDistinct(\"item_idx\").alias(\"num_items\"))\n", + " .filter(sf.col(\"num_items\") == sf.lit(K))\n", + " .count()\n", + " == 10\n", + ")\n", + "assert (\n", + " pred.groupBy(\"user_idx\")\n", + " .agg(sf.count(\"item_idx\").alias(\"num_items\"))\n", + " .filter(sf.col(\"num_items\") == sf.lit(K))\n", + " .count()\n", + " == 10\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [], + "source": [ + "log = sim.get_log(users)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cellId": "363j2r5xezoduowlgowzy8", + "execution_id": "23bcc021-9eb1-4d41-b46b-6a05442fd92e" + }, + "source": [ + "## Response function initialization" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [], + "source": [ + "proba_model = NNResponseTransformer.load(\"response_function_checkpoint\")\n", + "br = BernoulliResponse(inputCol=\"response_proba\", outputCol=\"response\", seed=SEED)\n", + "response_pipeline = Pipeline(stages=[proba_model, br])\n", + "response_model = response_pipeline.fit(items)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "cellId": "1t5x6egzvjmz7yk9tc7gf", + "execution_id": "50e4dc71-1b47-4832-af9f-efcd08b5b47c" + }, + "source": [ + "## Simulation cycle" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "cellId": "9xd6azbslgcdb0hsl7sm" + }, + "outputs": [ + { + "data": { + "image/png": "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IoiPj29WFuL06dPo0qWLSBE5ttraWkgkN/5zLZVKodVqRYrIPnAGyEIWLlyI6dOnIy4uDoMHD8Z7772HmpoazJgxQ+zQHNKcOXOwfv16fPvtt1AqlSgqKgIAeHp6wsXFReToHItSqWy298rNzQ0+Pj7ckyWSF198EcOGDcObb76JSZMm4fDhw1i9ejVWr14tdmgOacKECfjHP/6BsLAwREVFIT09He+88w5mzpwpdmg2jcfgLWjlypX417/+haKiIsTExOD999/HkCFDxA7LIQmC0OL1tWvX4plnnrFsMNTM8OHDeQxeZNu3b0dCQgLOnDmDiIgILFy4EM8++6zYYTmkqqoqLF68GFu2bEFJSQmCgoIwefJkvPrqq3BychI7PJvFBIiIiIgcDvcAERERkcNhAkREREQOhwkQERERORwmQERERORwmAARERGRw2ECRERERA6HCRARERE5HCZARERE5HCYABGR6IYPH44FCxaIHcYNBEHA1q1bxQ6DiMyElaCJSHTl5eWQy+VQKpUIDw/HggULLJYQLVmyBFu3bkVGRsYN14uKiuDl5QWFQmGROIjIstgMlYhE5+3tbfJnNjQ03FafpM6dO5swGiKyNlwCIyLRGZbAhg8fjosXL+LFF1+EIAg3NK3du3cv7rrrLri4uCA0NBTz589HTU2N8f3h4eF44403MG3aNHh4eGDWrFkAgJdffhk9e/aEq6srunbtisWLF0OtVgMAEhMTsXTpUmRmZhpfLzExEUDzJbBjx47h3nvvhYuLC3x8fDBr1ixUV1cb3//MM8/goYcewttvv43AwED4+Phgzpw5xtciIuvCBIiIrMbmzZsREhKC119/HYWFhSgsLAQAnDt3Dvfddx8effRRHD16FBs3bsTevXsxd+7cGz7+7bffRnR0NNLT07F48WIAgFKpRGJiIrKzs/Gf//wHn3zyCd59910AwBNPPIGXXnoJUVFRxtd74oknmsVVU1ODsWPHwsvLCykpKfjqq6/wyy+/NHv9X3/9FefOncOvv/6KTz/9FImJicaEioisC5fAiMhqeHt7QyqVQqlU3rAEtXz5ckydOtW4L6hHjx54//33cc899+DDDz+Es7MzAODee+/FSy+9dMMzX3nlFeP/h4eH489//jM2bNiAv/71r3BxcYG7uztkMlmrS17r169HfX091q1bBzc3NwDAypUrMWHCBKxYsQIBAQEAAC8vL6xcuRJSqRSRkZEYP348du7ciWeffdYkXx8iMh0mQERk9TIzM3H06FF88cUXxms6nQ5arRY5OTno3bs3ACAuLq7Zx27cuBHvv/8+zp07h+rqajQ2NsLDw6Ndr3/ixAlER0cbkx8AiI+Ph1arxalTp4wJUFRUFKRSqfGewMBAHDt2rF2vRUSWwQSIiKxedXU1Zs+ejfnz5zd7X1hYmPH/r09QAODAgQOYOnUqli5dirFjx8LT0xMbNmzAv//9b7PEKZfLb3hbEARotVqzvBYR3R4mQERkVZycnKDRaG64NmDAAGRnZ6N79+7tetb+/fvRpUsX/P3vfzdeu3jx4i1f7/d69+6NxMRE1NTUGJOsffv2QSKRoFevXu2KiYisAzdBE5FVCQ8PR3JyMvLz81FWVgZAf5Jr//79mDt3LjIyMnDmzBl8++23zTYh/16PHj2Qm5uLDRs24Ny5c3j//fexZcuWZq+Xk5ODjIwMlJWVQaVSNXvO1KlT4ezsjOnTpyMrKwu//vor5s2bh6efftq4/EVEtoUJEBFZlddffx0XLlxAt27d4OfnBwDo378/du/ejdOnT+Ouu+5CbGwsXn31VQQFBbX6rIkTJ+LFF1/E3LlzERMTg/379xtPhxk8+uijuO+++zBixAj4+fnhyy+/bPYcV1dX/PzzzygvL8egQYPw2GOPYeTIkVi5cqXpPnEisihWgiYiIiKHwxkgIiIicjhMgIiIiMjhMAEiIiIih8MEiIiIiBwOEyAiIiJyOEyAiIiIyOEwASIiIiKHwwSIiIiIHA4TICIiInI4TICIiIjI4TABIiIiIofz/7Hu5ehDeFUtAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Iteration 9 finished in 72.66888523101807 sec.\n" + ] + } + ], + "source": [ + "metrics = []\n", + "# sim.update_log(spark.createDataFrame([], schema=SIM_LOG_SCHEMA), iteration=-1)\n", + "\n", + "for i in range(NUM_ITER):\n", + " print(f\"Iteration {i} started\")\n", + " iter_time = time.time()\n", + " # visiting users\n", + " current_users = sim.sample_users(0.05).cache()\n", + " user_idx = current_users.select(\"user_idx\").rdd.flatMap(lambda x: x).collect()\n", + "\n", + " log = sim.get_log(users)\n", + " recs = model.predict(\n", + " log=log, k=K, users=current_users, items=items, filter_seen_items=False\n", + " )\n", + " # getting responses\n", + " true_resp = sim.sample_responses(\n", + " recs_df=recs,\n", + " user_features=current_users,\n", + " item_features=items,\n", + " action_models=response_model,\n", + " )\n", + " # update the interaction history\n", + " sim.update_log(true_resp, iteration=i)\n", + " # measure quality\n", + " metrics.append(calc_metric(true_resp))\n", + " # refitting the model\n", + " model._clear_cache()\n", + " train_log = sim.log\n", + " model.fit(\n", + " log=train_log.select(\"user_idx\", \"item_idx\", \"response\").withColumnRenamed(\n", + " \"response\", \"relevance\"\n", + " )\n", + " )\n", + "\n", + " current_users.unpersist()\n", + " if log is not None:\n", + " log.unpersist()\n", + " recs.unpersist()\n", + " true_resp.unpersist()\n", + " train_log.unpersist()\n", + "\n", + " plot_metric(metrics)\n", + " print(f\"Iteration {i} finished in {time.time() - iter_time} sec.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "cellId": "zpg8amfm8mvopmyulehl9" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "+--------+--------+--------------------+-------------------+--------+------+\n", + "|item_idx|user_idx| relevance| response_proba|response|__iter|\n", + "+--------+--------+--------------------+-------------------+--------+------+\n", + "| 4743| 90|8.490598951946013E-4|0.23457157611846924| 1| 1|\n", + "| 2483| 90|5.957144256105823E-4|0.24364517629146576| 1| 1|\n", + "| 4641| 90|4.484400617341632E-4|0.23488779366016388| 1| 1|\n", + "| 3372| 90|4.484400617341632E-4| 0.2816031575202942| 0| 1|\n", + "| 25689| 90|3.178713280692018...| 0.2057647705078125| 0| 1|\n", + "+--------+--------+--------------------+-------------------+--------+------+\n", + "only showing top 5 rows\n", + "\n" + ] + } + ], + "source": [ + "sim.log.filter(sf.col(\"__iter\") == 1).filter(sf.col(\"response_proba\") > 0.2).show(5)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Final prediction" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "cellId": "s06ebpys4j0613t0tm6h5p" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + " \r" + ] + } + ], + "source": [ + "recs = model.predict(\n", + " log=sim.log, k=K, users=users, items=items, filter_seen_items=False\n", + ").cache()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "cellId": "jkr7id6a4fotox3u6qyrn" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[Stage 5082:====================================> (48 + 4) / 72]\r" + ] + } + ], + "source": [ + "# responses\n", + "true_resp = sim.sample_responses(\n", + " recs_df=recs,\n", + " user_features=users,\n", + " item_features=items,\n", + " action_models=response_model,\n", + ").cache()\n", + "\n", + "# quality\n", + "print(\n", + " f\"Average number of items purchased per user after model training = {calc_metric(true_resp)}\"\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.19" + }, + "notebookId": "cd4e698e-7892-45e1-a7aa-fe1453f4b402", + "notebookPath": "sources/sim4rec/sber-simulator-main/task_1.ipynb", + "vscode": { + "interpreter": { + "hash": "0c23ac1ac3d03469769ffca4283c7852312778d94b2cbd9b1a60eeafc1c4055f" + } + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/poetry.lock b/poetry.lock index 5de83fa..3377429 100644 --- a/poetry.lock +++ b/poetry.lock @@ -13,13 +13,13 @@ files = [ [[package]] name = "anyio" -version = "4.4.0" +version = "4.5.2" description = "High level compatibility layer for multiple asynchronous event loop implementations" optional = false python-versions = ">=3.8" files = [ - {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, - {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, + {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, + {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, ] [package.dependencies] @@ -29,9 +29,9 @@ sniffio = ">=1.1" typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} [package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.23)"] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21.0b1)"] +trio = ["trio (>=0.26.1)"] [[package]] name = "appnope" @@ -136,21 +136,18 @@ typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} [[package]] name = "asttokens" -version = "2.4.1" +version = "3.0.0" description = "Annotate AST trees with source code positions" optional = false -python-versions = "*" +python-versions = ">=3.8" files = [ - {file = "asttokens-2.4.1-py2.py3-none-any.whl", hash = "sha256:051ed49c3dcae8913ea7cd08e46a606dba30b79993209636c4875bc1d637bc24"}, - {file = "asttokens-2.4.1.tar.gz", hash = "sha256:b03869718ba9a6eb027e134bfdf69f38a236d681c83c160d510768af11254ba0"}, + {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, + {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"}, ] -[package.dependencies] -six = ">=1.12.0" - [package.extras] -astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] -test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] +astroid = ["astroid (>=2,<4)"] +test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "async-lru" @@ -168,19 +165,19 @@ typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} [[package]] name = "attrs" -version = "24.2.0" +version = "24.3.0" description = "Classes Without Boilerplate" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, - {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, + {file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"}, + {file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"}, ] [package.extras] benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] @@ -254,13 +251,13 @@ css = ["tinycss2 (>=1.1.0,<1.3)"] [[package]] name = "certifi" -version = "2024.8.30" +version = "2024.12.14" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"}, - {file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"}, + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, ] [[package]] @@ -344,101 +341,116 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "3.3.2" +version = "3.4.0" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7.0" files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, ] [[package]] @@ -690,33 +702,37 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"] [[package]] name = "debugpy" -version = "1.8.5" +version = "1.8.11" description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" files = [ - {file = "debugpy-1.8.5-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7e4d594367d6407a120b76bdaa03886e9eb652c05ba7f87e37418426ad2079f7"}, - {file = "debugpy-1.8.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4413b7a3ede757dc33a273a17d685ea2b0c09dbd312cc03f5534a0fd4d40750a"}, - {file = "debugpy-1.8.5-cp310-cp310-win32.whl", hash = "sha256:dd3811bd63632bb25eda6bd73bea8e0521794cda02be41fa3160eb26fc29e7ed"}, - {file = "debugpy-1.8.5-cp310-cp310-win_amd64.whl", hash = "sha256:b78c1250441ce893cb5035dd6f5fc12db968cc07f91cc06996b2087f7cefdd8e"}, - {file = "debugpy-1.8.5-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:606bccba19f7188b6ea9579c8a4f5a5364ecd0bf5a0659c8a5d0e10dcee3032a"}, - {file = "debugpy-1.8.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db9fb642938a7a609a6c865c32ecd0d795d56c1aaa7a7a5722d77855d5e77f2b"}, - {file = "debugpy-1.8.5-cp311-cp311-win32.whl", hash = "sha256:4fbb3b39ae1aa3e5ad578f37a48a7a303dad9a3d018d369bc9ec629c1cfa7408"}, - {file = "debugpy-1.8.5-cp311-cp311-win_amd64.whl", hash = "sha256:345d6a0206e81eb68b1493ce2fbffd57c3088e2ce4b46592077a943d2b968ca3"}, - {file = "debugpy-1.8.5-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:5b5c770977c8ec6c40c60d6f58cacc7f7fe5a45960363d6974ddb9b62dbee156"}, - {file = "debugpy-1.8.5-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a65b00b7cdd2ee0c2cf4c7335fef31e15f1b7056c7fdbce9e90193e1a8c8cb"}, - {file = "debugpy-1.8.5-cp312-cp312-win32.whl", hash = "sha256:c9f7c15ea1da18d2fcc2709e9f3d6de98b69a5b0fff1807fb80bc55f906691f7"}, - {file = "debugpy-1.8.5-cp312-cp312-win_amd64.whl", hash = "sha256:28ced650c974aaf179231668a293ecd5c63c0a671ae6d56b8795ecc5d2f48d3c"}, - {file = "debugpy-1.8.5-cp38-cp38-macosx_12_0_x86_64.whl", hash = "sha256:3df6692351172a42af7558daa5019651f898fc67450bf091335aa8a18fbf6f3a"}, - {file = "debugpy-1.8.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd04a73eb2769eb0bfe43f5bfde1215c5923d6924b9b90f94d15f207a402226"}, - {file = "debugpy-1.8.5-cp38-cp38-win32.whl", hash = "sha256:8f913ee8e9fcf9d38a751f56e6de12a297ae7832749d35de26d960f14280750a"}, - {file = "debugpy-1.8.5-cp38-cp38-win_amd64.whl", hash = "sha256:a697beca97dad3780b89a7fb525d5e79f33821a8bc0c06faf1f1289e549743cf"}, - {file = "debugpy-1.8.5-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:0a1029a2869d01cb777216af8c53cda0476875ef02a2b6ff8b2f2c9a4b04176c"}, - {file = "debugpy-1.8.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e84c276489e141ed0b93b0af648eef891546143d6a48f610945416453a8ad406"}, - {file = "debugpy-1.8.5-cp39-cp39-win32.whl", hash = "sha256:ad84b7cde7fd96cf6eea34ff6c4a1b7887e0fe2ea46e099e53234856f9d99a34"}, - {file = "debugpy-1.8.5-cp39-cp39-win_amd64.whl", hash = "sha256:7b0fe36ed9d26cb6836b0a51453653f8f2e347ba7348f2bbfe76bfeb670bfb1c"}, - {file = "debugpy-1.8.5-py2.py3-none-any.whl", hash = "sha256:55919dce65b471eff25901acf82d328bbd5b833526b6c1364bd5133754777a44"}, - {file = "debugpy-1.8.5.zip", hash = "sha256:b2112cfeb34b4507399d298fe7023a16656fc553ed5246536060ca7bd0e668d0"}, + {file = "debugpy-1.8.11-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:2b26fefc4e31ff85593d68b9022e35e8925714a10ab4858fb1b577a8a48cb8cd"}, + {file = "debugpy-1.8.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61bc8b3b265e6949855300e84dc93d02d7a3a637f2aec6d382afd4ceb9120c9f"}, + {file = "debugpy-1.8.11-cp310-cp310-win32.whl", hash = "sha256:c928bbf47f65288574b78518449edaa46c82572d340e2750889bbf8cd92f3737"}, + {file = "debugpy-1.8.11-cp310-cp310-win_amd64.whl", hash = "sha256:8da1db4ca4f22583e834dcabdc7832e56fe16275253ee53ba66627b86e304da1"}, + {file = "debugpy-1.8.11-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:85de8474ad53ad546ff1c7c7c89230db215b9b8a02754d41cb5a76f70d0be296"}, + {file = "debugpy-1.8.11-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ffc382e4afa4aee367bf413f55ed17bd91b191dcaf979890af239dda435f2a1"}, + {file = "debugpy-1.8.11-cp311-cp311-win32.whl", hash = "sha256:40499a9979c55f72f4eb2fc38695419546b62594f8af194b879d2a18439c97a9"}, + {file = "debugpy-1.8.11-cp311-cp311-win_amd64.whl", hash = "sha256:987bce16e86efa86f747d5151c54e91b3c1e36acc03ce1ddb50f9d09d16ded0e"}, + {file = "debugpy-1.8.11-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:84e511a7545d11683d32cdb8f809ef63fc17ea2a00455cc62d0a4dbb4ed1c308"}, + {file = "debugpy-1.8.11-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce291a5aca4985d82875d6779f61375e959208cdf09fcec40001e65fb0a54768"}, + {file = "debugpy-1.8.11-cp312-cp312-win32.whl", hash = "sha256:28e45b3f827d3bf2592f3cf7ae63282e859f3259db44ed2b129093ca0ac7940b"}, + {file = "debugpy-1.8.11-cp312-cp312-win_amd64.whl", hash = "sha256:44b1b8e6253bceada11f714acf4309ffb98bfa9ac55e4fce14f9e5d4484287a1"}, + {file = "debugpy-1.8.11-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:8988f7163e4381b0da7696f37eec7aca19deb02e500245df68a7159739bbd0d3"}, + {file = "debugpy-1.8.11-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c1f6a173d1140e557347419767d2b14ac1c9cd847e0b4c5444c7f3144697e4e"}, + {file = "debugpy-1.8.11-cp313-cp313-win32.whl", hash = "sha256:bb3b15e25891f38da3ca0740271e63ab9db61f41d4d8541745cfc1824252cb28"}, + {file = "debugpy-1.8.11-cp313-cp313-win_amd64.whl", hash = "sha256:d8768edcbeb34da9e11bcb8b5c2e0958d25218df7a6e56adf415ef262cd7b6d1"}, + {file = "debugpy-1.8.11-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:ad7efe588c8f5cf940f40c3de0cd683cc5b76819446abaa50dc0829a30c094db"}, + {file = "debugpy-1.8.11-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:189058d03a40103a57144752652b3ab08ff02b7595d0ce1f651b9acc3a3a35a0"}, + {file = "debugpy-1.8.11-cp38-cp38-win32.whl", hash = "sha256:32db46ba45849daed7ccf3f2e26f7a386867b077f39b2a974bb5c4c2c3b0a280"}, + {file = "debugpy-1.8.11-cp38-cp38-win_amd64.whl", hash = "sha256:116bf8342062246ca749013df4f6ea106f23bc159305843491f64672a55af2e5"}, + {file = "debugpy-1.8.11-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:654130ca6ad5de73d978057eaf9e582244ff72d4574b3e106fb8d3d2a0d32458"}, + {file = "debugpy-1.8.11-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23dc34c5e03b0212fa3c49a874df2b8b1b8fda95160bd79c01eb3ab51ea8d851"}, + {file = "debugpy-1.8.11-cp39-cp39-win32.whl", hash = "sha256:52d8a3166c9f2815bfae05f386114b0b2d274456980d41f320299a8d9a5615a7"}, + {file = "debugpy-1.8.11-cp39-cp39-win_amd64.whl", hash = "sha256:52c3cf9ecda273a19cc092961ee34eb9ba8687d67ba34cc7b79a521c1c64c4c0"}, + {file = "debugpy-1.8.11-py2.py3-none-any.whl", hash = "sha256:0e22f846f4211383e6a416d04b4c13ed174d24cc5d43f5fd52e7821d0ebc8920"}, + {file = "debugpy-1.8.11.tar.gz", hash = "sha256:6ad2688b69235c43b020e04fecccdf6a96c8943ca9c2fb340b8adc103c655e57"}, ] [[package]] @@ -764,13 +780,13 @@ files = [ [[package]] name = "dill" -version = "0.3.8" +version = "0.3.9" description = "serialize all of Python" optional = false python-versions = ">=3.8" files = [ - {file = "dill-0.3.8-py3-none-any.whl", hash = "sha256:c36ca9ffb54365bdd2f8eb3eff7d2a21237f8452b57ace88b1ac615b7e815bd7"}, - {file = "dill-0.3.8.tar.gz", hash = "sha256:3ebe3c479ad625c4553aca177444d89b486b1d84982eeacded644afc0cf797ca"}, + {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, + {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, ] [package.extras] @@ -833,13 +849,13 @@ text-unidecode = "1.3" [[package]] name = "fastjsonschema" -version = "2.20.0" +version = "2.21.1" description = "Fastest Python implementation of JSON schema" optional = false python-versions = "*" files = [ - {file = "fastjsonschema-2.20.0-py3-none-any.whl", hash = "sha256:5875f0b0fa7a0043a91e93a9b8f793bcbbba9691e7fd83dca95c28ba26d21f0a"}, - {file = "fastjsonschema-2.20.0.tar.gz", hash = "sha256:3d48fc5300ee96f5d116f10fe6f28d938e6008f59a6a025c2649475b87f76a23"}, + {file = "fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667"}, + {file = "fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4"}, ] [package.extras] @@ -847,53 +863,61 @@ devel = ["colorama", "json-spec", "jsonschema", "pylint", "pytest", "pytest-benc [[package]] name = "fonttools" -version = "4.53.1" +version = "4.55.3" description = "Tools to manipulate font files" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.53.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0679a30b59d74b6242909945429dbddb08496935b82f91ea9bf6ad240ec23397"}, - {file = "fonttools-4.53.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8bf06b94694251861ba7fdeea15c8ec0967f84c3d4143ae9daf42bbc7717fe3"}, - {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b96cd370a61f4d083c9c0053bf634279b094308d52fdc2dd9a22d8372fdd590d"}, - {file = "fonttools-4.53.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1c7c5aa18dd3b17995898b4a9b5929d69ef6ae2af5b96d585ff4005033d82f0"}, - {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e013aae589c1c12505da64a7d8d023e584987e51e62006e1bb30d72f26522c41"}, - {file = "fonttools-4.53.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9efd176f874cb6402e607e4cc9b4a9cd584d82fc34a4b0c811970b32ba62501f"}, - {file = "fonttools-4.53.1-cp310-cp310-win32.whl", hash = "sha256:c8696544c964500aa9439efb6761947393b70b17ef4e82d73277413f291260a4"}, - {file = "fonttools-4.53.1-cp310-cp310-win_amd64.whl", hash = "sha256:8959a59de5af6d2bec27489e98ef25a397cfa1774b375d5787509c06659b3671"}, - {file = "fonttools-4.53.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da33440b1413bad53a8674393c5d29ce64d8c1a15ef8a77c642ffd900d07bfe1"}, - {file = "fonttools-4.53.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ff7e5e9bad94e3a70c5cd2fa27f20b9bb9385e10cddab567b85ce5d306ea923"}, - {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6e7170d675d12eac12ad1a981d90f118c06cf680b42a2d74c6c931e54b50719"}, - {file = "fonttools-4.53.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee32ea8765e859670c4447b0817514ca79054463b6b79784b08a8df3a4d78e3"}, - {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6e08f572625a1ee682115223eabebc4c6a2035a6917eac6f60350aba297ccadb"}, - {file = "fonttools-4.53.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b21952c092ffd827504de7e66b62aba26fdb5f9d1e435c52477e6486e9d128b2"}, - {file = "fonttools-4.53.1-cp311-cp311-win32.whl", hash = "sha256:9dfdae43b7996af46ff9da520998a32b105c7f098aeea06b2226b30e74fbba88"}, - {file = "fonttools-4.53.1-cp311-cp311-win_amd64.whl", hash = "sha256:d4d0096cb1ac7a77b3b41cd78c9b6bc4a400550e21dc7a92f2b5ab53ed74eb02"}, - {file = "fonttools-4.53.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:d92d3c2a1b39631a6131c2fa25b5406855f97969b068e7e08413325bc0afba58"}, - {file = "fonttools-4.53.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3b3c8ebafbee8d9002bd8f1195d09ed2bd9ff134ddec37ee8f6a6375e6a4f0e8"}, - {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32f029c095ad66c425b0ee85553d0dc326d45d7059dbc227330fc29b43e8ba60"}, - {file = "fonttools-4.53.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f5e6c3510b79ea27bb1ebfcc67048cde9ec67afa87c7dd7efa5c700491ac7f"}, - {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f677ce218976496a587ab17140da141557beb91d2a5c1a14212c994093f2eae2"}, - {file = "fonttools-4.53.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9e6ceba2a01b448e36754983d376064730690401da1dd104ddb543519470a15f"}, - {file = "fonttools-4.53.1-cp312-cp312-win32.whl", hash = "sha256:791b31ebbc05197d7aa096bbc7bd76d591f05905d2fd908bf103af4488e60670"}, - {file = "fonttools-4.53.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ed170b5e17da0264b9f6fae86073be3db15fa1bd74061c8331022bca6d09bab"}, - {file = "fonttools-4.53.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c818c058404eb2bba05e728d38049438afd649e3c409796723dfc17cd3f08749"}, - {file = "fonttools-4.53.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:651390c3b26b0c7d1f4407cad281ee7a5a85a31a110cbac5269de72a51551ba2"}, - {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e54f1bba2f655924c1138bbc7fa91abd61f45c68bd65ab5ed985942712864bbb"}, - {file = "fonttools-4.53.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9cd19cf4fe0595ebdd1d4915882b9440c3a6d30b008f3cc7587c1da7b95be5f"}, - {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:2af40ae9cdcb204fc1d8f26b190aa16534fcd4f0df756268df674a270eab575d"}, - {file = "fonttools-4.53.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:35250099b0cfb32d799fb5d6c651220a642fe2e3c7d2560490e6f1d3f9ae9169"}, - {file = "fonttools-4.53.1-cp38-cp38-win32.whl", hash = "sha256:f08df60fbd8d289152079a65da4e66a447efc1d5d5a4d3f299cdd39e3b2e4a7d"}, - {file = "fonttools-4.53.1-cp38-cp38-win_amd64.whl", hash = "sha256:7b6b35e52ddc8fb0db562133894e6ef5b4e54e1283dff606fda3eed938c36fc8"}, - {file = "fonttools-4.53.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:75a157d8d26c06e64ace9df037ee93a4938a4606a38cb7ffaf6635e60e253b7a"}, - {file = "fonttools-4.53.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4824c198f714ab5559c5be10fd1adf876712aa7989882a4ec887bf1ef3e00e31"}, - {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:becc5d7cb89c7b7afa8321b6bb3dbee0eec2b57855c90b3e9bf5fb816671fa7c"}, - {file = "fonttools-4.53.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84ec3fb43befb54be490147b4a922b5314e16372a643004f182babee9f9c3407"}, - {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:73379d3ffdeecb376640cd8ed03e9d2d0e568c9d1a4e9b16504a834ebadc2dfb"}, - {file = "fonttools-4.53.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:02569e9a810f9d11f4ae82c391ebc6fb5730d95a0657d24d754ed7763fb2d122"}, - {file = "fonttools-4.53.1-cp39-cp39-win32.whl", hash = "sha256:aae7bd54187e8bf7fd69f8ab87b2885253d3575163ad4d669a262fe97f0136cb"}, - {file = "fonttools-4.53.1-cp39-cp39-win_amd64.whl", hash = "sha256:e5b708073ea3d684235648786f5f6153a48dc8762cdfe5563c57e80787c29fbb"}, - {file = "fonttools-4.53.1-py3-none-any.whl", hash = "sha256:f1f8758a2ad110bd6432203a344269f445a2907dc24ef6bccfd0ac4e14e0d71d"}, - {file = "fonttools-4.53.1.tar.gz", hash = "sha256:e128778a8e9bc11159ce5447f76766cefbd876f44bd79aff030287254e4752c4"}, + {file = "fonttools-4.55.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1dcc07934a2165ccdc3a5a608db56fb3c24b609658a5b340aee4ecf3ba679dc0"}, + {file = "fonttools-4.55.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f7d66c15ba875432a2d2fb419523f5d3d347f91f48f57b8b08a2dfc3c39b8a3f"}, + {file = "fonttools-4.55.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27e4ae3592e62eba83cd2c4ccd9462dcfa603ff78e09110680a5444c6925d841"}, + {file = "fonttools-4.55.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62d65a3022c35e404d19ca14f291c89cc5890032ff04f6c17af0bd1927299674"}, + {file = "fonttools-4.55.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d342e88764fb201286d185093781bf6628bbe380a913c24adf772d901baa8276"}, + {file = "fonttools-4.55.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dd68c87a2bfe37c5b33bcda0fba39b65a353876d3b9006fde3adae31f97b3ef5"}, + {file = "fonttools-4.55.3-cp310-cp310-win32.whl", hash = "sha256:1bc7ad24ff98846282eef1cbeac05d013c2154f977a79886bb943015d2b1b261"}, + {file = "fonttools-4.55.3-cp310-cp310-win_amd64.whl", hash = "sha256:b54baf65c52952db65df39fcd4820668d0ef4766c0ccdf32879b77f7c804d5c5"}, + {file = "fonttools-4.55.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8c4491699bad88efe95772543cd49870cf756b019ad56294f6498982408ab03e"}, + {file = "fonttools-4.55.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5323a22eabddf4b24f66d26894f1229261021dacd9d29e89f7872dd8c63f0b8b"}, + {file = "fonttools-4.55.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5480673f599ad410695ca2ddef2dfefe9df779a9a5cda89503881e503c9c7d90"}, + {file = "fonttools-4.55.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da9da6d65cd7aa6b0f806556f4985bcbf603bf0c5c590e61b43aa3e5a0f822d0"}, + {file = "fonttools-4.55.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e894b5bd60d9f473bed7a8f506515549cc194de08064d829464088d23097331b"}, + {file = "fonttools-4.55.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aee3b57643827e237ff6ec6d28d9ff9766bd8b21e08cd13bff479e13d4b14765"}, + {file = "fonttools-4.55.3-cp311-cp311-win32.whl", hash = "sha256:eb6ca911c4c17eb51853143624d8dc87cdcdf12a711fc38bf5bd21521e79715f"}, + {file = "fonttools-4.55.3-cp311-cp311-win_amd64.whl", hash = "sha256:6314bf82c54c53c71805318fcf6786d986461622dd926d92a465199ff54b1b72"}, + {file = "fonttools-4.55.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9e736f60f4911061235603a6119e72053073a12c6d7904011df2d8fad2c0e35"}, + {file = "fonttools-4.55.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a8aa2c5e5b8b3bcb2e4538d929f6589a5c6bdb84fd16e2ed92649fb5454f11c"}, + {file = "fonttools-4.55.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:07f8288aacf0a38d174445fc78377a97fb0b83cfe352a90c9d9c1400571963c7"}, + {file = "fonttools-4.55.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8d5e8916c0970fbc0f6f1bece0063363bb5857a7f170121a4493e31c3db3314"}, + {file = "fonttools-4.55.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae3b6600565b2d80b7c05acb8e24d2b26ac407b27a3f2e078229721ba5698427"}, + {file = "fonttools-4.55.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:54153c49913f45065c8d9e6d0c101396725c5621c8aee744719300f79771d75a"}, + {file = "fonttools-4.55.3-cp312-cp312-win32.whl", hash = "sha256:827e95fdbbd3e51f8b459af5ea10ecb4e30af50221ca103bea68218e9615de07"}, + {file = "fonttools-4.55.3-cp312-cp312-win_amd64.whl", hash = "sha256:e6e8766eeeb2de759e862004aa11a9ea3d6f6d5ec710551a88b476192b64fd54"}, + {file = "fonttools-4.55.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a430178ad3e650e695167cb53242dae3477b35c95bef6525b074d87493c4bf29"}, + {file = "fonttools-4.55.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:529cef2ce91dc44f8e407cc567fae6e49a1786f2fefefa73a294704c415322a4"}, + {file = "fonttools-4.55.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e75f12c82127486fac2d8bfbf5bf058202f54bf4f158d367e41647b972342ca"}, + {file = "fonttools-4.55.3-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:859c358ebf41db18fb72342d3080bce67c02b39e86b9fbcf1610cca14984841b"}, + {file = "fonttools-4.55.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:546565028e244a701f73df6d8dd6be489d01617863ec0c6a42fa25bf45d43048"}, + {file = "fonttools-4.55.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aca318b77f23523309eec4475d1fbbb00a6b133eb766a8bdc401faba91261abe"}, + {file = "fonttools-4.55.3-cp313-cp313-win32.whl", hash = "sha256:8c5ec45428edaa7022f1c949a632a6f298edc7b481312fc7dc258921e9399628"}, + {file = "fonttools-4.55.3-cp313-cp313-win_amd64.whl", hash = "sha256:11e5de1ee0d95af4ae23c1a138b184b7f06e0b6abacabf1d0db41c90b03d834b"}, + {file = "fonttools-4.55.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:caf8230f3e10f8f5d7593eb6d252a37caf58c480b19a17e250a63dad63834cf3"}, + {file = "fonttools-4.55.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b586ab5b15b6097f2fb71cafa3c98edfd0dba1ad8027229e7b1e204a58b0e09d"}, + {file = "fonttools-4.55.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8c2794ded89399cc2169c4d0bf7941247b8d5932b2659e09834adfbb01589aa"}, + {file = "fonttools-4.55.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf4fe7c124aa3f4e4c1940880156e13f2f4d98170d35c749e6b4f119a872551e"}, + {file = "fonttools-4.55.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:86721fbc389ef5cc1e2f477019e5069e8e4421e8d9576e9c26f840dbb04678de"}, + {file = "fonttools-4.55.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:89bdc5d88bdeec1b15af790810e267e8332d92561dce4f0748c2b95c9bdf3926"}, + {file = "fonttools-4.55.3-cp38-cp38-win32.whl", hash = "sha256:bc5dbb4685e51235ef487e4bd501ddfc49be5aede5e40f4cefcccabc6e60fb4b"}, + {file = "fonttools-4.55.3-cp38-cp38-win_amd64.whl", hash = "sha256:cd70de1a52a8ee2d1877b6293af8a2484ac82514f10b1c67c1c5762d38073e56"}, + {file = "fonttools-4.55.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bdcc9f04b36c6c20978d3f060e5323a43f6222accc4e7fcbef3f428e216d96af"}, + {file = "fonttools-4.55.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c3ca99e0d460eff46e033cd3992a969658c3169ffcd533e0a39c63a38beb6831"}, + {file = "fonttools-4.55.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22f38464daa6cdb7b6aebd14ab06609328fe1e9705bb0fcc7d1e69de7109ee02"}, + {file = "fonttools-4.55.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed63959d00b61959b035c7d47f9313c2c1ece090ff63afea702fe86de00dbed4"}, + {file = "fonttools-4.55.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5e8d657cd7326eeaba27de2740e847c6b39dde2f8d7cd7cc56f6aad404ddf0bd"}, + {file = "fonttools-4.55.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:fb594b5a99943042c702c550d5494bdd7577f6ef19b0bc73877c948a63184a32"}, + {file = "fonttools-4.55.3-cp39-cp39-win32.whl", hash = "sha256:dc5294a3d5c84226e3dbba1b6f61d7ad813a8c0238fceea4e09aa04848c3d851"}, + {file = "fonttools-4.55.3-cp39-cp39-win_amd64.whl", hash = "sha256:aedbeb1db64496d098e6be92b2e63b5fac4e53b1b92032dfc6988e1ea9134a4d"}, + {file = "fonttools-4.55.3-py3-none-any.whl", hash = "sha256:f412604ccbeee81b091b420272841e5ec5ef68967a9790e80bffd0e30b8e2977"}, + {file = "fonttools-4.55.3.tar.gz", hash = "sha256:3983313c2a04d6cc1fe9251f8fc647754cf49a61dac6cb1e7249ae67afaafc45"}, ] [package.extras] @@ -967,13 +991,13 @@ files = [ [[package]] name = "httpcore" -version = "1.0.5" +version = "1.0.7" description = "A minimal low-level HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, - {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, + {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, + {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, ] [package.dependencies] @@ -984,17 +1008,17 @@ h11 = ">=0.13,<0.15" asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<0.26.0)"] +trio = ["trio (>=0.22.0,<1.0)"] [[package]] name = "httpx" -version = "0.27.2" +version = "0.28.1" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, - {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, ] [package.dependencies] @@ -1002,7 +1026,6 @@ anyio = "*" certifi = "*" httpcore = "==1.*" idna = "*" -sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] @@ -1013,15 +1036,18 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "idna" -version = "3.8" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" files = [ - {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, - {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "imagesize" version = "1.4.1" @@ -1212,22 +1238,22 @@ colors = ["colorama (>=0.4.6)"] [[package]] name = "jedi" -version = "0.19.1" +version = "0.19.2" description = "An autocompletion tool for Python that can be used for text editors." optional = false python-versions = ">=3.6" files = [ - {file = "jedi-0.19.1-py2.py3-none-any.whl", hash = "sha256:e983c654fe5c02867aef4cdfce5a2fbb4a50adc0af145f70504238f18ef5e7e0"}, - {file = "jedi-0.19.1.tar.gz", hash = "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd"}, + {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, + {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, ] [package.dependencies] -parso = ">=0.8.3,<0.9.0" +parso = ">=0.8.4,<0.9.0" [package.extras] docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] -testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] [[package]] name = "jinja2" @@ -1259,15 +1285,18 @@ files = [ [[package]] name = "json5" -version = "0.9.25" +version = "0.10.0" description = "A Python implementation of the JSON5 data format." optional = false -python-versions = ">=3.8" +python-versions = ">=3.8.0" files = [ - {file = "json5-0.9.25-py3-none-any.whl", hash = "sha256:34ed7d834b1341a86987ed52f3f76cd8ee184394906b6e22a1e0deb9ab294e8f"}, - {file = "json5-0.9.25.tar.gz", hash = "sha256:548e41b9be043f9426776f05df8635a00fe06104ea51ed24b67f908856e151ae"}, + {file = "json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa"}, + {file = "json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559"}, ] +[package.extras] +dev = ["build (==1.2.2.post1)", "coverage (==7.5.3)", "mypy (==1.13.0)", "pip (==24.3.1)", "pylint (==3.2.3)", "ruff (==0.7.3)", "twine (==5.1.1)", "uv (==0.5.1)"] + [[package]] name = "jsonpointer" version = "3.0.0" @@ -1346,13 +1375,13 @@ notebook = "*" [[package]] name = "jupyter-client" -version = "8.6.2" +version = "8.6.3" description = "Jupyter protocol implementation and client libraries" optional = false python-versions = ">=3.8" files = [ - {file = "jupyter_client-8.6.2-py3-none-any.whl", hash = "sha256:50cbc5c66fd1b8f65ecb66bc490ab73217993632809b6e505687de18e9dea39f"}, - {file = "jupyter_client-8.6.2.tar.gz", hash = "sha256:2bda14d55ee5ba58552a8c53ae43d215ad9868853489213f37da060ced54d8df"}, + {file = "jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f"}, + {file = "jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419"}, ] [package.dependencies] @@ -1508,13 +1537,13 @@ test = ["jupyter-server (>=2.0.0)", "pytest (>=7.0)", "pytest-jupyter[server] (> [[package]] name = "jupyterlab" -version = "4.2.5" +version = "4.3.3" description = "JupyterLab computational environment" optional = false python-versions = ">=3.8" files = [ - {file = "jupyterlab-4.2.5-py3-none-any.whl", hash = "sha256:73b6e0775d41a9fee7ee756c80f58a6bed4040869ccc21411dc559818874d321"}, - {file = "jupyterlab-4.2.5.tar.gz", hash = "sha256:ae7f3a1b8cb88b4f55009ce79fa7c06f99d70cd63601ee4aa91815d054f46f75"}, + {file = "jupyterlab-4.3.3-py3-none-any.whl", hash = "sha256:32a8fd30677e734ffcc3916a4758b9dab21b02015b668c60eb36f84357b7d4b1"}, + {file = "jupyterlab-4.3.3.tar.gz", hash = "sha256:76fa39e548fdac94dc1204af5956c556f54c785f70ee26aa47ea08eda4d5bbcd"}, ] [package.dependencies] @@ -1530,15 +1559,15 @@ jupyter-server = ">=2.4.0,<3" jupyterlab-server = ">=2.27.1,<3" notebook-shim = ">=0.2" packaging = "*" -setuptools = ">=40.1.0" +setuptools = ">=40.8.0" tomli = {version = ">=1.2.2", markers = "python_version < \"3.11\""} tornado = ">=6.2.0" traitlets = "*" [package.extras] -dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.3.5)"] -docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<7.3.0)", "sphinx-copybutton"] -docs-screenshots = ["altair (==5.3.0)", "ipython (==8.16.1)", "ipywidgets (==8.1.2)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.1.post2)", "matplotlib (==3.8.3)", "nbconvert (>=7.0.0)", "pandas (==2.2.1)", "scipy (==1.12.0)", "vega-datasets (==0.9.0)"] +dev = ["build", "bump2version", "coverage", "hatch", "pre-commit", "pytest-cov", "ruff (==0.6.9)"] +docs = ["jsx-lexer", "myst-parser", "pydata-sphinx-theme (>=0.13.0)", "pytest", "pytest-check-links", "pytest-jupyter", "sphinx (>=1.8,<8.1.0)", "sphinx-copybutton"] +docs-screenshots = ["altair (==5.4.1)", "ipython (==8.16.1)", "ipywidgets (==8.1.5)", "jupyterlab-geojson (==3.4.0)", "jupyterlab-language-pack-zh-cn (==4.2.post3)", "matplotlib (==3.9.2)", "nbconvert (>=7.0.0)", "pandas (==2.2.3)", "scipy (==1.14.1)", "vega-datasets (==0.9.0)"] test = ["coverage", "pytest (>=7.0)", "pytest-check-links (>=0.7)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter (>=0.5.3)", "pytest-timeout", "pytest-tornasync", "requests", "requests-cache", "virtualenv"] upgrade-extension = ["copier (>=9,<10)", "jinja2-time (<0.3)", "pydantic (<3.0)", "pyyaml-include (<3.0)", "tomli-w (<2.0)"] @@ -1713,6 +1742,27 @@ files = [ {file = "kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60"}, ] +[[package]] +name = "lightning-utilities" +version = "0.11.9" +description = "Lightning toolbox for across the our ecosystem." +optional = false +python-versions = ">=3.8" +files = [ + {file = "lightning_utilities-0.11.9-py3-none-any.whl", hash = "sha256:ac6d4e9e28faf3ff4be997876750fee10dc604753dbc429bf3848a95c5d7e0d2"}, + {file = "lightning_utilities-0.11.9.tar.gz", hash = "sha256:f5052b81344cc2684aa9afd74b7ce8819a8f49a858184ec04548a5a109dfd053"}, +] + +[package.dependencies] +packaging = ">=17.1" +setuptools = "*" +typing-extensions = "*" + +[package.extras] +cli = ["fire"] +docs = ["requests (>=2.0.0)"] +typing = ["mypy (>=1.0.0)", "types-setuptools"] + [[package]] name = "llvmlite" version = "0.41.1" @@ -1921,13 +1971,13 @@ files = [ [[package]] name = "nbclient" -version = "0.10.0" +version = "0.10.1" description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." optional = false python-versions = ">=3.8.0" files = [ - {file = "nbclient-0.10.0-py3-none-any.whl", hash = "sha256:f13e3529332a1f1f81d82a53210322476a168bb7090a0289c795fe9cc11c9d3f"}, - {file = "nbclient-0.10.0.tar.gz", hash = "sha256:4b3f1b7dba531e498449c4db4f53da339c91d449dc11e9af3a43b4eb5c5abb09"}, + {file = "nbclient-0.10.1-py3-none-any.whl", hash = "sha256:949019b9240d66897e442888cfb618f69ef23dc71c01cb5fced8499c2cfc084d"}, + {file = "nbclient-0.10.1.tar.gz", hash = "sha256:3e93e348ab27e712acd46fccd809139e356eb9a31aab641d1a7991a6eb4e6f68"}, ] [package.dependencies] @@ -1938,7 +1988,7 @@ traitlets = ">=5.4" [package.extras] dev = ["pre-commit"] -docs = ["autodoc-traits", "mock", "moto", "myst-parser", "nbclient[test]", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling"] +docs = ["autodoc-traits", "flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "mock", "moto", "myst-parser", "nbconvert (>=7.0.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "sphinx (>=1.7)", "sphinx-book-theme", "sphinxcontrib-spelling", "testpath", "xmltodict"] test = ["flaky", "ipykernel (>=6.19.3)", "ipython", "ipywidgets", "nbconvert (>=7.0.0)", "pytest (>=7.0,<8)", "pytest-asyncio", "pytest-cov (>=4.0)", "testpath", "xmltodict"] [[package]] @@ -2013,18 +2063,18 @@ files = [ [[package]] name = "notebook" -version = "7.2.2" +version = "7.3.1" description = "Jupyter Notebook - A web-based notebook environment for interactive computing" optional = false python-versions = ">=3.8" files = [ - {file = "notebook-7.2.2-py3-none-any.whl", hash = "sha256:c89264081f671bc02eec0ed470a627ed791b9156cad9285226b31611d3e9fe1c"}, - {file = "notebook-7.2.2.tar.gz", hash = "sha256:2ef07d4220421623ad3fe88118d687bc0450055570cdd160814a59cf3a1c516e"}, + {file = "notebook-7.3.1-py3-none-any.whl", hash = "sha256:212e1486b2230fe22279043f33c7db5cf9a01d29feb063a85cb139747b7c9483"}, + {file = "notebook-7.3.1.tar.gz", hash = "sha256:84381c2a82d867517fd25b86e986dae1fe113a70b98f03edff9b94e499fec8fa"}, ] [package.dependencies] jupyter-server = ">=2.4.0,<3" -jupyterlab = ">=4.2.0,<4.3" +jupyterlab = ">=4.3.2,<4.4" jupyterlab-server = ">=2.27.1,<3" notebook-shim = ">=0.2,<0.3" tornado = ">=6.2.0" @@ -2123,67 +2173,6 @@ files = [ {file = "numpy-1.24.4.tar.gz", hash = "sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463"}, ] -[[package]] -name = "nvidia-cublas-cu11" -version = "11.10.3.66" -description = "CUBLAS native runtime libraries" -optional = false -python-versions = ">=3" -files = [ - {file = "nvidia_cublas_cu11-11.10.3.66-py3-none-manylinux1_x86_64.whl", hash = "sha256:d32e4d75f94ddfb93ea0a5dda08389bcc65d8916a25cb9f37ac89edaeed3bded"}, - {file = "nvidia_cublas_cu11-11.10.3.66-py3-none-win_amd64.whl", hash = "sha256:8ac17ba6ade3ed56ab898a036f9ae0756f1e81052a317bf98f8c6d18dc3ae49e"}, -] - -[package.dependencies] -setuptools = "*" -wheel = "*" - -[[package]] -name = "nvidia-cuda-nvrtc-cu11" -version = "11.7.99" -description = "NVRTC native runtime libraries" -optional = false -python-versions = ">=3" -files = [ - {file = "nvidia_cuda_nvrtc_cu11-11.7.99-2-py3-none-manylinux1_x86_64.whl", hash = "sha256:9f1562822ea264b7e34ed5930567e89242d266448e936b85bc97a3370feabb03"}, - {file = "nvidia_cuda_nvrtc_cu11-11.7.99-py3-none-manylinux1_x86_64.whl", hash = "sha256:f7d9610d9b7c331fa0da2d1b2858a4a8315e6d49765091d28711c8946e7425e7"}, - {file = "nvidia_cuda_nvrtc_cu11-11.7.99-py3-none-win_amd64.whl", hash = "sha256:f2effeb1309bdd1b3854fc9b17eaf997808f8b25968ce0c7070945c4265d64a3"}, -] - -[package.dependencies] -setuptools = "*" -wheel = "*" - -[[package]] -name = "nvidia-cuda-runtime-cu11" -version = "11.7.99" -description = "CUDA Runtime native Libraries" -optional = false -python-versions = ">=3" -files = [ - {file = "nvidia_cuda_runtime_cu11-11.7.99-py3-none-manylinux1_x86_64.whl", hash = "sha256:cc768314ae58d2641f07eac350f40f99dcb35719c4faff4bc458a7cd2b119e31"}, - {file = "nvidia_cuda_runtime_cu11-11.7.99-py3-none-win_amd64.whl", hash = "sha256:bc77fa59a7679310df9d5c70ab13c4e34c64ae2124dd1efd7e5474b71be125c7"}, -] - -[package.dependencies] -setuptools = "*" -wheel = "*" - -[[package]] -name = "nvidia-cudnn-cu11" -version = "8.5.0.96" -description = "cuDNN runtime libraries" -optional = false -python-versions = ">=3" -files = [ - {file = "nvidia_cudnn_cu11-8.5.0.96-2-py3-none-manylinux1_x86_64.whl", hash = "sha256:402f40adfc6f418f9dae9ab402e773cfed9beae52333f6d86ae3107a1b9527e7"}, - {file = "nvidia_cudnn_cu11-8.5.0.96-py3-none-manylinux1_x86_64.whl", hash = "sha256:71f8111eb830879ff2836db3cccf03bbd735df9b0d17cd93761732ac50a8a108"}, -] - -[package.dependencies] -setuptools = "*" -wheel = "*" - [[package]] name = "overrides" version = "7.7.0" @@ -2414,13 +2403,13 @@ files = [ [[package]] name = "platformdirs" -version = "4.3.2" +version = "4.3.6" 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.3.2-py3-none-any.whl", hash = "sha256:eb1c8582560b34ed4ba105009a4badf7f6f85768b30126f351328507b2beb617"}, - {file = "platformdirs-4.3.2.tar.gz", hash = "sha256:9e5e27a08aa095dd127b9f2e764d74254f482fef22b0970773bfba79d091ab8c"}, + {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [package.extras] @@ -2445,13 +2434,13 @@ testing = ["pytest", "pytest-benchmark"] [[package]] name = "prometheus-client" -version = "0.20.0" +version = "0.21.1" description = "Python client for the Prometheus monitoring system." optional = false python-versions = ">=3.8" files = [ - {file = "prometheus_client-0.20.0-py3-none-any.whl", hash = "sha256:cde524a85bce83ca359cc837f28b8c0db5cac7aa653a588fd7e84ba061c329e7"}, - {file = "prometheus_client-0.20.0.tar.gz", hash = "sha256:287629d00b147a32dcb2be0b9df905da599b2d82f80377083ec8463309a4bb89"}, + {file = "prometheus_client-0.21.1-py3-none-any.whl", hash = "sha256:594b45c410d6f4f8888940fe80b5cc2521b305a1fafe1c58609ef715a001f301"}, + {file = "prometheus_client-0.21.1.tar.gz", hash = "sha256:252505a722ac04b0456be05c05f75f45d760c2911ffc45f2a06bcaed9f3ae3fb"}, ] [package.extras] @@ -2459,13 +2448,13 @@ twisted = ["twisted"] [[package]] name = "prompt-toolkit" -version = "3.0.47" +version = "3.0.48" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.47-py3-none-any.whl", hash = "sha256:0d7bfa67001d5e39d02c224b663abc33687405033a8c422d0d675a5a13361d10"}, - {file = "prompt_toolkit-3.0.47.tar.gz", hash = "sha256:1e1b29cb58080b1e69f207c893a1a7bf16d127a5c30c9d17a25a5d77792e5360"}, + {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, + {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, ] [package.dependencies] @@ -2664,12 +2653,12 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pyspark" -version = "3.5.2" +version = "3.5.3" description = "Apache Spark Python API" optional = false python-versions = ">=3.8" files = [ - {file = "pyspark-3.5.2.tar.gz", hash = "sha256:bbb36eba09fa24e86e0923d7e7a986041b90c714e11c6aa976f9791fe9edde5e"}, + {file = "pyspark-3.5.3.tar.gz", hash = "sha256:68b7cc0c0c570a7d8644f49f40d2da8709b01d30c9126cc8cf93b4f84f3d9747"}, ] [package.dependencies] @@ -2684,13 +2673,13 @@ sql = ["numpy (>=1.15,<2)", "pandas (>=1.0.5)", "pyarrow (>=4.0.0)"] [[package]] name = "pytest" -version = "8.3.3" +version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, - {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] [package.dependencies] @@ -2738,15 +2727,21 @@ six = ">=1.5" [[package]] name = "python-json-logger" -version = "2.0.7" -description = "A python library adding a json log formatter" +version = "3.2.1" +description = "JSON Log Formatter for the Python Logging Package" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "python-json-logger-2.0.7.tar.gz", hash = "sha256:23e7ec02d34237c5aa1e29a070193a4ea87583bb4e7f8fd06d3de8264c4b2e1c"}, - {file = "python_json_logger-2.0.7-py3-none-any.whl", hash = "sha256:f380b826a991ebbe3de4d897aeec42760035ac760345e57b812938dc8b35e2bd"}, + {file = "python_json_logger-3.2.1-py3-none-any.whl", hash = "sha256:cdc17047eb5374bd311e748b42f99d71223f3b0e186f4206cc5d52aefe85b090"}, + {file = "python_json_logger-3.2.1.tar.gz", hash = "sha256:8eb0554ea17cb75b05d2848bc14fb02fbdbd9d6972120781b974380bfa162008"}, ] +[package.dependencies] +typing_extensions = {version = "*", markers = "python_version < \"3.10\""} + +[package.extras] +dev = ["backports.zoneinfo", "black", "build", "freezegun", "mdx_truly_sane_lists", "mike", "mkdocs", "mkdocs-awesome-pages-plugin", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-material (>=8.5)", "mkdocstrings[python]", "msgspec", "msgspec-python313-pre", "mypy", "orjson", "pylint", "pytest", "tzdata", "validate-pyproject[all]"] + [[package]] name = "pyts" version = "0.12.0" @@ -2782,40 +2777,44 @@ files = [ [[package]] name = "pywin32" -version = "306" +version = "308" description = "Python for Window Extensions" optional = false python-versions = "*" files = [ - {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, - {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, - {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, - {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, - {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, - {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, - {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, - {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, - {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, - {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, - {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, - {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, - {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, - {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, + {file = "pywin32-308-cp310-cp310-win32.whl", hash = "sha256:796ff4426437896550d2981b9c2ac0ffd75238ad9ea2d3bfa67a1abd546d262e"}, + {file = "pywin32-308-cp310-cp310-win_amd64.whl", hash = "sha256:4fc888c59b3c0bef905ce7eb7e2106a07712015ea1c8234b703a088d46110e8e"}, + {file = "pywin32-308-cp310-cp310-win_arm64.whl", hash = "sha256:a5ab5381813b40f264fa3495b98af850098f814a25a63589a8e9eb12560f450c"}, + {file = "pywin32-308-cp311-cp311-win32.whl", hash = "sha256:5d8c8015b24a7d6855b1550d8e660d8daa09983c80e5daf89a273e5c6fb5095a"}, + {file = "pywin32-308-cp311-cp311-win_amd64.whl", hash = "sha256:575621b90f0dc2695fec346b2d6302faebd4f0f45c05ea29404cefe35d89442b"}, + {file = "pywin32-308-cp311-cp311-win_arm64.whl", hash = "sha256:100a5442b7332070983c4cd03f2e906a5648a5104b8a7f50175f7906efd16bb6"}, + {file = "pywin32-308-cp312-cp312-win32.whl", hash = "sha256:587f3e19696f4bf96fde9d8a57cec74a57021ad5f204c9e627e15c33ff568897"}, + {file = "pywin32-308-cp312-cp312-win_amd64.whl", hash = "sha256:00b3e11ef09ede56c6a43c71f2d31857cf7c54b0ab6e78ac659497abd2834f47"}, + {file = "pywin32-308-cp312-cp312-win_arm64.whl", hash = "sha256:9b4de86c8d909aed15b7011182c8cab38c8850de36e6afb1f0db22b8959e3091"}, + {file = "pywin32-308-cp313-cp313-win32.whl", hash = "sha256:1c44539a37a5b7b21d02ab34e6a4d314e0788f1690d65b48e9b0b89f31abbbed"}, + {file = "pywin32-308-cp313-cp313-win_amd64.whl", hash = "sha256:fd380990e792eaf6827fcb7e187b2b4b1cede0585e3d0c9e84201ec27b9905e4"}, + {file = "pywin32-308-cp313-cp313-win_arm64.whl", hash = "sha256:ef313c46d4c18dfb82a2431e3051ac8f112ccee1a34f29c263c583c568db63cd"}, + {file = "pywin32-308-cp37-cp37m-win32.whl", hash = "sha256:1f696ab352a2ddd63bd07430080dd598e6369152ea13a25ebcdd2f503a38f1ff"}, + {file = "pywin32-308-cp37-cp37m-win_amd64.whl", hash = "sha256:13dcb914ed4347019fbec6697a01a0aec61019c1046c2b905410d197856326a6"}, + {file = "pywin32-308-cp38-cp38-win32.whl", hash = "sha256:5794e764ebcabf4ff08c555b31bd348c9025929371763b2183172ff4708152f0"}, + {file = "pywin32-308-cp38-cp38-win_amd64.whl", hash = "sha256:3b92622e29d651c6b783e368ba7d6722b1634b8e70bd376fd7610fe1992e19de"}, + {file = "pywin32-308-cp39-cp39-win32.whl", hash = "sha256:7873ca4dc60ab3287919881a7d4f88baee4a6e639aa6962de25a98ba6b193341"}, + {file = "pywin32-308-cp39-cp39-win_amd64.whl", hash = "sha256:71b3322d949b4cc20776436a9c9ba0eeedcbc9c650daa536df63f0ff111bb920"}, ] [[package]] name = "pywinpty" -version = "2.0.13" +version = "2.0.14" description = "Pseudo terminal support for Windows from Python." optional = false python-versions = ">=3.8" files = [ - {file = "pywinpty-2.0.13-cp310-none-win_amd64.whl", hash = "sha256:697bff211fb5a6508fee2dc6ff174ce03f34a9a233df9d8b5fe9c8ce4d5eaf56"}, - {file = "pywinpty-2.0.13-cp311-none-win_amd64.whl", hash = "sha256:b96fb14698db1284db84ca38c79f15b4cfdc3172065b5137383910567591fa99"}, - {file = "pywinpty-2.0.13-cp312-none-win_amd64.whl", hash = "sha256:2fd876b82ca750bb1333236ce98488c1be96b08f4f7647cfdf4129dfad83c2d4"}, - {file = "pywinpty-2.0.13-cp38-none-win_amd64.whl", hash = "sha256:61d420c2116c0212808d31625611b51caf621fe67f8a6377e2e8b617ea1c1f7d"}, - {file = "pywinpty-2.0.13-cp39-none-win_amd64.whl", hash = "sha256:71cb613a9ee24174730ac7ae439fd179ca34ccb8c5349e8d7b72ab5dea2c6f4b"}, - {file = "pywinpty-2.0.13.tar.gz", hash = "sha256:c34e32351a3313ddd0d7da23d27f835c860d32fe4ac814d372a3ea9594f41dde"}, + {file = "pywinpty-2.0.14-cp310-none-win_amd64.whl", hash = "sha256:0b149c2918c7974f575ba79f5a4aad58bd859a52fa9eb1296cc22aa412aa411f"}, + {file = "pywinpty-2.0.14-cp311-none-win_amd64.whl", hash = "sha256:cf2a43ac7065b3e0dc8510f8c1f13a75fb8fde805efa3b8cff7599a1ef497bc7"}, + {file = "pywinpty-2.0.14-cp312-none-win_amd64.whl", hash = "sha256:55dad362ef3e9408ade68fd173e4f9032b3ce08f68cfe7eacb2c263ea1179737"}, + {file = "pywinpty-2.0.14-cp313-none-win_amd64.whl", hash = "sha256:074fb988a56ec79ca90ed03a896d40707131897cefb8f76f926e3834227f2819"}, + {file = "pywinpty-2.0.14-cp39-none-win_amd64.whl", hash = "sha256:5725fd56f73c0531ec218663bd8c8ff5acc43c78962fab28564871b5fce053fd"}, + {file = "pywinpty-2.0.14.tar.gz", hash = "sha256:18bd9529e4a5daf2d9719aa17788ba6013e594ae94c5a0c27e83df3278b0660e"}, ] [[package]] @@ -3064,114 +3063,114 @@ files = [ [[package]] name = "rpds-py" -version = "0.20.0" +version = "0.20.1" description = "Python bindings to Rust's persistent data structures (rpds)" optional = false python-versions = ">=3.8" files = [ - {file = "rpds_py-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2"}, - {file = "rpds_py-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf"}, - {file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140"}, - {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f"}, - {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce"}, - {file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94"}, - {file = "rpds_py-0.20.0-cp310-none-win32.whl", hash = "sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee"}, - {file = "rpds_py-0.20.0-cp310-none-win_amd64.whl", hash = "sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399"}, - {file = "rpds_py-0.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489"}, - {file = "rpds_py-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209"}, - {file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3"}, - {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272"}, - {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad"}, - {file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58"}, - {file = "rpds_py-0.20.0-cp311-none-win32.whl", hash = "sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0"}, - {file = "rpds_py-0.20.0-cp311-none-win_amd64.whl", hash = "sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c"}, - {file = "rpds_py-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6"}, - {file = "rpds_py-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4"}, - {file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef"}, - {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821"}, - {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940"}, - {file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174"}, - {file = "rpds_py-0.20.0-cp312-none-win32.whl", hash = "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139"}, - {file = "rpds_py-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585"}, - {file = "rpds_py-0.20.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29"}, - {file = "rpds_py-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879"}, - {file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f"}, - {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c"}, - {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2"}, - {file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57"}, - {file = "rpds_py-0.20.0-cp313-none-win32.whl", hash = "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a"}, - {file = "rpds_py-0.20.0-cp313-none-win_amd64.whl", hash = "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2"}, - {file = "rpds_py-0.20.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:f2fbf7db2012d4876fb0d66b5b9ba6591197b0f165db8d99371d976546472a24"}, - {file = "rpds_py-0.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1e5f3cd7397c8f86c8cc72d5a791071431c108edd79872cdd96e00abd8497d29"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce9845054c13696f7af7f2b353e6b4f676dab1b4b215d7fe5e05c6f8bb06f965"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c3e130fd0ec56cb76eb49ef52faead8ff09d13f4527e9b0c400307ff72b408e1"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b16aa0107ecb512b568244ef461f27697164d9a68d8b35090e9b0c1c8b27752"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7f429242aae2947246587d2964fad750b79e8c233a2367f71b554e9447949c"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af0fc424a5842a11e28956e69395fbbeab2c97c42253169d87e90aac2886d751"}, - {file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b8c00a3b1e70c1d3891f0db1b05292747f0dbcfb49c43f9244d04c70fbc40eb8"}, - {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:40ce74fc86ee4645d0a225498d091d8bc61f39b709ebef8204cb8b5a464d3c0e"}, - {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4fe84294c7019456e56d93e8ababdad5a329cd25975be749c3f5f558abb48253"}, - {file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:338ca4539aad4ce70a656e5187a3a31c5204f261aef9f6ab50e50bcdffaf050a"}, - {file = "rpds_py-0.20.0-cp38-none-win32.whl", hash = "sha256:54b43a2b07db18314669092bb2de584524d1ef414588780261e31e85846c26a5"}, - {file = "rpds_py-0.20.0-cp38-none-win_amd64.whl", hash = "sha256:a1862d2d7ce1674cffa6d186d53ca95c6e17ed2b06b3f4c476173565c862d232"}, - {file = "rpds_py-0.20.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22"}, - {file = "rpds_py-0.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda"}, - {file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580"}, - {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b"}, - {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420"}, - {file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b"}, - {file = "rpds_py-0.20.0-cp39-none-win32.whl", hash = "sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7"}, - {file = "rpds_py-0.20.0-cp39-none-win_amd64.whl", hash = "sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344"}, - {file = "rpds_py-0.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec"}, - {file = "rpds_py-0.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8"}, - {file = "rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121"}, + {file = "rpds_py-0.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a649dfd735fff086e8a9d0503a9f0c7d01b7912a333c7ae77e1515c08c146dad"}, + {file = "rpds_py-0.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f16bc1334853e91ddaaa1217045dd7be166170beec337576818461268a3de67f"}, + {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14511a539afee6f9ab492b543060c7491c99924314977a55c98bfa2ee29ce78c"}, + {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3ccb8ac2d3c71cda472b75af42818981bdacf48d2e21c36331b50b4f16930163"}, + {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c142b88039b92e7e0cb2552e8967077e3179b22359e945574f5e2764c3953dcf"}, + {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f19169781dddae7478a32301b499b2858bc52fc45a112955e798ee307e294977"}, + {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13c56de6518e14b9bf6edde23c4c39dac5b48dcf04160ea7bce8fca8397cdf86"}, + {file = "rpds_py-0.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:925d176a549f4832c6f69fa6026071294ab5910e82a0fe6c6228fce17b0706bd"}, + {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:78f0b6877bfce7a3d1ff150391354a410c55d3cdce386f862926a4958ad5ab7e"}, + {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3dd645e2b0dcb0fd05bf58e2e54c13875847687d0b71941ad2e757e5d89d4356"}, + {file = "rpds_py-0.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4f676e21db2f8c72ff0936f895271e7a700aa1f8d31b40e4e43442ba94973899"}, + {file = "rpds_py-0.20.1-cp310-none-win32.whl", hash = "sha256:648386ddd1e19b4a6abab69139b002bc49ebf065b596119f8f37c38e9ecee8ff"}, + {file = "rpds_py-0.20.1-cp310-none-win_amd64.whl", hash = "sha256:d9ecb51120de61e4604650666d1f2b68444d46ae18fd492245a08f53ad2b7711"}, + {file = "rpds_py-0.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:762703bdd2b30983c1d9e62b4c88664df4a8a4d5ec0e9253b0231171f18f6d75"}, + {file = "rpds_py-0.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0b581f47257a9fce535c4567782a8976002d6b8afa2c39ff616edf87cbeff712"}, + {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:842c19a6ce894493563c3bd00d81d5100e8e57d70209e84d5491940fdb8b9e3a"}, + {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42cbde7789f5c0bcd6816cb29808e36c01b960fb5d29f11e052215aa85497c93"}, + {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c8e9340ce5a52f95fa7d3b552b35c7e8f3874d74a03a8a69279fd5fca5dc751"}, + {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ba6f89cac95c0900d932c9efb7f0fb6ca47f6687feec41abcb1bd5e2bd45535"}, + {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a916087371afd9648e1962e67403c53f9c49ca47b9680adbeef79da3a7811b0"}, + {file = "rpds_py-0.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:200a23239781f46149e6a415f1e870c5ef1e712939fe8fa63035cd053ac2638e"}, + {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:58b1d5dd591973d426cbb2da5e27ba0339209832b2f3315928c9790e13f159e8"}, + {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6b73c67850ca7cae0f6c56f71e356d7e9fa25958d3e18a64927c2d930859b8e4"}, + {file = "rpds_py-0.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d8761c3c891cc51e90bc9926d6d2f59b27beaf86c74622c8979380a29cc23ac3"}, + {file = "rpds_py-0.20.1-cp311-none-win32.whl", hash = "sha256:cd945871335a639275eee904caef90041568ce3b42f402c6959b460d25ae8732"}, + {file = "rpds_py-0.20.1-cp311-none-win_amd64.whl", hash = "sha256:7e21b7031e17c6b0e445f42ccc77f79a97e2687023c5746bfb7a9e45e0921b84"}, + {file = "rpds_py-0.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:36785be22066966a27348444b40389f8444671630063edfb1a2eb04318721e17"}, + {file = "rpds_py-0.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:142c0a5124d9bd0e2976089484af5c74f47bd3298f2ed651ef54ea728d2ea42c"}, + {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbddc10776ca7ebf2a299c41a4dde8ea0d8e3547bfd731cb87af2e8f5bf8962d"}, + {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15a842bb369e00295392e7ce192de9dcbf136954614124a667f9f9f17d6a216f"}, + {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be5ef2f1fc586a7372bfc355986226484e06d1dc4f9402539872c8bb99e34b01"}, + {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbcf360c9e3399b056a238523146ea77eeb2a596ce263b8814c900263e46031a"}, + {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecd27a66740ffd621d20b9a2f2b5ee4129a56e27bfb9458a3bcc2e45794c96cb"}, + {file = "rpds_py-0.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0b937b2a1988f184a3e9e577adaa8aede21ec0b38320d6009e02bd026db04fa"}, + {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6889469bfdc1eddf489729b471303739bf04555bb151fe8875931f8564309afc"}, + {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:19b73643c802f4eaf13d97f7855d0fb527fbc92ab7013c4ad0e13a6ae0ed23bd"}, + {file = "rpds_py-0.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3c6afcf2338e7f374e8edc765c79fbcb4061d02b15dd5f8f314a4af2bdc7feb5"}, + {file = "rpds_py-0.20.1-cp312-none-win32.whl", hash = "sha256:dc73505153798c6f74854aba69cc75953888cf9866465196889c7cdd351e720c"}, + {file = "rpds_py-0.20.1-cp312-none-win_amd64.whl", hash = "sha256:8bbe951244a838a51289ee53a6bae3a07f26d4e179b96fc7ddd3301caf0518eb"}, + {file = "rpds_py-0.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:6ca91093a4a8da4afae7fe6a222c3b53ee4eef433ebfee4d54978a103435159e"}, + {file = "rpds_py-0.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b9c2fe36d1f758b28121bef29ed1dee9b7a2453e997528e7d1ac99b94892527c"}, + {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f009c69bc8c53db5dfab72ac760895dc1f2bc1b62ab7408b253c8d1ec52459fc"}, + {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6740a3e8d43a32629bb9b009017ea5b9e713b7210ba48ac8d4cb6d99d86c8ee8"}, + {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32b922e13d4c0080d03e7b62991ad7f5007d9cd74e239c4b16bc85ae8b70252d"}, + {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe00a9057d100e69b4ae4a094203a708d65b0f345ed546fdef86498bf5390982"}, + {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fe9b04b6fa685bd39237d45fad89ba19e9163a1ccaa16611a812e682913496"}, + {file = "rpds_py-0.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:aa7ac11e294304e615b43f8c441fee5d40094275ed7311f3420d805fde9b07b4"}, + {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aa97af1558a9bef4025f8f5d8c60d712e0a3b13a2fe875511defc6ee77a1ab7"}, + {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:483b29f6f7ffa6af845107d4efe2e3fa8fb2693de8657bc1849f674296ff6a5a"}, + {file = "rpds_py-0.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37fe0f12aebb6a0e3e17bb4cd356b1286d2d18d2e93b2d39fe647138458b4bcb"}, + {file = "rpds_py-0.20.1-cp313-none-win32.whl", hash = "sha256:a624cc00ef2158e04188df5e3016385b9353638139a06fb77057b3498f794782"}, + {file = "rpds_py-0.20.1-cp313-none-win_amd64.whl", hash = "sha256:b71b8666eeea69d6363248822078c075bac6ed135faa9216aa85f295ff009b1e"}, + {file = "rpds_py-0.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5b48e790e0355865197ad0aca8cde3d8ede347831e1959e158369eb3493d2191"}, + {file = "rpds_py-0.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:3e310838a5801795207c66c73ea903deda321e6146d6f282e85fa7e3e4854804"}, + {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249280b870e6a42c0d972339e9cc22ee98730a99cd7f2f727549af80dd5a963"}, + {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e79059d67bea28b53d255c1437b25391653263f0e69cd7dec170d778fdbca95e"}, + {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2b431c777c9653e569986ecf69ff4a5dba281cded16043d348bf9ba505486f36"}, + {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da584ff96ec95e97925174eb8237e32f626e7a1a97888cdd27ee2f1f24dd0ad8"}, + {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a0629ec053fc013808a85178524e3cb63a61dbc35b22499870194a63578fb9"}, + {file = "rpds_py-0.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fbf15aff64a163db29a91ed0868af181d6f68ec1a3a7d5afcfe4501252840bad"}, + {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:07924c1b938798797d60c6308fa8ad3b3f0201802f82e4a2c41bb3fafb44cc28"}, + {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4a5a844f68776a7715ecb30843b453f07ac89bad393431efbf7accca3ef599c1"}, + {file = "rpds_py-0.20.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:518d2ca43c358929bf08f9079b617f1c2ca6e8848f83c1225c88caeac46e6cbc"}, + {file = "rpds_py-0.20.1-cp38-none-win32.whl", hash = "sha256:3aea7eed3e55119635a74bbeb80b35e776bafccb70d97e8ff838816c124539f1"}, + {file = "rpds_py-0.20.1-cp38-none-win_amd64.whl", hash = "sha256:7dca7081e9a0c3b6490a145593f6fe3173a94197f2cb9891183ef75e9d64c425"}, + {file = "rpds_py-0.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b41b6321805c472f66990c2849e152aff7bc359eb92f781e3f606609eac877ad"}, + {file = "rpds_py-0.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a90c373ea2975519b58dece25853dbcb9779b05cc46b4819cb1917e3b3215b6"}, + {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16d4477bcb9fbbd7b5b0e4a5d9b493e42026c0bf1f06f723a9353f5153e75d30"}, + {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:84b8382a90539910b53a6307f7c35697bc7e6ffb25d9c1d4e998a13e842a5e83"}, + {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4888e117dd41b9d34194d9e31631af70d3d526efc363085e3089ab1a62c32ed1"}, + {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5265505b3d61a0f56618c9b941dc54dc334dc6e660f1592d112cd103d914a6db"}, + {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e75ba609dba23f2c95b776efb9dd3f0b78a76a151e96f96cc5b6b1b0004de66f"}, + {file = "rpds_py-0.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1791ff70bc975b098fe6ecf04356a10e9e2bd7dc21fa7351c1742fdeb9b4966f"}, + {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d126b52e4a473d40232ec2052a8b232270ed1f8c9571aaf33f73a14cc298c24f"}, + {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c14937af98c4cc362a1d4374806204dd51b1e12dded1ae30645c298e5a5c4cb1"}, + {file = "rpds_py-0.20.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3d089d0b88996df627693639d123c8158cff41c0651f646cd8fd292c7da90eaf"}, + {file = "rpds_py-0.20.1-cp39-none-win32.whl", hash = "sha256:653647b8838cf83b2e7e6a0364f49af96deec64d2a6578324db58380cff82aca"}, + {file = "rpds_py-0.20.1-cp39-none-win_amd64.whl", hash = "sha256:fa41a64ac5b08b292906e248549ab48b69c5428f3987b09689ab2441f267d04d"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7a07ced2b22f0cf0b55a6a510078174c31b6d8544f3bc00c2bcee52b3d613f74"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:68cb0a499f2c4a088fd2f521453e22ed3527154136a855c62e148b7883b99f9a"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa3060d885657abc549b2a0f8e1b79699290e5d83845141717c6c90c2df38311"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:95f3b65d2392e1c5cec27cff08fdc0080270d5a1a4b2ea1d51d5f4a2620ff08d"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2cc3712a4b0b76a1d45a9302dd2f53ff339614b1c29603a911318f2357b04dd2"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d4eea0761e37485c9b81400437adb11c40e13ef513375bbd6973e34100aeb06"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f5179583d7a6cdb981151dd349786cbc318bab54963a192692d945dd3f6435d"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2fbb0ffc754490aff6dabbf28064be47f0f9ca0b9755976f945214965b3ace7e"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:a94e52537a0e0a85429eda9e49f272ada715506d3b2431f64b8a3e34eb5f3e75"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:92b68b79c0da2a980b1c4197e56ac3dd0c8a149b4603747c4378914a68706979"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:93da1d3db08a827eda74356f9f58884adb254e59b6664f64cc04cdff2cc19b0d"}, + {file = "rpds_py-0.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:754bbed1a4ca48479e9d4182a561d001bbf81543876cdded6f695ec3d465846b"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ca449520e7484534a2a44faf629362cae62b660601432d04c482283c47eaebab"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9c4cb04a16b0f199a8c9bf807269b2f63b7b5b11425e4a6bd44bd6961d28282c"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63804105143c7e24cee7db89e37cb3f3941f8e80c4379a0b355c52a52b6780"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:55cd1fa4ecfa6d9f14fbd97ac24803e6f73e897c738f771a9fe038f2f11ff07c"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f8f741b6292c86059ed175d80eefa80997125b7c478fb8769fd9ac8943a16c0"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fc212779bf8411667234b3cdd34d53de6c2b8b8b958e1e12cb473a5f367c338"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ad56edabcdb428c2e33bbf24f255fe2b43253b7d13a2cdbf05de955217313e6"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0a3a1e9ee9728b2c1734f65d6a1d376c6f2f6fdcc13bb007a08cc4b1ff576dc5"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e13de156137b7095442b288e72f33503a469aa1980ed856b43c353ac86390519"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:07f59760ef99f31422c49038964b31c4dfcfeb5d2384ebfc71058a7c9adae2d2"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:59240685e7da61fb78f65a9f07f8108e36a83317c53f7b276b4175dc44151684"}, + {file = "rpds_py-0.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:83cba698cfb3c2c5a7c3c6bac12fe6c6a51aae69513726be6411076185a8b24a"}, + {file = "rpds_py-0.20.1.tar.gz", hash = "sha256:e1791c4aabd117653530dccd24108fa03cc6baf21f58b950d0a73c3b3b29a350"}, ] [[package]] @@ -3331,33 +3330,33 @@ win32 = ["pywin32"] [[package]] name = "setuptools" -version = "74.1.2" +version = "75.3.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-74.1.2-py3-none-any.whl", hash = "sha256:5f4c08aa4d3ebcb57a50c33b1b07e94315d7fc7230f7115e47fc99776c8ce308"}, - {file = "setuptools-74.1.2.tar.gz", hash = "sha256:95b40ed940a1c67eb70fc099094bd6e99c6ee7c23aa2306f4d2697ba7916f9c6"}, + {file = "setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd"}, + {file = "setuptools-75.3.0.tar.gz", hash = "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686"}, ] [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.text (>=3.7)", "more-itertools (>=8.8)", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.12.*)", "pytest-mypy"] [[package]] name = "six" -version = "1.16.0" +version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] @@ -3449,22 +3448,22 @@ testing = ["covdefaults (>=2.3)", "coverage (>=7.4.2)", "diff-cover (>=8.0.3)", [[package]] name = "sphinx-rtd-theme" -version = "2.0.0" +version = "3.0.2" description = "Read the Docs theme for Sphinx" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" files = [ - {file = "sphinx_rtd_theme-2.0.0-py2.py3-none-any.whl", hash = "sha256:ec93d0856dc280cf3aee9a4c9807c60e027c7f7b461b77aeffed682e68f0e586"}, - {file = "sphinx_rtd_theme-2.0.0.tar.gz", hash = "sha256:bd5d7b80622406762073a04ef8fadc5f9151261563d47027de09910ce03afe6b"}, + {file = "sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13"}, + {file = "sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85"}, ] [package.dependencies] -docutils = "<0.21" -sphinx = ">=5,<8" +docutils = ">0.18,<0.22" +sphinx = ">=6,<9" sphinxcontrib-jquery = ">=4,<5" [package.extras] -dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] +dev = ["bump2version", "transifex-client", "twine", "wheel"] [[package]] name = "sphinxcontrib-applehelp" @@ -3633,13 +3632,13 @@ files = [ [[package]] name = "tinycss2" -version = "1.3.0" +version = "1.4.0" description = "A tiny CSS parser" optional = false python-versions = ">=3.8" files = [ - {file = "tinycss2-1.3.0-py3-none-any.whl", hash = "sha256:54a8dbdffb334d536851be0226030e9505965bb2f30f21a4a82c55fb2a80fae7"}, - {file = "tinycss2-1.3.0.tar.gz", hash = "sha256:152f9acabd296a8375fbca5b84c961ff95971fcfc32e79550c8df8e29118c54d"}, + {file = "tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289"}, + {file = "tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7"}, ] [package.dependencies] @@ -3651,13 +3650,43 @@ test = ["pytest", "ruff"] [[package]] name = "tomli" -version = "2.0.1" +version = "2.2.1" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] @@ -3673,118 +3702,124 @@ files = [ [[package]] name = "torch" -version = "1.13.1" +version = "1.9.1" description = "Tensors and Dynamic neural networks in Python with strong GPU acceleration" optional = false -python-versions = ">=3.7.0" +python-versions = ">=3.6.2" files = [ - {file = "torch-1.13.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:fd12043868a34a8da7d490bf6db66991108b00ffbeecb034228bfcbbd4197143"}, - {file = "torch-1.13.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:d9fe785d375f2e26a5d5eba5de91f89e6a3be5d11efb497e76705fdf93fa3c2e"}, - {file = "torch-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:98124598cdff4c287dbf50f53fb455f0c1e3a88022b39648102957f3445e9b76"}, - {file = "torch-1.13.1-cp310-none-macosx_10_9_x86_64.whl", hash = "sha256:393a6273c832e047581063fb74335ff50b4c566217019cc6ace318cd79eb0566"}, - {file = "torch-1.13.1-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:0122806b111b949d21fa1a5f9764d1fd2fcc4a47cb7f8ff914204fd4fc752ed5"}, - {file = "torch-1.13.1-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:22128502fd8f5b25ac1cd849ecb64a418382ae81dd4ce2b5cebaa09ab15b0d9b"}, - {file = "torch-1.13.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:76024be052b659ac1304ab8475ab03ea0a12124c3e7626282c9c86798ac7bc11"}, - {file = "torch-1.13.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:ea8dda84d796094eb8709df0fcd6b56dc20b58fdd6bc4e8d7109930dafc8e419"}, - {file = "torch-1.13.1-cp37-cp37m-win_amd64.whl", hash = "sha256:2ee7b81e9c457252bddd7d3da66fb1f619a5d12c24d7074de91c4ddafb832c93"}, - {file = "torch-1.13.1-cp37-none-macosx_10_9_x86_64.whl", hash = "sha256:0d9b8061048cfb78e675b9d2ea8503bfe30db43d583599ae8626b1263a0c1380"}, - {file = "torch-1.13.1-cp37-none-macosx_11_0_arm64.whl", hash = "sha256:f402ca80b66e9fbd661ed4287d7553f7f3899d9ab54bf5c67faada1555abde28"}, - {file = "torch-1.13.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:727dbf00e2cf858052364c0e2a496684b9cb5aa01dc8a8bc8bbb7c54502bdcdd"}, - {file = "torch-1.13.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:df8434b0695e9ceb8cc70650afc1310d8ba949e6db2a0525ddd9c3b2b181e5fe"}, - {file = "torch-1.13.1-cp38-cp38-win_amd64.whl", hash = "sha256:5e1e722a41f52a3f26f0c4fcec227e02c6c42f7c094f32e49d4beef7d1e213ea"}, - {file = "torch-1.13.1-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:33e67eea526e0bbb9151263e65417a9ef2d8fa53cbe628e87310060c9dcfa312"}, - {file = "torch-1.13.1-cp38-none-macosx_11_0_arm64.whl", hash = "sha256:eeeb204d30fd40af6a2d80879b46a7efbe3cf43cdbeb8838dd4f3d126cc90b2b"}, - {file = "torch-1.13.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:50ff5e76d70074f6653d191fe4f6a42fdbe0cf942fbe2a3af0b75eaa414ac038"}, - {file = "torch-1.13.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:2c3581a3fd81eb1f0f22997cddffea569fea53bafa372b2c0471db373b26aafc"}, - {file = "torch-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:0aa46f0ac95050c604bcf9ef71da9f1172e5037fdf2ebe051962d47b123848e7"}, - {file = "torch-1.13.1-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:6930791efa8757cb6974af73d4996b6b50c592882a324b8fb0589c6a9ba2ddaf"}, - {file = "torch-1.13.1-cp39-none-macosx_11_0_arm64.whl", hash = "sha256:e0df902a7c7dd6c795698532ee5970ce898672625635d885eade9976e5a04949"}, -] - -[package.dependencies] -nvidia-cublas-cu11 = {version = "11.10.3.66", markers = "platform_system == \"Linux\""} -nvidia-cuda-nvrtc-cu11 = {version = "11.7.99", markers = "platform_system == \"Linux\""} -nvidia-cuda-runtime-cu11 = {version = "11.7.99", markers = "platform_system == \"Linux\""} -nvidia-cudnn-cu11 = {version = "8.5.0.96", markers = "platform_system == \"Linux\""} + {file = "torch-1.9.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:06435080ba0a2c8f88b65af0550b973c5aa7771eacd9b17f69057fc7436a8ae2"}, + {file = "torch-1.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b92f934b3c95578b3fd37cc06afca208d63f02b0d01b806e979cb4e46124a7f8"}, + {file = "torch-1.9.1-cp36-none-macosx_10_9_x86_64.whl", hash = "sha256:54dacb6a3f63c54334fadbf22fb6e9ee865085a4e0368962edff5babda057606"}, + {file = "torch-1.9.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:dd3ca91dc1a9fe3fbcddf035cb2fb8be44d57a527b845cd196ba69249adecccf"}, + {file = "torch-1.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:42ca081a2e0e759844e70cad7efd8fcfb2f81634dffa73a226564eb83d989e5b"}, + {file = "torch-1.9.1-cp37-none-macosx_10_9_x86_64.whl", hash = "sha256:335961a5c893f7b33b29aecbc19382a1a1b0106b3457a1c45148e1e14f8f5e09"}, + {file = "torch-1.9.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:1fb49ca0ca8edefbb3f47f6801482144c3a746ec21a65eb3f0839a1d8fb24705"}, + {file = "torch-1.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:936d303c5e1d60259fb71d95a33e84d84fececa25a0fae112f6a23286ff183c8"}, + {file = "torch-1.9.1-cp38-none-macosx_10_9_x86_64.whl", hash = "sha256:351dda9f483486bec66ed838234e96f077e6886c88110bb1e2f4a708ed2356ce"}, + {file = "torch-1.9.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:35ec703bc535bde7e8790ab9500f02d4413d995ac981520501fde95e268781e1"}, + {file = "torch-1.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:e470697006a4c08e4fb6a645e8ca49b0d36c8e7ccf413deef5161335bd7399f1"}, + {file = "torch-1.9.1-cp39-none-macosx_10_9_x86_64.whl", hash = "sha256:a198332e2d344d25e423ae2df98d56d83060f19e9f4cf23164dffc8d403efeb8"}, +] + +[package.dependencies] typing-extensions = "*" +[[package]] +name = "torchmetrics" +version = "1.2.1" +description = "PyTorch native Metrics" +optional = false +python-versions = ">=3.8" +files = [ + {file = "torchmetrics-1.2.1-py3-none-any.whl", hash = "sha256:fe03a8c53d0ae5800d34ea615f56295fda281282cd83f647d2184e81c1d4efee"}, + {file = "torchmetrics-1.2.1.tar.gz", hash = "sha256:217387738f84939c39b534b20d4983e737cc448d27aaa5340e0327948d97ca3e"}, +] + +[package.dependencies] +lightning-utilities = ">=0.8.0" +numpy = ">1.20.0" +packaging = ">17.1" +torch = ">=1.8.1" +typing-extensions = {version = "*", markers = "python_version < \"3.9\""} + [package.extras] -opt-einsum = ["opt-einsum (>=3.3)"] +-tests = ["bert-score (==0.3.13)", "dython (<=0.7.4)", "fairlearn", "fast-bss-eval (>=0.1.0)", "faster-coco-eval (>=1.3.3)", "huggingface-hub (<0.20)", "jiwer (>=2.3.0)", "kornia (>=0.6.7)", "lpips (<=0.1.4)", "mir-eval (>=0.6)", "netcal (>1.0.0)", "numpy (<1.25.0)", "pandas (>1.0.0)", "pandas (>=1.4.0)", "pytorch-msssim (==1.0.0)", "rouge-score (>0.1.0)", "sacrebleu (>=2.0.0)", "scikit-image (>=0.19.0)", "scipy (>1.0.0)", "sewar (>=0.4.4)", "statsmodels (>0.13.5)", "torch-complex (<=0.4.3)"] +all = ["SciencePlots (>=2.0.0)", "matplotlib (>=3.2.0)", "mypy (==1.7.1)", "nltk (>=3.6)", "piq (<=0.8.0)", "pycocotools (>2.0.0)", "pystoi (>=0.3.0)", "regex (>=2021.9.24)", "scipy (>1.0.0)", "torch (==2.1.1)", "torch-fidelity (<=0.4.0)", "torchaudio (>=0.10.0)", "torchvision (>=0.8)", "tqdm (>=4.41.0)", "transformers (>4.4.0)", "transformers (>=4.10.0)", "types-PyYAML", "types-emoji", "types-protobuf", "types-requests", "types-setuptools", "types-six", "types-tabulate"] +audio = ["pystoi (>=0.3.0)", "torchaudio (>=0.10.0)"] +detection = ["pycocotools (>2.0.0)", "torchvision (>=0.8)"] +dev = ["SciencePlots (>=2.0.0)", "bert-score (==0.3.13)", "dython (<=0.7.4)", "fairlearn", "fast-bss-eval (>=0.1.0)", "faster-coco-eval (>=1.3.3)", "huggingface-hub (<0.20)", "jiwer (>=2.3.0)", "kornia (>=0.6.7)", "lpips (<=0.1.4)", "matplotlib (>=3.2.0)", "mir-eval (>=0.6)", "mypy (==1.7.1)", "netcal (>1.0.0)", "nltk (>=3.6)", "numpy (<1.25.0)", "pandas (>1.0.0)", "pandas (>=1.4.0)", "piq (<=0.8.0)", "pycocotools (>2.0.0)", "pystoi (>=0.3.0)", "pytorch-msssim (==1.0.0)", "regex (>=2021.9.24)", "rouge-score (>0.1.0)", "sacrebleu (>=2.0.0)", "scikit-image (>=0.19.0)", "scipy (>1.0.0)", "sewar (>=0.4.4)", "statsmodels (>0.13.5)", "torch (==2.1.1)", "torch-complex (<=0.4.3)", "torch-fidelity (<=0.4.0)", "torchaudio (>=0.10.0)", "torchvision (>=0.8)", "tqdm (>=4.41.0)", "transformers (>4.4.0)", "transformers (>=4.10.0)", "types-PyYAML", "types-emoji", "types-protobuf", "types-requests", "types-setuptools", "types-six", "types-tabulate"] +image = ["scipy (>1.0.0)", "torch-fidelity (<=0.4.0)", "torchvision (>=0.8)"] +multimodal = ["piq (<=0.8.0)", "transformers (>=4.10.0)"] +text = ["nltk (>=3.6)", "regex (>=2021.9.24)", "tqdm (>=4.41.0)", "transformers (>4.4.0)"] +typing = ["mypy (==1.7.1)", "torch (==2.1.1)", "types-PyYAML", "types-emoji", "types-protobuf", "types-requests", "types-setuptools", "types-six", "types-tabulate"] +visual = ["SciencePlots (>=2.0.0)", "matplotlib (>=3.2.0)"] [[package]] name = "torchvision" -version = "0.14.1" +version = "0.10.1" description = "image and video datasets and models for torch deep learning" optional = false -python-versions = ">=3.7" +python-versions = "*" files = [ - {file = "torchvision-0.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eeb05dd9dd3af5428fee525400759daf8da8e4caec45ddd6908cfb36571f6433"}, - {file = "torchvision-0.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8d0766ea92affa7af248e327dd85f7c9cfdf51a57530b43212d4e1858548e9d7"}, - {file = "torchvision-0.14.1-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:6d7b35653113664ea3fdcb71f515cfbf29d2fe393000fd8aaff27a1284de6908"}, - {file = "torchvision-0.14.1-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:8a9eb773a2fa8f516e404ac09c059fb14e6882c48fdbb9c946327d2ce5dba6cd"}, - {file = "torchvision-0.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:13986f0c15377ff23039e1401012ccb6ecf71024ce53def27139e4eac5a57592"}, - {file = "torchvision-0.14.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb7a793fd33ce1abec24b42778419a3fb1e3159d7dfcb274a3ca8fb8cbc408dc"}, - {file = "torchvision-0.14.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:89fb0419780ec9a9eb9f7856a0149f6ac9f956b28f44b0c0080c6b5b48044db7"}, - {file = "torchvision-0.14.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:a2d4237d3c9705d7729eb4534e4eb06f1d6be7ff1df391204dfb51586d9b0ecb"}, - {file = "torchvision-0.14.1-cp37-cp37m-win_amd64.whl", hash = "sha256:92a324712a87957443cc34223274298ae9496853f115c252f8fc02b931f2340e"}, - {file = "torchvision-0.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:68ed03359dcd3da9cd21b8ab94da21158df8a6a0c5bad0bf4a42f0e448d28cb3"}, - {file = "torchvision-0.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:30fcf0e9fe57d4ac4ce6426659a57dce199637ccb6c70be1128670f177692624"}, - {file = "torchvision-0.14.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:0ed02aefd09bf1114d35f1aa7dce55aa61c2c7e57f9aa02dce362860be654e85"}, - {file = "torchvision-0.14.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a541e49fc3c4e90e49e6988428ab047415ed52ea97d0c0bfd147d8bacb8f4df8"}, - {file = "torchvision-0.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:6099b3191dc2516099a32ae38a5fb349b42e863872a13545ab1a524b6567be60"}, - {file = "torchvision-0.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c5e744f56e5f5b452deb5fc0f3f2ba4d2f00612d14d8da0dbefea8f09ac7690b"}, - {file = "torchvision-0.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:758b20d079e810b4740bd60d1eb16e49da830e3360f9be379eb177ee221fa5d4"}, - {file = "torchvision-0.14.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:83045507ef8d3c015d4df6be79491375b2f901352cfca6e72b4723e9c4f9a55d"}, - {file = "torchvision-0.14.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:eaed58cf454323ed9222d4e0dd5fb897064f454b400696e03a5200e65d3a1e76"}, - {file = "torchvision-0.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:b337e1245ca4353623dd563c03cd8f020c2496a7c5d12bba4d2e381999c766e0"}, + {file = "torchvision-0.10.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:bc99a984b162ee5626787eaee885d9fec1a5f16837f9d0c8223cca3269b9e47d"}, + {file = "torchvision-0.10.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:46a70a30ea7aeab63e67504778f2565fbb1c153fdd8e1a8c6a22193aec4dbddd"}, + {file = "torchvision-0.10.1-cp36-cp36m-win_amd64.whl", hash = "sha256:e504d9d51eae60a98925aee4a3fd58655abd5669659ad7431f7791a93af166fc"}, + {file = "torchvision-0.10.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cd7e2b1a89d5a08f24325fc12441f5ba2822f407489377ac7841bf351a1f4d37"}, + {file = "torchvision-0.10.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:99d3e01e1d67d12bcc88e826431b70cad5b8e4729a277c04601f83358a120508"}, + {file = "torchvision-0.10.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4ebffeee5468a0934952030eaba1de1dbb08154132235ee1d9049e41dfb1600d"}, + {file = "torchvision-0.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d7c2d6c20244404fc9ca3568c88c305cb5a81d526d5912d52d22c64999bd4353"}, + {file = "torchvision-0.10.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:d6420bf21b9d0bdbabe55d64c8b11c61f8eb077948a55d5707946fcb17d97cec"}, + {file = "torchvision-0.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:453e935212193e89b4bbb8d51082d8138631c2f8a420390284b1946d893df6eb"}, + {file = "torchvision-0.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1c186f42b4f8aa9a01c56c3a758693b0447aa169afb9fba0051177f8fecbd691"}, + {file = "torchvision-0.10.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:ac8dfbe4933013dda898b815e2476ebbc35e3a16b9352dfdd66e773c77755bec"}, + {file = "torchvision-0.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6c8fe90213be4bce590ac9647b34db022d5d1ae94f309a733b9a64e65232173a"}, ] [package.dependencies] numpy = "*" -pillow = ">=5.3.0,<8.3.dev0 || >=8.4.dev0" -requests = "*" -torch = "1.13.1" -typing-extensions = "*" +pillow = ">=5.3.0" +torch = "1.9.1" [package.extras] scipy = ["scipy"] [[package]] name = "tornado" -version = "6.4.1" +version = "6.4.2" 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"}, + {file = "tornado-6.4.2-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e828cce1123e9e44ae2a50a9de3055497ab1d0aeb440c5ac23064d9e44880da1"}, + {file = "tornado-6.4.2-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:072ce12ada169c5b00b7d92a99ba089447ccc993ea2143c9ede887e0937aa803"}, + {file = "tornado-6.4.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a017d239bd1bb0919f72af256a970624241f070496635784d9bf0db640d3fec"}, + {file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c36e62ce8f63409301537222faffcef7dfc5284f27eec227389f2ad11b09d946"}, + {file = "tornado-6.4.2-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bca9eb02196e789c9cb5c3c7c0f04fb447dc2adffd95265b2c7223a8a615ccbf"}, + {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:304463bd0772442ff4d0f5149c6f1c2135a1fae045adf070821c6cdc76980634"}, + {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:c82c46813ba483a385ab2a99caeaedf92585a1f90defb5693351fa7e4ea0bf73"}, + {file = "tornado-6.4.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:932d195ca9015956fa502c6b56af9eb06106140d844a335590c1ec7f5277d10c"}, + {file = "tornado-6.4.2-cp38-abi3-win32.whl", hash = "sha256:2876cef82e6c5978fde1e0d5b1f919d756968d5b4282418f3146b79b58556482"}, + {file = "tornado-6.4.2-cp38-abi3-win_amd64.whl", hash = "sha256:908b71bf3ff37d81073356a5fadcc660eb10c1476ee6e2725588626ce7e5ca38"}, + {file = "tornado-6.4.2.tar.gz", hash = "sha256:92bad5b4746e9879fd7bf1eb21dce4e3fc5128d71601f80005afa39237ad620b"}, ] [[package]] name = "tqdm" -version = "4.66.5" +version = "4.67.1" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.66.5-py3-none-any.whl", hash = "sha256:90279a3770753eafc9194a0364852159802111925aa30eb3f9d85b0e805ac7cd"}, - {file = "tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad"}, + {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, + {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [package.extras] -dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] +dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] +discord = ["requests"] notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] @@ -3806,13 +3841,13 @@ test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0, [[package]] name = "types-python-dateutil" -version = "2.9.0.20240906" +version = "2.9.0.20241206" description = "Typing stubs for python-dateutil" optional = false python-versions = ">=3.8" files = [ - {file = "types-python-dateutil-2.9.0.20240906.tar.gz", hash = "sha256:9706c3b68284c25adffc47319ecc7947e5bb86b3773f843c73906fd598bc176e"}, - {file = "types_python_dateutil-2.9.0.20240906-py3-none-any.whl", hash = "sha256:27c8cc2d058ccb14946eebcaaa503088f4f6dbc4fb6093d3d456a49aef2753f6"}, + {file = "types_python_dateutil-2.9.0.20241206-py3-none-any.whl", hash = "sha256:e248a4bc70a486d3e3ec84d0dc30eec3a5f979d6e7ee4123ae043eedbb987f53"}, + {file = "types_python_dateutil-2.9.0.20241206.tar.gz", hash = "sha256:18f493414c26ffba692a72369fea7a154c502646301ebfe3d56a04b3767284cb"}, ] [[package]] @@ -3842,13 +3877,13 @@ dev = ["flake8", "flake8-annotations", "flake8-bandit", "flake8-bugbear", "flake [[package]] name = "urllib3" -version = "2.2.2" +version = "2.2.3" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] [package.extras] @@ -3910,20 +3945,6 @@ docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx-rtd-theme (>=1.1.0)"] optional = ["python-socks", "wsaccel"] test = ["websockets"] -[[package]] -name = "wheel" -version = "0.44.0" -description = "A built-package format for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "wheel-0.44.0-py3-none-any.whl", hash = "sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f"}, - {file = "wheel-0.44.0.tar.gz", hash = "sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49"}, -] - -[package.extras] -test = ["pytest (>=6.0.0)", "setuptools (>=65)"] - [[package]] name = "widgetsnbextension" version = "4.0.13" @@ -3937,13 +3958,13 @@ files = [ [[package]] name = "zipp" -version = "3.20.1" +version = "3.20.2" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.8" files = [ - {file = "zipp-3.20.1-py3-none-any.whl", hash = "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064"}, - {file = "zipp-3.20.1.tar.gz", hash = "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b"}, + {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, + {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, ] [package.extras] @@ -3957,4 +3978,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.8, <3.10" -content-hash = "832fca2fa6b7cf8e377d2b098d8517e122ac9dc3ea2bfd5713ce6f2556126cd0" +content-hash = "a2b8dfcb4bf08b1074199b1246a4f23f90eea04cd0e540655320774fde862957" diff --git a/pyproject.toml b/pyproject.toml old mode 100644 new mode 100755 index f1a3a0e..c970915 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,8 @@ repository = "https://github.com/sb-ai-lab/Sim4Rec" python = ">=3.8, <3.10" pyarrow = "*" sdv = "0.15.0" -torch = "*" +torch = ">=1.9.1" +torchmetrics="*" pandas = "*" pyspark = ">=3.0" numpy = ">=1.20.0" diff --git a/sim4rec/response/__init__.py b/sim4rec/response/__init__.py index b6e05f9..43dd04c 100644 --- a/sim4rec/response/__init__.py +++ b/sim4rec/response/__init__.py @@ -5,9 +5,12 @@ NoiseResponse, CosineSimilatiry, BernoulliResponse, - ParametricResponseFunction + ParametricResponseFunction, ) +from .nn_response import NNResponseTransformer, NNResponseEstimator + + __all__ = [ 'ActionModelEstimator', 'ActionModelTransformer', @@ -15,5 +18,7 @@ 'NoiseResponse', 'CosineSimilatiry', 'BernoulliResponse', - 'ParametricResponseFunction' + 'ParametricResponseFunction', + 'NNResponseTransformer', + 'NNResponseEstimator', ] diff --git a/sim4rec/response/nn_response.py b/sim4rec/response/nn_response.py new file mode 100644 index 0000000..52d078c --- /dev/null +++ b/sim4rec/response/nn_response.py @@ -0,0 +1,204 @@ +import os +import pickle +import pyspark.sql.functions as sf + +from .response import ActionModelEstimator, ActionModelTransformer +from .nn_utils.models import ResponseModel +from .nn_utils.embeddings import IndexEmbedding +from .nn_utils.datasets import RecommendationData + +from pyspark.sql.types import ( + StructType, + StructField, + IntegerType, + DoubleType, +) + +# Dataframe schema for simulator logs. +SIM_LOG_SCHEMA = StructType( + [ + StructField("user_idx", IntegerType(), True), + StructField("item_idx", IntegerType(), True), + StructField("relevance", DoubleType(), True), + StructField("response_proba", DoubleType(), True), + StructField("response", IntegerType(), True), + StructField("__iter", IntegerType(), True), + ] +) +SIM_LOG_COLS = [field.name for field in SIM_LOG_SCHEMA.fields] + + +class NNResponseTransformer(ActionModelTransformer): + def __init__(self, **kwargs): + super().__init__() + self.hist_data = None + for param, value in kwargs.items(): + setattr(self, param, value) + + @classmethod + def load(cls, checkpoint_dir): + """Load model saved with `NNResponseTransformer.save` method.""" + with open(os.path.join(checkpoint_dir, "_params.pkl"), "rb") as f: + params_dict = pickle.load(f) + params_dict["backbone_response_model"] = ResponseModel.load(checkpoint_dir) + with open(os.path.join(checkpoint_dir, "_item_indexer.pkl"), "rb") as f: + params_dict["item_indexer"] = pickle.load(f) + with open(os.path.join(checkpoint_dir, "_user_indexer.pkl"), "rb") as f: + params_dict["user_indexer"] = pickle.load(f) + return cls(**params_dict) + + def save(self, path): + """Save response model at given path.""" + os.makedirs(path) + self.backbone_response_model.dump(path) + with open(os.path.join(path, "_item_indexer.pkl"), "wb") as f: + pickle.dump(self.item_indexer, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path, "_user_indexer.pkl"), "wb") as f: + pickle.dump(self.user_indexer, f, pickle.HIGHEST_PROTOCOL) + with open(os.path.join(path, "_params.pkl"), "wb") as f: + pickle.dump( + { + "outputCol": self.outputCol, + "log_dir": self.log_dir, + "hist_data_dir": self.hist_data_dir, + }, + f, + pickle.HIGHEST_PROTOCOL, + ) + + def _transform(self, new_recs): + """ + Predict responses for given dataframe with recommendations. + Response function gets dataframe with columns + and returns dataframe with columns . + If the initial dataframe had some other columns, they will be returned as well. + + To sample clicks from this raw probabilities, please use `.response.BernoulliResponse` + + :param new_recs: new recommendations. + :returns: same dataframe, but with predicted click probabilities. + """ + + def predict_udf(df): + # This import is required for correct serialization on worker's side. + from .nn_utils.datasets import PandasRecommendationData + + dataset = PandasRecommendationData( + log=df, + item_indexer=self.item_indexer, + user_indexer=self.user_indexer, + ) + dataset = self.backbone_response_model.transform(dataset=dataset) + + return dataset._log[SIM_LOG_COLS] + + spark = new_recs.sql_ctx.sparkSession + + # read the historical data + hist_data = spark.read.schema(SIM_LOG_SCHEMA).parquet(self.hist_data_dir) + if not hist_data: + print("Warning: the historical data is empty") + hist_data = spark.createDataFrame([], schema=SIM_LOG_SCHEMA) + + # filter users whom we don't need + hist_data = hist_data.join(new_recs, on="user_idx", how="semi") + + # read the updated simulator log + simlog = spark.read.schema(SIM_LOG_SCHEMA).parquet(self.log_dir) + if not simlog: + print("Warning: the simulator log is empty") + simlog = spark.createDataFrame([], schema=SIM_LOG_SCHEMA) + + # filter users whom we don't need + simlog = simlog.join(new_recs, on="user_idx", how="semi") + + # since all the historical records are older than simulated by design, + # and new slates are newer than simulated, i can simply concat it + NEW_ITER_NO = 9999999 # this is just a large number + combined_data = hist_data.unionByName(simlog).unionByName( + new_recs.withColumn("response_proba", sf.lit(0.0)) + .withColumn("response", sf.lit(0.0)) + .withColumn( + "__iter", + sf.lit(NEW_ITER_NO), + ) + ) + + # the dataframe is assumed to be already partitioned by user_idx, + # here we actually just compute response probabilities for + # one user by one worker + groupping_column = "user_idx" + result_df = combined_data.groupby(groupping_column).applyInPandas( + predict_udf, SIM_LOG_SCHEMA + ) + filtered_df = result_df.filter(sf.col("__iter") == NEW_ITER_NO) + return filtered_df.select(new_recs.columns + [self.outputCol]) + + +class NNResponseEstimator(ActionModelEstimator): + def __init__( + self, + log_dir: str, + model_name: str, + hist_data_dir=None, + val_data_dir=None, + outputCol: str = "response_proba", + **kwargs, + ): + """ + :param log_dir: The directory containing simulation logs. + :param model_name: Backbone model name. + :param hist_data_dir: (Optional) Spark DataFrame with historical data. + :param val_data_dir: (Optional) Spark DataFrame with validation data. + TODO: split automatically. + :param outputCol: Output column for MLLib pipeline. + + """ + self.fit_params = kwargs + self.outputCol = outputCol + + # sim log is not loaded immideately, because + # it can be not created when the response model is initialized + self.log_dir = log_dir + self.hist_data_dir = hist_data_dir + self.val_data_dir = val_data_dir + + # create new model + self.item_indexer = self.user_indexer = None + self.model_name = model_name + self.backbone_response_model = None + + def _fit(self, train_data): + """ + Fits the model on given data. + + :param DataFrame train_data: Data to train on, this data must match + the simulator log schema exactly. + """ + train_dataset = RecommendationData( + log=train_data, + item_indexer=self.item_indexer, + user_indexer=self.user_indexer, + ) + self.item_indexer = train_dataset._item_indexer + self.user_indexer = train_dataset._user_indexer + val_dataset = RecommendationData( + log=train_data.sql_ctx.sparkSession.read.parquet(self.val_data_dir), + item_indexer=self.item_indexer, + user_indexer=self.user_indexer, + ) + n_items = train_dataset.n_items + backbone_response_model = ResponseModel( + self.model_name, IndexEmbedding(n_items) + ) + backbone_response_model.fit( + train_dataset, val_data=val_dataset, **self.fit_params + ) + return NNResponseTransformer( + backbone_response_model=backbone_response_model, + item_indexer=self.item_indexer, + user_indexer=self.user_indexer, + hist_data_dir=self.hist_data_dir, + log_dir=self.log_dir, + outputCol=self.outputCol, + ) diff --git a/sim4rec/response/nn_utils/__init__.py b/sim4rec/response/nn_utils/__init__.py new file mode 100644 index 0000000..5267186 --- /dev/null +++ b/sim4rec/response/nn_utils/__init__.py @@ -0,0 +1 @@ +# __init__ \ No newline at end of file diff --git a/sim4rec/response/nn_utils/adversarial.py b/sim4rec/response/nn_utils/adversarial.py new file mode 100644 index 0000000..17c28a4 --- /dev/null +++ b/sim4rec/response/nn_utils/adversarial.py @@ -0,0 +1,359 @@ +import torch +import torch.nn as nn +import numpy as np +import gc +from tqdm.notebook import tqdm +from copy import deepcopy +import torch.nn.functional as F +from torch.nn.utils import clip_grad_norm_ +from sklearn.linear_model import LogisticRegression +from torchmetrics import F1Score, AUROC, Accuracy +from torchmetrics.functional.classification import binary_f1_score, binary_accuracy + + +def evaluate_model( + model, + data_loader, + device="cuda", + threshold=0.5, + silent=False, + debug=False, + **kwargs, +): + # run model on dataloader, compute metrics + f1 = F1Score(task="binary", average="macro", threshold=threshold).to(device) + acc = Accuracy(task="binary", threshold=threshold).to(device) + auc = AUROC(task="binary").to(device) + + model.to(device) + model.eval() + + for batch in tqdm(data_loader, desc="evaluating...", disable=silent): + batch = {k: v.to(device) for k, v in batch.items()} + with torch.no_grad(): + prediction_scores = torch.sigmoid(model(batch)) + corrects = (batch["responses"] > 0).float() + mask = batch["out_mask"] + + # # prediction_shape: (batch_size, max_sequence, 'max_slate, 2) + f1(prediction_scores[mask], corrects[mask]) + auc(prediction_scores[mask], corrects[mask]) + acc(prediction_scores[mask], corrects[mask]) + if debug: + print("\r", prediction_scores[mask], corrects[mask]) + + gc.collect() + return { + "f1": f1.compute().item(), + "roc-auc": auc.compute().item(), + "accuracy": acc.compute().item(), + } + + +def flatten(true, pred, mask, to_cpu=True): + mask = mask.flatten() + nnz_idx = mask.nonzero()[:, 0] + true, pred = [x.flatten()[nnz_idx] for x in [true, pred]] + if to_cpu: + true, pred = [x.cpu().numpy() for x in [true, pred]] + return true, pred + + +def fit_treshold(labels, scores): + best_f1, best_thold, acc = 0.0, 0.01, 0.0 + for thold in np.arange(1e-2, 1 - 1e-2, 0.01): + preds_labels = scores > thold + f1 = binary_f1_score(preds_labels, labels) + # print(f"{thold}: {f1}") + if f1 > best_f1: + acc = binary_accuracy(preds_labels, labels) + best_f1, best_thold = f1, thold + return best_f1, acc, best_thold + + +class Discriminator(nn.Module): + def __init__(self, embedding): + super(Discriminator, self).__init__() + self.embedding = embedding + self.emb_dim = embedding.embedding_dim + self.rnn_layer = nn.GRU( + input_size=self.emb_dim + 1, hidden_size=self.emb_dim, batch_first=True + ) + self.mlp = nn.Sequential( + nn.Linear(self.emb_dim, 10), nn.SELU(), nn.Linear(10, 1) + ) + + def forward(self, batch, gen_output): + item_embs, user_embs = self.embedding(batch) + + items = torch.cat( + [ + item_embs.flatten(0, 1), + # item_embs.flatten(0,1) + ], + dim=-1, + ) + h = user_embs.flatten(0, 1)[None, :, :] + clicks = (batch["responses"].flatten(0, 1) > 0).int().clone() + mask = batch["slates_mask"].flatten(0, 1).clone() # padding mask + + # x = {} + # x['items'] = torch.cat( + # [ + # item_embs.flatten(0,1), + # # torch.zeros_like(item_embs.flatten(0,1)), + # ], + # dim = -1 + # ) + # if self.training: + # indices = (batch['length'] - 1) + # else: + # indices = (batch['in_length'] - 1) + # indices[indices<0] = 0 + # indices = indices[:, None, None].repeat(1, 1, user_embs.size(-1)) + # user_embs = user_embs.gather(1, indices).squeeze(-2).unsqueeze(0) + # x['users'] = user_embs.repeat_interleave(max_sequence, 1) + # x['clicks'] = (batch['responses'].flatten(0,1) > 0 ).int().clone() + # x['mask'] = batch['slates_mask'].flatten(0,1).clone() + + # h = x['users'] + + fake = gen_output * mask + real = clicks + fake = torch.cat([items.detach(), fake[:, :, None]], axis=2) + real = torch.cat([items.detach(), real[:, :, None]], axis=2) + fake_out, _ = self.rnn_layer(fake, h) + real_out, _ = self.rnn_layer(real, h) + fake_out = fake_out * mask[:, :, None] + real_out = real_out * mask[:, :, None] + fake_out = fake_out.mean(axis=1) + real_out = real_out.mean(axis=1) + fake_out = self.mlp(fake_out)[:, 0] + real_out = self.mlp(real_out)[:, 0] + + return real_out - fake_out + + +class AdversarialNCM(nn.Module): + def __init__(self, embedding, readout=False): + super().__init__() + self.embedding = embedding + self.emb_dim = embedding.embedding_dim + self.rnn_layer = nn.GRU( + input_size=self.emb_dim, hidden_size=self.emb_dim, batch_first=True + ) + self.out_layer = nn.Linear(self.emb_dim, 1) + + self.thr = -1.5 + self.readout = readout + self.readout_mode = ( + "threshold" # ['soft' ,'threshold', 'sample', 'diff_sample'] + ) + + self.calibration = False + self.w = 1 + self.b = 0 + + def forward(self, batch, detach_embeddings=False, sample=None, to_reshape=True): + item_embs, user_embs = self.embedding(batch) + shp = item_embs.shape + + items = torch.cat( + [ + item_embs.flatten(0, 1), + # item_embs.flatten(0,1) + ], + dim=-1, + ) + inputs = items.detach() if detach_embeddings else items + h = user_embs.flatten(0, 1)[None, :, :] + clicks = (batch["responses"].flatten(0, 1) > 0).int().clone() + mask = batch["slates_mask"].flatten(0, 1).clone() # padding mask + + inputs = F.dropout1d(inputs, p=0.1, training=self.training) + h = F.dropout1d(h, p=0.1, training=self.training) + rnn_out, _ = self.rnn_layer(inputs, h) + y = self.out_layer(rnn_out)[:, :, 0] + + if self.training and sample is None: + clicks_flat, logits_flat = flatten(clicks, y.detach(), mask) + logreg = LogisticRegression() + logreg.fit(logits_flat[:, None], clicks_flat) + γ = 0.3 + self.w = (1 - γ) * self.w + γ * logreg.coef_[0, 0] + self.b = (1 - γ) * self.b + γ * logreg.intercept_[0] + y = self.w * y + self.b + else: + y = self.w * y + self.b + + if sample: + eps = 1e-8 + gumbel_sample = -( + (torch.rand_like(y) + eps).log() / (torch.rand_like(y) + eps).log() + + eps + ).log() + T = 0.5 + bernoulli_sample = torch.sigmoid((nn.LogSigmoid()(y) + gumbel_sample) / T) + hard_bernoulli_sample = ( + (bernoulli_sample > 0.5).to(torch.float32) - bernoulli_sample + ).detach() + bernoulli_sample + return bernoulli_sample if sample == "soft" else hard_bernoulli_sample + + else: + return y.reshape(shp[:-1]) if to_reshape else y + + +def train_adversarial( + model, + discriminator, + train_loader, + val_loader, + device="cuda", + lr=1e-3, + num_epochs=50, + silent=False, + early_stopping=None, + debug=False, + **kwargs, +): + if early_stopping is None: + early_stopping = num_epochs + model.to(device) + best_model = model + + auc = AUROC(task="binary").to(device) + optimizer = torch.optim.Adam(model.parameters(), lr=lr) + opt_dis = torch.optim.Adam(discriminator.parameters(), lr=1e-3) + epochs_without_improvement = 0 + best_val_scores = evaluate_model( + model, val_loader, device=device, silent=silent, debug=debug + ) + # best_test_scores = evaluate_model(model, test_loader, device=device, silent=silent, debug=debug) + best_loss = 999.0 + + # print(f"Test before learning: {best_test_scores}") + ebar = tqdm(range(num_epochs), desc="train") + + for epoch in ebar: + loss_accumulated = 0.0 + mean_grad_norm = 0.0 + model.train() + + labels = [] + preds = [] + + gc.collect() + # torch.cuda.empty_cache() + + if epoch > 10: + # discriminator training + for batch in tqdm(train_loader, desc=f"epoch {epoch}", disable=silent): + batch = {k: v.to(device) for k, v in batch.items()} + + sample_gen = model(batch, sample="hard", to_reshape=False).detach() + logits_dis = discriminator(batch, sample_gen) + opt_dis.zero_grad() + loss_dis = torch.nn.functional.binary_cross_entropy_with_logits( + logits_dis, torch.ones(logits_dis.shape[0]).to(device) + ) + loss_dis.backward() + opt_dis.step() + + if epoch > 20: + for g in optimizer.param_groups: + g["lr"] = 0.0001 + + # adversarial generator training + for batch in tqdm(train_loader, desc=f"epoch {epoch}", disable=silent): + batch = {k: v.to(device) for k, v in batch.items()} + + sample_gen = model(batch, detach_embeddings=True, sample="soft") + logits_dis = discriminator(batch, sample_gen) + + optimizer.zero_grad() + loss_gen = F.binary_cross_entropy_with_logits( + 1 - logits_dis, torch.ones(logits_dis.shape[0]).to(device) + ) + loss_gen.backward() + optimizer.step() + + for g in optimizer.param_groups: + g["lr"] = 0.001 + + for batch in tqdm(train_loader, desc=f"epoch {epoch}", disable=silent): + batch = {k: v.to(device) for k, v in batch.items()} + raw_scores = model(batch) ################################## + prediction_scores = torch.sigmoid(raw_scores) + corrects = (batch["responses"] > 0).float() + mask = batch["slates_mask"] + loss = torch.nn.functional.binary_cross_entropy_with_logits( + raw_scores[mask], + corrects[mask], + ) ###### + loss.backward() ############ + mean_grad_norm += clip_grad_norm_(model.parameters(), 1).sum().item() + optimizer.step() + loss_accumulated += loss.detach().cpu().item() + labels.append(corrects[batch["out_mask"]].detach().cpu()) + preds.append(prediction_scores[batch["out_mask"]].detach().cpu()) + auc( + prediction_scores[batch["out_mask"]].detach().cpu(), + corrects[batch["out_mask"]].detach().cpu(), + ) + + f1, acc, thold = fit_treshold(torch.cat(labels), torch.cat(preds)) + ebar.set_description(f"train... loss:{loss_accumulated}") + val_m = evaluate_model( + model, + val_loader, + device=device, + threshold=thold, + silent=silent, + debug=debug, + **kwargs, + ) + if not silent: + print( + f"Train: epoch: {epoch} | accuracy: {acc} | " + f"f1: {f1} | loss: {loss_accumulated} | " + f"auc: {auc.compute()} | thld {thold} | grad_norm: {mean_grad_norm / len(train_loader)}" + ) + print( + f"Val: epoch: {epoch} | accuracy: {val_m['accuracy']} | f1: {val_m['f1']} | auc: {val_m['roc-auc']}" + ) + + epochs_without_improvement += 1 + if (val_m["roc-auc"], val_m["f1"], val_m["accuracy"]) > ( + best_val_scores["roc-auc"], + best_val_scores["f1"], + best_val_scores["accuracy"], + ): + best_model = deepcopy(model) + best_val_scores = val_m + # best_test_scores = evaluate_model(model, test_loader, device=device, threshold=thold, silent=silent ) + print( + f"Val update: epoch: {epoch} |" + f"accuracy: {best_val_scores['accuracy']} | " + f"f1: {best_val_scores['f1']} | " + f"auc: {best_val_scores['roc-auc']} | " + f"treshold: {thold}" + ) + # print(f"Test: " + # f"accuracy: {best_test_scores['accuracy']} | " + # f"f1: {best_test_scores['f1']} | " + # f"auc: {best_test_scores['roc-auc']} | " + # ) + + auc.reset() + + if best_loss > loss_accumulated: + epochs_without_improvement = 0 + best_loss = loss_accumulated + + if epochs_without_improvement >= early_stopping or ( + best_val_scores["roc-auc"] == 1.0 + and best_val_scores["f1"] == 1.0 + and best_val_scores["accuracy"] == 1.0 + ): + break + return best_model, best_val_scores, thold diff --git a/sim4rec/response/nn_utils/datasets.py b/sim4rec/response/nn_utils/datasets.py new file mode 100755 index 0000000..b68458c --- /dev/null +++ b/sim4rec/response/nn_utils/datasets.py @@ -0,0 +1,324 @@ +import numpy as np +import pandas as pd +import pyspark +import pyspark.sql.functions as sf + +from abc import ABC, abstractmethod +from .utils import Indexer +from torch.utils.data import Dataset + + +class DatasetBase(Dataset, ABC): + """ + The items and users are reindexed, because torch.nn.Embeddings + and torch.Dataset requires integer indexes in [0, N]. This class + obtains indexes from keyword arguments (`item_id2index` and + `user_id2index`) if specified. You probably want to do it, + when usind different datasets obtained from one source. + Otherwise, create new indexer. + + :param log: pySpark DataFrame with interaction log + :param Indexer item_id2index: (Optinal) indexer for items + :param Indexer users_id2index: (Optinal) indexer for users + :param str padding_id: ID for padding item + :param str unknown_id: ID for previously unseen items or users. + :param int min_item_count: if item appears in logs less than `min_item_count` + times, it will be indexed as "unknown item". + """ + + def __init__( + self, + log, + item_indexer: Indexer = None, + user_indexer: Indexer = None, + padding_id=-1, + unknown_id=-2, + ): + super().__init__() + self._log = log + if item_indexer: + self._item_indexer = item_indexer + else: + self._item_indexer = Indexer(pad_id=padding_id, unk_id=unknown_id) + + if user_indexer: + self._user_indexer = user_indexer + else: + self._user_indexer = Indexer(pad_id=padding_id, unk_id=unknown_id) + + @property + def n_items(self): + return self._item_indexer.n_objs + + @property + def item_id2index(self): + return self._item_indexer.to_dict() + + @property + def n_users(self): + return self._user_indexer.n_objs + + @property + def user_id2index(self): + return self._user_indexer.to_dict() + + @property + def users(self): + return self._users + + def __len__(self): + return self.n_users + + def __getitem__(self, idx): + if type(idx) is list: + return self.__getitems__(idx) + return self.__getitems__([idx]) + + @abstractmethod + def apply_scoring(self, score_df): + """ + Apply scoring to the dataset. The scoring is applied to the + dataset in place. The scoring dataframe must contain the + following columns: + * `user_idx` | int | user index + * `item_idx` | int | item index + * `iter` | int | interaction number + * `response_proba` | float | score of the recommendation + """ + pass + + @abstractmethod + def _get_log_for_users(self, user_idxs): + """ + Given a list of user indexes, return a list of rows containing + aggregated data for given users. Each row corresponds to one interaction, + i.e. each pair ('user_idx', '__iter') supposed to be unique in log. + + The rows must be sorted by ('user_idx', '__iter'). This will allow avoid + using costlu groupby in further code. + """ + pass + + def __getitems__(self, user_idxs: list) -> list[dict]: + """Get data points for users with ids in `user_idx`""" + # user_log list of rows, each row corresponds to one interaction + users_log = self._get_log_for_users(user_idxs) + + batch = [] + curr_user_log = [] + prev_user = -1 + for row in users_log: + if prev_user == row["user_idx"]: + curr_user_log.append(row) + else: + if prev_user != -1: + user_index = self._user_indexer.index_np(prev_user).item() + batch.append(self._user_log_to_datapoint(curr_user_log, user_index)) + prev_user = row["user_idx"] + curr_user_log = [row] + user_index = self._user_indexer.index_np(prev_user).item() + batch.append(self._user_log_to_datapoint(curr_user_log, user_index)) + + return batch + + def _user_log_to_datapoint(self, slates: list, user_index: int): + """ + Gets one datapoint (a history of interactions for single user). + In what follows, define: + * R -- number of recommendations for this + * S - slate size + * Eu, Ei - embedding dim for users and items + Datapoint is a dictionary with the following content: + + Item data: + 'item_indexes': np.array with shape (R, S). Same as previous, + but with indexes (0...N) instead if ids. Used to + index embeddings: nn.Embeddings nor scipy.sparse + can not be used with custom index. + User data: + 'user_index': user index. + Interaction data: + 'slate_mask': np.array with shape (R, S). True for recommended items, + False for placeholder. + 'responses': np.array with shape (R, S). Cell (i, j) + contains an id number of iteractions item + at j-th position of i-th slate. + 'length': int. R. + """ + # Number of recommendations (R) + R = len(slates) + if R == 0: + return self.get_empty_data() + + # Get the maximum slate size (S) + S = max(len(s["item_idxs"]) for s in slates) + + # Prepare arrays to store the data + item_idxs = np.zeros((R, S), dtype=object) + slates_mask = np.zeros((R, S), dtype=bool) + responses = np.zeros((R, S), dtype=int) + timestamps = np.zeros((R, S), dtype=int) + + # Fill the data + for i, slate in enumerate(slates): + slate_size = len(slate["item_idxs"]) + item_idxs[i, :slate_size] = slate["item_idxs"] + slates_mask[i, :slate_size] = [True] * slate_size + responses[i, :slate_size] = slate["responses"] + timestamps[i, :slate_size] = [slate["__iter"]] * slate_size + + # Create the output dictionary + data_point = { + "item_indexes": self._item_indexer.index_np(item_idxs), + "user_index": user_index, + "slates_mask": slates_mask, + "responses": responses, + "timestamps": timestamps, + "length": R, + "slate_size": S, + } + # print(data_point) + return data_point + + def get_empty_data(self, slate_size=10): + """Empty data point""" + return { + "item_indexes": np.ones((1, slate_size), dtype=int), + "user_index": 1, # unknown index + "slates_mask": np.zeros((1, slate_size), dtype=bool), + "responses": np.zeros((1, slate_size), dtype=int), + "length": 1, # zero-length would cause problems during batch collation + "slate_size": slate_size, + "timestamps": np.ones((1, slate_size), dtype=int) * -(10**9), + } + + +class RecommendationData(DatasetBase): + """ + Recommednation dataset handler based on pySpark. Does not keep all the data in RAM. + Recommendation data is initialized as spark DataFrame with required columns: + * `user_idx` | int | user identificator + * `item_idx` | int | item identificator + * `__iter` | int | timestamp + + In additional to required columns, the following columns are optional and + used only in certain applications. + * `response` | int | response, filled with 0s if not present + * `response_proba` | float | response probability, filled with 0.0 if not present + * `slate_pos` | int | position of item in recommendation slate + * `relevance` | float | relevance of recommemded item in slate. This columns is used + only sllate_pos is not present, and then slate_pos is + assigned according to relevances. + """ + + def __init__( + self, + log: pyspark.sql.DataFrame, + item_indexer=None, + user_indexer=None, + padding_id=-1, + unknown_id=-2, + min_item_count=1, + ): + """ + Initializes the dataset from `log` pyspark dataframe. + """ + super().__init__(log, item_indexer, user_indexer, padding_id, unknown_id) + if not item_indexer: + self._item_indexer.update_from_iter( + [ + row["item_idx"] + for row in self._log.groupBy("item_idx") + .agg(sf.count("*").alias("count")) + .filter(sf.col("count") >= min_item_count) + .select("item_idx") + .collect() + ] + ) + + # in _users we store only users which are actually present + # in log rather than all indexed users + self._users = [ + row["user_idx"] for row in self._log.select("user_idx").distinct().collect() + ] + if not user_indexer: + self._user_indexer.update_from_iter(self._users) + + def _get_log_for_users(self, user_idxs: list): + users_log = self._log.filter(sf.col("user_idx").isin(user_idxs)) + users_log = ( + ( + users_log.groupBy("user_idx", "__iter").agg( + sf.collect_list("item_idx").alias("item_idxs"), + sf.collect_list("response").alias("responses"), + ) + ) + .orderBy("user_idx", "__iter") + .collect() + ) + return users_log + + def apply_scoring(self, score_df): + raise NotImplementedError("Not implemented yet") + + +class PandasRecommendationData(DatasetBase): + """Temporary Dataset, normally used inside pandas user-defined functions""" + + def __init__( + self, + log: pd.DataFrame, + item_indexer: Indexer = None, + user_indexer: Indexer = None, + padding_id=None, + min_item_count=1, + ): + """ + Initializes the dataset from `log` pandas dataframe. + """ + super(PandasRecommendationData, self).__init__( + log=log, item_indexer=item_indexer, user_indexer=user_indexer + ) + if not item_indexer: + self._item_indexer.update_from_iter(self._log.item_idx.unique()) + if not user_indexer: + self._user_indexer.update_from_iter(self._log.user_idx.unique()) + self._users = self._log.user_idx.unique() + + def _get_log_for_users(self, user_idxs: list): + """ + Extract rows belonging to specific set of users. + + :param user_idxs: list of user indexes. If None, all users are returned. + """ + users_log = self._log + if user_idxs: + users_log = self._log[self._log["user_idx"].isin(user_idxs)] + users_log = ( + users_log.groupby(["user_idx", "__iter"]) + .agg( + item_idx=pd.NamedAgg(column="item_idx", aggfunc=list), + response=pd.NamedAgg(column="response", aggfunc=list), + ) + .rename(columns={"item_idx": "item_idxs", "response": "responses"}) + .reset_index() + ) + # convert to list of rows to match spark .collect() format: + users_log = users_log.to_dict(orient="records") + return users_log + + def apply_scoring(self, score_df): + """Add predicted probabilities to log""" + score_df = ( + score_df.groupby(["user_index", "item_index", "__iter"]) + .agg({"score": "first"}) + .reset_index() + ) + self._log["item_index"] = self._item_indexer.index_np(self._log["item_idx"]) + self._log["user_index"] = self._user_indexer.index_np(self._log["user_idx"]) + self._log = self._log.merge( + score_df, on=["user_index", "item_index", "__iter"], how="left" + ) + self._log["response_proba"] = self._log["score"] + self._log.drop(columns=["score", "item_index", "user_index"], inplace=True) + return self diff --git a/sim4rec/response/nn_utils/embeddings.py b/sim4rec/response/nn_utils/embeddings.py new file mode 100755 index 0000000..f589a8a --- /dev/null +++ b/sim4rec/response/nn_utils/embeddings.py @@ -0,0 +1,165 @@ +import torch +import numpy as np +import torch.nn as nn +from sklearn.utils.extmath import randomized_svd +from abc import ABC, abstractmethod + +def add_zero_item(item_embeddings): + """ + Adds an artificial zero item to a given item sequence + Item embeddings are assumed to be of a shape + (batch, sequence_len, embedding_dim) or (batch, sequence_len) + """ + return torch.cat( + [torch.zeros_like(item_embeddings[:, :1, ...]), item_embeddings], dim=1 + ) + +class EmbeddingBase(ABC, nn.Module): + """ + Defines a common interface for all embeddings + + :param embedding_dim: desired dimensionality of the embedding layer. + :param user_aggregate: if None, user embeddings will be produced + independently from item embeddings. Otherwise, user embeddings are + aggregations ('sum' or 'mean') of items previously consumed by this user. + Default: 'mean'. + """ + + def __init__(self, embedding_dim: int, user_aggregate: str = "mean"): + super().__init__() + self.user_agg = user_aggregate + self.embedding_dim = embedding_dim + + def forward(self, batch: dict) -> tuple[torch.Tensor, torch.Tensor]: + """ + This method returns embeddings for both items and users. + + If user aggregation is specified, the resulting user embeddings + are obtained by aggregating consumed item embeddings. Otherwise, + produce items and user embeddings independintly. + + Shapes of embedings are `(batch_size, sequence_len, slate_size, embedding_dim)` + for items and `(batch_size, sequence_len, embedding_dim)` for users. + + :param batch: batched data (see :func:`utils.collate_recommendation_data`) + :returns: a tuple of item and user embeddings. + """ + item_embeddings = self._get_item_embeddings(batch) + if not self.user_agg: + user_embeddings = self._get_user_embeddings(batch) + else: + user_embeddings = self._aggregate_item_embeddings(item_embeddings, batch) + return item_embeddings, user_embeddings + + @abstractmethod + def _get_item_embeddings(self, batch): + """ + This method produces embeddings for all items in the batch. + + :param batch: batched data (see :func:`utils.collate_recommendation_data`) + :returns: tensor containing user embeddings for each recommendation with shape: + `(batch_size, sequence_len, slate_size, embedding_dim)` + """ + pass + + @abstractmethod + def _get_user_embeddings(self, batch): + """ + This method produces embeddings for all items in the batch. + + :param batch: batched data (see :func:`utils.collate_recommendation_data`) + :returns: tensor containing user embeddings for each recommendation with shape: + `(batch_size, sequence_len, embedding_dim)` + """ + pass + + def _aggregate_item_embeddings(self, item_embeddings, batch): + """ + Aggregate consumed item embeddings into user embeddings. + + For each user in batch, at interaction `t` user embedding is defined as + aggregating all items during interactions `1 \hdots t-1`, which + received positive responses. Aggregation can be "sum" or "mean", + depending on the initialization parameter `user_aggregate. + For the first interaction in each session (hence without any previous clicks), + user embeddings are set to zero. + + :param item_embeddings: tensor with item embeddings with expected shape: + `(batch_size, sequence_len, slate_size, embedding dim)`. + :param batch: batched data (see :func:`utils.collate_recommendation_data`) + :returns: tensor containing user embeddings for each recommendation with shape: + `(batch_size, sequence_len, embedding_dim)` + """ + batch_size, max_sequence = batch["responses"].shape[:2] + + # Shift the batch for 1 item to avoid leakage + item_embeddings_shifted = torch.cat( + [ + torch.zeros_like(item_embeddings[:, :1, :, :]), + item_embeddings[:, :-1, :, :], + ], + dim=1, + ) + + responses_shifted = torch.cat( + [ + torch.zeros_like(batch["responses"][:, :1, :]), + batch["responses"][:, :-1, :], + ], + dim=1, + ) + consumed_embedding = item_embeddings_shifted * responses_shifted[..., None] + consumed_embedding = consumed_embedding.sum(-2).cumsum(-2) + + if self.user_agg == "sum": + pass + elif self.user_agg == "mean": + total_responses = responses_shifted.sum(-1).cumsum(-1) + nnz = total_responses > 0 + consumed_embedding[nnz] /= total_responses[nnz].unsqueeze(-1) + else: + raise ValueError(f"Unknown aggregation {self.agg}") + + return consumed_embedding + + +class IndexEmbedding(EmbeddingBase): + """ + Learnable nn.Embeddings for item and user indexes. + + It is assumed, that every batch contains the 'item_indexes' + and 'user_indexes' keys with values being torch.tensor of shape + (batch_size, sequence_len, slate_size) for items and + (batch_size, sequence_len) for users. + + + :param n_items: number of unique items. + :param n_users: number of unique users. + """ + + def __init__( + self, + n_items: int, + n_users: int = None, + embedding_dim: int = 32, + user_aggregate: str = "mean", + ): + super().__init__(embedding_dim, user_aggregate=user_aggregate) + self.item_embeddings = nn.Embedding(n_items, embedding_dim) + if not user_aggregate: + assert n_users, "Number of users is undefined" + self.user_embedding = nn.Embedding(n_users, embedding_dim) + + def _get_item_embeddings(self, batch): + return self.item_embeddings(batch["item_indexes"]) + + def _get_user_embeddings(self, batch): + return self.user_embedding(batch["user_indexes"]) + + +def stack_embeddings(user_embs, item_embs): + """Concatenate user and item embeddings""" + return torch.cat( + [item_embs, user_embs[:, :, None, :].repeat(1, 1, item_embs.size(-2), 1)], + dim=-1, + ) diff --git a/sim4rec/response/nn_utils/models.py b/sim4rec/response/nn_utils/models.py new file mode 100755 index 0000000..852def3 --- /dev/null +++ b/sim4rec/response/nn_utils/models.py @@ -0,0 +1,386 @@ +import os +import pickle +from collections import namedtuple +from copy import deepcopy + +import numpy as np +import pandas as pd +import torch +import torch.nn as nn +from torch.nn.utils import clip_grad_norm_ +from torchmetrics import AUROC +from torchmetrics.functional import accuracy, f1_score +from tqdm.auto import tqdm + +try: + import mlflow +except ImportError: + pass + +from .adversarial import AdversarialNCM +from .datasets import RecommendationData, PandasRecommendationData +from .sessionwise import ( + SCOT, + AggregatedSlatewiseGRU, + DummyTransformerGRU, + SessionwiseGRU, + SessionwiseTransformer, + TransformerGRU, +) +from .slatewise import ( + DotProduct, + LogisticRegression, + NeuralClickModel, + SlatewiseGRU, + SlatewiseTransformer, +) +from .utils import create_loader + +Metrics = namedtuple("metrics", ["rocauc", "f1", "accuracy"]) + + +class ResponseModel: + def __init__( + self, model, embeddings, calibrator=None, log_to_mlflow=False, **kwargs + ): + """ + :param model: string name of model + :param embeddings: exemplar of embeddings class + :param calibrator: Sklearn-compatible calibration instance. + If given, responses are generated according to predicted probabilities + (i.e., with threshold 0.5 for determiinistic responses or sampled), + otherwise theshold is fitted to raw model scores. + """ + self._embeddings = embeddings + self.model_name = model + self.threshold = 0.5 + self._calibrator = calibrator + self.auc = AUROC(task="binary") + self.log_to_mlflow = log_to_mlflow + if model == "DotProduct": + self._model = DotProduct(embeddings, **kwargs) + elif model == "LogisticRegression": + self._model = LogisticRegression(embeddings, **kwargs) + elif model == "SlatewiseTransformer": + self._model = SlatewiseTransformer(embeddings, **kwargs) + elif model == "SessionwiseTransformer": + self._model = SessionwiseTransformer(embeddings, **kwargs) + elif model == "DummyTransformerGRU": + self._model = DummyTransformerGRU(embeddings, **kwargs) + elif model == "TransformerGRU": + self._model = TransformerGRU(embeddings, **kwargs) + elif model == "SCOT": + self._model = SCOT(embeddings, **kwargs) + elif model == "SlatewiseGRU": + self._model = SlatewiseGRU(embeddings, **kwargs) + elif model == "AggregatedSlatewiseGRU": + self._model = AggregatedSlatewiseGRU(embeddings, **kwargs) + elif model == "SessionwiseGRU": + self._model = SessionwiseGRU(embeddings, **kwargs) + elif model == "NCMBase": + self._model = NeuralClickModel(embeddings, **kwargs) + elif model == "NCMDiffSample": + self._model = NeuralClickModel(embeddings, readout="diff_sample", **kwargs) + elif model == "AdversarialNCM": + self._model = AdversarialNCM(embeddings) + else: + raise ValueError(f"unknown model {model}") + if self.log_to_mlflow: + mlflow.log_params({"model": self.model_name, **kwargs}) + + def set_calibrator(self, calibrator): + self._calibrator = calibrator + + def dump(self, path): + """ + Save model parameters and weights checkpoint on a disk. + :param path: where to save teh model + """ + params = { + "model_name": self.model_name, + "threshold": self.threshold, + "device": self.device, + "calibrator": self._calibrator, + } + with open(os.path.join(path, "params.pkl"), "wb") as f: + pickle.dump(params, f, pickle.HIGHEST_PROTOCOL) + torch.save(self._model, os.path.join(path, "model.pt")) + torch.save(self._embeddings, os.path.join(path, "embeddings.pt")) + + @classmethod + def load(cls, path): + """ + Loads model from files creqated by `ResponseModel.dump` method. + :param path: where the model is saved + """ + embeddings = torch.load(os.path.join(path, "embeddings.pt")) + with open(os.path.join(path, "params.pkl"), "rb") as f: + params = pickle.load(f) + model = cls(params["model_name"], embeddings) + model.threshold = params["threshold"] + model._model = torch.load(os.path.join(path, "model.pt")) + model.device = params["device"] + return model + + def _val_epoch(self, data_loader, silent=True): + """Run model on given dataloader, compute metrics""" + self.auc.reset() + self._model.eval() + loss_accumulated = 0.0 + for batch in tqdm(data_loader, desc="evaluate:", disable=silent): + batch = {k: v.to(self.device) for k, v in batch.items()} + with torch.no_grad(): + scores = self._model(batch) + prediction_probs = torch.sigmoid(scores) + corrects = (batch["responses"] > 0).float() + mask = batch["out_mask"] + self.auc(prediction_probs[mask].cpu(), corrects[mask].cpu()) + + criterion = nn.functional.binary_cross_entropy_with_logits + loss_mask = batch["slates_mask"] + loss = criterion( + scores[loss_mask], + corrects[loss_mask], + ) + loss_accumulated += loss.cpu().item() + + self.val_loss = loss_accumulated + + def _train_epoch(self, data_loader, optimizer, criterion, silent=False): + loss_accumulated = 0.0 + for batch in tqdm(data_loader, desc="train epoch:", disable=silent): + batch = {k: v.to(self.device) for k, v in batch.items()} + mask = batch["slates_mask"] + corrects = (batch["responses"] > 0).float() + + scores = self._model(batch) + loss = criterion( + scores[mask], + corrects[mask], + ) + loss_accumulated += loss.detach().cpu().item() + loss.backward() + clip_grad_norm_(self._model.parameters(), 1.0) + optimizer.step() + + metric_mask = batch["out_mask"] + self.auc( + torch.sigmoid(scores[metric_mask]).detach().cpu(), + corrects[metric_mask].detach().cpu(), + ) + + return loss_accumulated + + def _train( + self, + train_loader, + val_loader, + device="cuda", + lr=1e-3, + num_epochs=100, + silent=False, + early_stopping=7, + debug=False, + ): + if early_stopping == 0: + early_stopping = num_epochs + + optimizer = torch.optim.Adam(self._model.parameters(), lr=lr) + epochs_without_improvement = 0 + criterion = nn.functional.binary_cross_entropy_with_logits + self.ebar = tqdm(range(num_epochs), desc="epoch") + best_model = deepcopy(self._model) + best_model_calibrator = deepcopy(self._calibrator) + self.best_val_scores = self.evaluate(val_loader, silent=silent) + best_val_loss = None + best_epoch = 0 + + for epoch in self.ebar: + self._model.train() + train_loss = self._train_epoch( + train_loader, optimizer, criterion, silent=silent + ) + preds, target = torch.cat(self.auc.preds), torch.cat(self.auc.target) + self._fit_threshold_f1(preds, target) + if self._calibrator: + self._calibrator.fit(preds.numpy(), target.numpy()) + self.auc.reset() + + val_scores = self.evaluate(val_loader, silent=silent) + epochs_without_improvement += 1 + + # updating best checkpoint based on roc_auc, then f1, then accuracy + if val_scores >= self.best_val_scores: + best_model = deepcopy(self._model) + best_model_calibrator = deepcopy(self._calibrator) + self.best_val_scores = val_scores + best_epoch = epoch + + if self.log_to_mlflow: + metrics = { + "val_auc": val_scores.rocauc.numpy().tolist(), + "val_f1": val_scores.f1.numpy().tolist(), + "val_accuracy": val_scores.accuracy.numpy().tolist(), + "best_val_auc": self.best_val_scores.rocauc.numpy().tolist(), + "best_val_f1": self.best_val_scores.f1.numpy().tolist(), + "best_val_accuracy": self.best_val_scores.accuracy.numpy().tolist(), + "threshold": self.threshold, + "epochs_without_improvement": epochs_without_improvement, + "train_loss": train_loss, + "val_loss": self.val_loss, + "best_val_loss": best_val_loss, + "best_epoch": best_epoch, + } + if self._calibrator is not None: + metrics.update( + { + "calibrator_a_": self._calibrator.a_, + "calibrator_b_": self._calibrator.b_, + } + ) + + mlflow.log_metrics(metrics, step=epoch) + + # early stopping + if not best_val_loss or best_val_loss > self.val_loss: + epochs_without_improvement = 0 + best_val_loss = self.val_loss + if epochs_without_improvement >= early_stopping or val_scores == ( + 1.0, + 1.0, + 1.0, + ): + print("Early stopping") + break + self._model = best_model + self._calibrator = best_model_calibrator + + def evaluate(self, datalaoder, silent=True) -> Metrics: + self._val_epoch(datalaoder, silent=silent) + preds, target = torch.cat(self.auc.preds), torch.cat(self.auc.target) + metrics = Metrics( + self.auc.compute(), + f1_score( + preds, target, task="binary", threshold=self.threshold, average="macro" + ), + accuracy(preds, target, task="binary", threshold=self.threshold), + ) + if not silent: + print(metrics) + return metrics + + def _get_probs_and_responses(self, raw_scores, response_type="sample"): + """ + Compute calibrated probabilities and predicted responses for given raw model scores. + + :param raw_scores: backbone model output, after sigmmoid + :param resoponse_type: 'sample' or 'deterministic' + """ + if self._calibrator is None: + predicted_probs = raw_scores + predicted_responses = (predicted_probs >= self.threshold).long() + else: + shp = raw_scores.shape + predicted_probs = torch.tensor( + self._calibrator.predict(raw_scores.cpu().flatten()) + ).reshape(shp) + predicted_responses = (predicted_probs >= 0.5).long() + if response_type == "sample": + predicted_responses = torch.bernoulli(predicted_probs).long() + elif response_type == "deterministic": + pass + else: + raise ValueError(f"unkbnown response type {response_type}") + return predicted_probs, predicted_responses + + def fit( + self, + train_data: RecommendationData, + batch_size, + device="cuda", + silent=False, + val_data: RecommendationData = None, + **kwargs, + ): + """ + Fits model to given dataset. + """ + + self.to(device) + + # if validation dataset is not given, split train + if val_data is None: + train_data, val_data = train_data.split_by_users(0.8, seed=123) + val_loader = create_loader(val_data, batch_size=batch_size) + train_loader = create_loader(train_data, batch_size=batch_size) + self._train(train_loader, val_loader, silent=silent, device=device, **kwargs) + + def _get_scores( + self, + dataset: RecommendationData, + batch_size: int, + **kwargs, + ): + """ + Run model on dataset, get predicted click probabilities. + + :return: (user_ids, timestamps, items, scores) + """ + users, items, scores, timestamps = [], [], [], [] + + loader = create_loader(dataset, batch_size=batch_size) + for batch in loader: + with torch.no_grad(): + batch = {k: v.to(self.device) for k, v in batch.items()} + mask = batch["slates_mask"] + raw_scores = torch.sigmoid(self._model(batch)) + items.append(batch["item_indexes"][mask].detach().cpu().numpy()) + # create a view s.t. [user_1, ... ] -> [[user_1 x slate_size] x sequence_size, ... ] + # then select by mask for items to allign with scores and item_idx sequences + users.append( + batch["user_indexes"][:, None, None] + .expand_as(mask)[mask] + .detach() + .cpu() + .numpy() + ) + scores.append(raw_scores[mask].detach().cpu().numpy()) + timestamps.append(batch["timestamps"][mask].detach().cpu().numpy()) + return users, timestamps, items, scores + + def transform(self, dataset, batch_size=128, **kwargs): + """ + Returns a recommendation dataset with response probabilities provided. + + :param RecommendationData dataset: datset to operate on. + """ + if type(dataset) is PandasRecommendationData: + user_idx, timestamp, item_idx, score = self._get_scores( + dataset, batch_size, **kwargs + ) + score_df = pd.DataFrame( + { + "user_index": np.concatenate(user_idx), + "__iter": np.concatenate(timestamp), + "item_index": np.concatenate(item_idx), + "score": np.concatenate(score), + } + ) + return deepcopy(dataset).apply_scoring(score_df) + else: + raise NotImplementedError + + def to(self, device: str): + self._model = self._model.to(device) + self.device = device + + def _fit_threshold_f1(self, preds, target): + best_f1 = 0.0 + for thold in np.arange(0.0, 1.0, 0.01): + f1 = f1_score( + preds, target, task="binary", threshold=thold, average="macro" + ).item() + if f1 >= best_f1: + self.threshold = thold + best_f1 = f1 + return best_f1 diff --git a/sim4rec/response/nn_utils/sessionwise.py b/sim4rec/response/nn_utils/sessionwise.py new file mode 100755 index 0000000..b38eb67 --- /dev/null +++ b/sim4rec/response/nn_utils/sessionwise.py @@ -0,0 +1,355 @@ +import torch +import torch.nn as nn +from .embeddings import add_zero_item, stack_embeddings + + +class SessionwiseGRU(nn.Module): + """GRU on all recommended items in session""" + + def __init__(self, embedding, output_dim=1, dropout=0.1): + super().__init__() + self.embedding_dim = embedding.embedding_dim + self.embedding = embedding + self.rnn_layer = nn.GRU( + input_size=embedding.embedding_dim, + hidden_size=embedding.embedding_dim, + batch_first=True, + dropout=dropout, + ) + self.out_layer = nn.Linear(embedding.embedding_dim, output_dim) + + def forward(self, batch): + item_embs, user_embs = self.embedding(batch) + shp = item_embs.shape[:-1] # (batch_size, session_len, slate_size) + # flatening slates into one long sequence + item_embs = item_embs.flatten(1, 2) + + # hidden state is the user embedding before the first iteraction + hidden = user_embs[None, :, 0, :].contiguous() + rnn_out, _ = self.rnn_layer( + item_embs, + hidden, + ) + return self.out_layer(rnn_out).reshape(shp) + + +class AggregatedSlatewiseGRU(nn.Module): + """ + Slatewise GRU cell, whose hidden state initialized + with aggregated hiddens over the previous slate + """ + + def __init__(self, embedding, output_dim=1, dropout=0.1): + super().__init__() + self.embedding_dim = embedding.embedding_dim + self.embedding = embedding + self.rnn_layer = torch.nn.GRU( + input_size=embedding.embedding_dim, + hidden_size=embedding.embedding_dim, + batch_first=True, + dropout=dropout, + ) + self.out_layer = nn.Linear(embedding.embedding_dim, output_dim) + + def forward(self, batch): + item_embs, user_embs = self.embedding(batch) + shp = item_embs.shape[:-1] # (batch_size, session_len, slate_size) + session_len = shp[1] + + # initial hidden is the user embedding before the first iteraction + hidden = user_embs[None, ..., 0, :].contiguous() + preds = [] + + # iterate over slates + for slate_no in range(session_len): + # run GRU on the current slate + rnn_out, hidden = self.rnn_layer( + item_embs[..., slate_no, :, :], + hidden, + ) + # save output for further prediction + preds.append(rnn_out[..., None, :, :]) + # aggregate hiddens for the next slate + hidden = rnn_out.mean(dim=1)[None, :, :] + + preds = torch.cat(preds, axis=1) + return self.out_layer(preds).reshape(shp) + + +class SCOT(nn.Module): + """ + Session-wise transformer, working on sequences of clicked-only items. + """ + + def __init__(self, embedding, nheads=2, output_dim=1, debug=False): + super().__init__() + self.embedding_dim = embedding.embedding_dim + self.embedding = embedding + self.nheads = nheads + self.attention = nn.MultiheadAttention( + 2 * self.embedding_dim, num_heads=nheads, batch_first=True + ) + self.out_layer = nn.Sequential( + nn.LayerNorm(2 * embedding.embedding_dim), + nn.Linear(embedding.embedding_dim * 2, embedding.embedding_dim * 2), + nn.GELU(), + nn.Linear(embedding.embedding_dim * 2, output_dim), + ) + + def forward(self, batch): + # getting embeddings & flatening them into one sequence + item_embs, user_embs = self.embedding(batch) + item_embs = stack_embeddings(user_embs, item_embs) + shp = item_embs.shape[:-1] + device = item_embs.device + slate_size = item_embs.size(-2) + session_length = item_embs.size(1) + batch_size = item_embs.size(0) + + # Tensor of shape (batch, session_length, slate_size) + # indicating which iteration this item belongs to + slate_num_for_item = torch.arange(session_length).to(device) + slate_num_for_item = slate_num_for_item[None, :, None].repeat( + batch_size, 1, slate_size + ) + + # Adding a dummy "zero item". It is required, pytorch + # attention implementation will fail if there are sequences + # with no keys in batch + item_embs = item_embs.flatten(1, 2) + item_embs = add_zero_item(item_embs) + slate_num_for_item = slate_num_for_item.flatten(1, 2) + 1 + slate_num_for_item = add_zero_item(slate_num_for_item) + + # gathering clicked items + keys = item_embs + clicked_mask = batch["responses"].flatten(1, 2) > 0 + clicked_mask = ~add_zero_item(~clicked_mask) + clicked_items_slateno, clicked_items = [], [] + for i in range(batch_size): + clicked_items.append(keys[i][clicked_mask[i], :]) + clicked_items_slateno.append(slate_num_for_item[i][clicked_mask[i]]) + keys = nn.utils.rnn.pad_sequence( + clicked_items, batch_first=True, padding_value=float("nan") + ) + slate_num_clicked_items = nn.utils.rnn.pad_sequence( + clicked_items_slateno, batch_first=True, padding_value=session_length + 1 + ) + key_padding_mask = keys.isnan().any(-1) + keys = keys.nan_to_num(0) + + # Now `keys` is a sequence of all clicked items in each session. + # We are constructing a mask to forbid model looking into future iteractions + # Mask shape: (num_heads * bsize, all_items_sequence_length, clicked_sequence_len) + # with True on position a pair (item, clicked_item) if `clicked_item` is recommended + # after the `item` + attn_mask = [] + for i in range(batch_size): + slateno = slate_num_for_item[i] + clicked_slateno = slate_num_clicked_items[i] + mask = slateno[:, None] <= clicked_slateno[None, :] + mask[:, 0] = False # always can attend the 'zero item' + attn_mask.append(mask) + + attn_mask = torch.nn.utils.rnn.pad_sequence( + attn_mask, batch_first=True, padding_value=True + ) + attn_mask.to(device) + + # run the model + features, attn_map = self.attention( + item_embs, + keys, + keys, + key_padding_mask=key_padding_mask, + attn_mask=attn_mask.repeat_interleave(self.nheads, 0), + ) + + # remove artificial `zero item` + features = features[:, 1:, :] + return self.out_layer(features).reshape(shp).squeeze(-1) + + +class DummyTransformerGRU(nn.Module): + """ + Output features of slatewise attention layer and + sessionwise GRU layer are concatenated. This model + id used for ablation study of Two-Stage Transformer+GRU model. + """ + + def __init__(self, embedding, nheads=2, output_dim=1): + super().__init__() + self.embedding_dim = embedding.embedding_dim + self.embedding = embedding + self.attention = nn.MultiheadAttention( + 2 * embedding.embedding_dim, num_heads=nheads, batch_first=True + ) + self.rnn_layer = nn.GRU( + input_size=embedding.embedding_dim, + hidden_size=embedding.embedding_dim, + batch_first=True, + ) + self.out_layer = nn.Linear(3 * embedding.embedding_dim, output_dim) + + def get_attention_embeddings(self, item_embs, user_embs, slate_mask): + shp = item_embs.shape[:-1] + # reinterpret sequence dimension as batch dimension + features = stack_embeddings(user_embs, item_embs).flatten(0, 1) + key_padding_mask = slate_mask.flatten(0, 1) + # add an artificial item + features = add_zero_item(features) + key_padding_mask = add_zero_item(~key_padding_mask) + # run module + features, attn_map = self.attention( + features, features, features, key_padding_mask=key_padding_mask + ) + # drop the artificial item + features = features[:, 1:, ...] + features = features.reshape(shp + (self.embedding_dim * 2,)) + return features + + def forward(self, batch): + item_embs, user_embs = self.embedding(batch) + slate_mask = batch["slates_mask"].clone() + + # slatewise attention + att_features = self.get_attention_embeddings(item_embs, user_embs, slate_mask) + + # sequencewise gru + gru_features, _ = self.rnn_layer(item_embs.flatten(1, 2)) + gru_features = gru_features.reshape(item_embs.shape) + + # concatenation + features = torch.cat([att_features, gru_features], dim=-1) + + return self.out_layer(features).squeeze(-1) + + +class SessionwiseTransformer(nn.Module): + """ + Just a large transformer on sequence of items. + """ + + def __init__(self, embedding, nheads=2, output_dim=1): + super().__init__() + self.nheads = nheads + self.embedding_dim = embedding.embedding_dim + self.embedding = embedding + self.attention = nn.MultiheadAttention( + 2 * self.embedding_dim, num_heads=nheads, batch_first=True + ) + self.out_layer = nn.Linear(2 * embedding.embedding_dim, output_dim) + + def forward(self, batch): + # getting embeddings & flatening them into one sequence + item_embs, user_embs = self.embedding(batch) + item_embs = stack_embeddings(user_embs, item_embs) + shp = item_embs.shape[:-1] + device = item_embs.device + slate_size = item_embs.size(-2) + session_length = item_embs.size(1) + batch_size = item_embs.size(0) + + # Tensor of shape (batch, session_length, slate_size) + # indicating which iteration this item belongs to + slate_num_for_item = torch.arange(session_length).to(device) + slate_num_for_item = slate_num_for_item[None, :, None].repeat( + batch_size, 1, slate_size + ) + + # Adding a dummy "zero item". It is required, pytorch + # attention implementation will fail if there are sequences + # with no keys in batch. We will drop out response on it later. + keys = item_embs.flatten(1, 2) + keys = add_zero_item(keys) + slate_num_for_item = slate_num_for_item.flatten(1, 2) + 1 + slate_num_for_item = add_zero_item(slate_num_for_item) + + # forbid attending to the padding `items` + key_padding_mask = batch["slates_mask"].flatten(1, 2).clone() + key_padding_mask = add_zero_item(~key_padding_mask) + + # forbid model looking into future (and into current iteraction) + attn_mask = [] + for i in range(batch_size): + slateno = slate_num_for_item[i] + clicked_slateno = slate_num_for_item[i] + mask = slateno[:, None] <= clicked_slateno[None, :] + mask[:, 0] = False # always can attend the 'zero item' + attn_mask.append(mask) + future_mask = nn.utils.rnn.pad_sequence( + attn_mask, batch_first=True, padding_value=True + ) + future_mask.to(device) + + # calculating the attention layer + features, attn_map = self.attention( + keys, + keys, + keys, + key_padding_mask=key_padding_mask, + attn_mask=future_mask.repeat_interleave(self.nheads, 0), + ) + + # removing artificial `zero item` + features = features[:, 1:, :] + keys = keys[:, 1:, :] + + return self.out_layer(features).reshape(shp).squeeze(-1) + + +class TransformerGRU(nn.Module): + """ + Two-stage model with attention layer operating on each slate independently, + and a session-wise GRU layer to handle the preference drift. + """ + + def __init__(self, embedding, nheads=2, output_dim=1): + super().__init__() + self.embedding_dim = embedding.embedding_dim + self.embedding = embedding + self.attention = nn.MultiheadAttention( + 2 * embedding.embedding_dim, num_heads=nheads, batch_first=True + ) + self.rnn_cell = nn.GRUCell( + input_size=2 * embedding.embedding_dim, + hidden_size=embedding.embedding_dim, + ) + self.out_layer = nn.Linear(2 * embedding.embedding_dim, output_dim) + + def get_attention_embeddings(self, item_embs, user_embs, slate_mask): + shp = item_embs.shape[:-1] + # reinterpret sequence dimension as batch dimension + features = stack_embeddings(user_embs, item_embs).flatten(0, 1) + key_padding_mask = slate_mask.flatten(0, 1) + # add an artificial item + features = add_zero_item(features) + key_padding_mask = add_zero_item(~key_padding_mask) + features, attn_map = self.attention( + features, features, features, key_padding_mask=key_padding_mask + ) + # drop the artificial item + features = features[:, 1:, ...] + features = features.reshape(shp + (self.embedding_dim * 2,)) + return features + + def forward(self, batch): + item_embs, user_embs = self.embedding(batch) + slate_mask = batch["slates_mask"].clone() + session_length = item_embs.shape[1] + + preds, hidden = [], user_embs[..., 0, :] + for slate_no in range(session_length): + # select current slate, run slatewise attention on it + att_features = self.get_attention_embeddings( + item_embs[..., slate_no, :, :].unsqueeze(-3), + hidden[..., None, :], + slate_mask[..., slate_no, :].unsqueeze(-3), + ) + # save the attention features + preds.append(att_features) + + # run GRU cell on aggregated attention features + hidden = self.rnn_cell(att_features.squeeze(-3).mean(-2), hidden) + preds = torch.cat(preds, dim=-3) + return self.out_layer(preds).squeeze(-1) diff --git a/sim4rec/response/nn_utils/slatewise.py b/sim4rec/response/nn_utils/slatewise.py new file mode 100755 index 0000000..eddfd15 --- /dev/null +++ b/sim4rec/response/nn_utils/slatewise.py @@ -0,0 +1,187 @@ +import torch + +# import absч +import torch.nn as nn +from .embeddings import stack_embeddings + + +class DotProduct(torch.nn.Module): + """ + Model whose prediction scoreis are just a dot product of user and item embeddings. + """ + + def __init__(self, embedding): + super().__init__() + self.embedding = embedding + + def forward(self, batch): + item_embs, user_embs = self.embedding(batch) + scores = item_embs * user_embs[:, :, None, :].repeat( + 1, 1, item_embs.size(-2), 1 + ) + scores = scores.sum(-1) + return scores + + +class LogisticRegression(torch.nn.Module): + """ + Logistic Regression run on a concatenation of the user's and the item's embedding. + """ + + def __init__(self, embedding, output_dim=1): + super().__init__() + self.embedding = embedding + self.linear = torch.nn.Linear(2 * embedding.embedding_dim, output_dim) + + def forward(self, batch): + item_embs, user_embs = self.embedding(batch) + features = stack_embeddings(user_embs, item_embs) + return self.linear(features).squeeze(-1) + + +class SlatewiseGRU(torch.nn.Module): + """ + GRU acting on each slate independently. + """ + + def __init__(self, embedding, dropout=0, output_dim=1): + super().__init__() + self.embedding = embedding + self.rnn_layer = torch.nn.GRU( + input_size=embedding.embedding_dim, + hidden_size=embedding.embedding_dim, + batch_first=True, + dropout=dropout, + ) + self.out_layer = torch.nn.Linear(embedding.embedding_dim, output_dim) + + def forward(self, batch): + item_embs, user_embs = self.embedding(batch) + # (batch_size, session_len, slate_size) + shp = item_embs.shape[:-1] + + # Reinterpreting session dim as an independent batch dim + # now it is (batch * session, slate, embedding) + item_embs = item_embs.flatten(0, 1) + hidden = user_embs.flatten(0, 1)[None, ...].contiguous() + rnn_out, _ = self.rnn_layer( + item_embs, + hidden, + ) + return self.out_layer(rnn_out).reshape(shp) + + +class SlatewiseTransformer(torch.nn.Module): + """ + Transformer acting on each slate independently. + """ + + def __init__(self, embedding, nheads=2, output_dim=1): + super().__init__() + self.embedding_dim = embedding.embedding_dim + self.embedding = embedding + self.attention = torch.nn.MultiheadAttention( + 2 * embedding.embedding_dim, num_heads=nheads, batch_first=True + ) + self.out_layer = torch.nn.Linear(2 * embedding.embedding_dim, output_dim) + + def forward(self, batch): + item_embs, user_embs = self.embedding(batch) + # (batch_size, session_len, slate_size) + shp = item_embs.shape[:-1] + + # Reinterpreting session dim as an independent batch dim + # now it is (batch * session, slate, embedding) + features = stack_embeddings(user_embs, item_embs) + features = features.flatten(0, 1) + + # adding a zero item + features = torch.cat([torch.zeros_like(features[..., :1, :]), features], dim=-2) + + # key padding mask forbids attending to padding items + key_padding_mask = batch["slates_mask"].flatten(0, 1) + key_padding_mask = torch.cat( + [torch.ones_like(key_padding_mask[..., :1]), key_padding_mask], dim=-1 + ) + + # evaluating attention layer + features, attn_map = self.attention( + features, features, features, key_padding_mask=~key_padding_mask + ) + + # removing zero item + features = features[..., 1:, :] + + out = self.out_layer(features) + out = out.reshape(shp).squeeze(-1) + return out + + +class NeuralClickModel(nn.Module): + def __init__(self, embedding, readout=None, gumbel_temperature=1.0): + """ + :param readout: can be one of None, 'soft' ,'threshold', 'sample' and 'diff_sample' + """ + super().__init__() + self.embedding = embedding + self.embedding_dim = embedding.embedding_dim + self.rnn_layer = nn.GRU( + input_size=self.embedding_dim * 2, + hidden_size=self.embedding_dim, + batch_first=True, + ) + self.out_layer = nn.Linear(self.embedding_dim, 1) + self.readout = readout + self.gumbel_temperature = gumbel_temperature + + def forward(self, batch, threshold=0.0): + item_embs, user_embs = self.embedding(batch) + shp = item_embs.shape + slate_size = item_embs.shape[2] + + # duplicate item emneddings (second part will be reweighted later) + items = torch.cat([item_embs.flatten(0, 1), item_embs.flatten(0, 1)], dim=-1) + h = user_embs.flatten(0, 1)[None, :, :] + clicks = torch.zeros_like(batch["responses"]).flatten(0, 1) + + if self.readout: + res = [] + # iterate over recommended items in slate + for i in range(slate_size): + output, h = self.rnn_layer(items[:, [i], :], h) + y = self.out_layer(output)[:, :, 0] + if i + 1 == slate_size: + res.append(y) + break + # update the last half of item embedding + if self.readout == "threshold": + # hard readout + clicks = (y.detach()[:, :, None] > threshold).to(torch.float32) + items[:, [i + 1], self.embedding_dim :] *= clicks + elif self.readout == "soft": + # soft readout, each item is added with weight equal to predicted click proba + items[:, [i + 1], self.embedding_dim :] *= torch.sigmoid(y)[ + :, :, None + ] + elif self.readout == "diff_sample" or self.readout_mode == "sample": + # gumbel-softmax trick + eps = 1e-8 # to avoid numerical instability + gumbel_sample = (torch.rand_like(y) + eps).log() + gumbel_sample /= (torch.rand_like(y) + eps).log() + eps + gumbel_sample = gumbel_sample.log() + gumbel_sample *= -1 + bernoulli_sample = torch.sigmoid( + (nn.LogSigmoid()(y) + gumbel_sample) / self.gumbel_temperature + ) + if self.readout == "sample": + bernoulli_sample = bernoulli_sample.detach() + items[:, i + 1, self.embedding_dim :] *= bernoulli_sample + else: + raise NotImplementedError + res.append(y) + y = torch.cat(res, axis=1) + else: + items[:, 1:, self.embedding_dim :] *= clicks[:, :-1, None] + rnn_out, _ = self.rnn_layer(items, h) + y = self.out_layer(rnn_out)[:, :, 0] + return y.reshape(shp[:-1]) diff --git a/sim4rec/response/nn_utils/utils.py b/sim4rec/response/nn_utils/utils.py new file mode 100644 index 0000000..27d0afe --- /dev/null +++ b/sim4rec/response/nn_utils/utils.py @@ -0,0 +1,231 @@ +import torch +import numpy as np + +from collections import Counter +from copy import deepcopy +from itertools import chain +from torch.utils.data import DataLoader, SubsetRandomSampler, BatchSampler + + +def pad_slates(arr, slate_size, padding_value): + return np.pad( + arr, + ((0, 0), (0, slate_size - arr.shape[1])), + mode="constant", + constant_values=padding_value, + ) + + +def collate_rec_data(batch: list, padding_value=0): + """ + Batch sessions of varying length: pad sequiences, prepare masks, etc. + :param list bacth: list of data points (usually obtained from torch sampler). + :param padding_value: value representing padding items/users. + :return dict batch: bathed data in torch.tensor format. + """ + + # lengths + batch_lengths = [b["length"] for b in batch] + batch_lengths = torch.tensor(np.stack(batch_lengths), dtype=torch.long) + + # max_sequence_len = max([b["length"] for b in batch]) + max_slate_size = max([b["slate_size"] for b in batch]) + + # if data contain slates of various length, + # pad the slates before paddings the sequences + for b in batch: + if b["slate_size"] < max_slate_size: + b["item_indexes"] = pad_slates( + b["item_indexes"], max_slate_size, padding_value + ) + b["slates_mask"] = pad_slates( + b["slates_mask"], max_slate_size, padding_value + ) + b["responses"] = pad_slates(b["responses"], max_slate_size, padding_value) + b["timestamps"] = pad_slates(b["timestamps"], max_slate_size, padding_value) + + # user indexes + user_indexes = torch.tensor([b["user_index"] for b in batch], dtype=torch.long) + + # item indexes + # shape: batch_size, max_sequence_len, max_slate_size + item_indexes = [torch.tensor(b["item_indexes"], dtype=torch.long) for b in batch] + item_indexes = torch.nn.utils.rnn.pad_sequence( + item_indexes, padding_value=padding_value, batch_first=True + ) + + # item mask: True for recommended items, False for paddings + # (both sequence padding and slate padding) + # shape: batch_size, max_sequence_len, max_slate_size + slate_masks = [torch.tensor(b["slates_mask"], dtype=torch.bool) for b in batch] + slate_masks = torch.nn.utils.rnn.pad_sequence( + slate_masks, padding_value=False, batch_first=True + ) + + # responses: number of clicks per recommended item + # shape: batch_size, max_sequence_len, max_slate_size + responses = [torch.tensor(b["responses"], dtype=torch.long) for b in batch] + responses = torch.nn.utils.rnn.pad_sequence( + responses, padding_value=padding_value, batch_first=True + ) + + # timestamps: we assume that (user_id, timestamp) is an unique + # identifier of slate, hence we need to pass it through model + # for further decoding model outputs + # shape: batch_size, max_sequence_len, max_slate_size + timestamps = [torch.tensor(b["timestamps"], dtype=torch.int) for b in batch] + timestamps = torch.nn.utils.rnn.pad_sequence( + timestamps, padding_value=padding_value, batch_first=True + ) + batch = { + "item_indexes": item_indexes, # recommended items indexes + "slates_mask": slate_masks, # batch mask, True for non-padding items + "responses": responses, # number of clicks for each item + "timestamps": timestamps, # interaction timestamp + "length": batch_lengths, # lenghts of each session in batch + "user_indexes": user_indexes, # indexes of users + "out_mask": slate_masks, # a mask for train-time metric computation + } + return batch + + +def concat_batch(left, right): + """ + Concatenate two batches (before collating). + In_lengths are summed. + Recommendation_idx are concatenated. + """ + sessionwise_fields = [ + "item_indexes", + "slates_mask", + "responses", + "timestamps", + "user_indexes", + "recommendation_idx", + ] + assert len(left) == len(right) + left_length = len(left) + + newbatch = deepcopy(left) + for i in range(left_length): + for key in sessionwise_fields: + if left[i][key] is None: + continue + newbatch[i][key] = np.concatenate([left[i][key], right[i][key]], axis=0) + newbatch[i]["in_length"] += right[i]["in_length"] + newbatch[i]["length"] += right[i]["length"] + return newbatch + + +def create_loader(dataset, batch_size, **kwargs): + """Creates dataloader for recommendation dataset""" + return DataLoader( + dataset, + sampler=BatchSampler( + SubsetRandomSampler(dataset.users), batch_size, drop_last=False + ), + batch_size=None, + collate_fn=collate_rec_data, + **kwargs, + ) + + +class Indexer: + """ + Handles mappings between some indexes and ids. + Padding tokens will always have index 0, and + unknown (rare) tokens will have index 1. + """ + + def __init__(self, pad_id=-1, unk_id=-2, **kwargs): + """ + :param pad_id: - Id for padding item. + :param unk_id: - Id for unknown item. + """ + self.unk_id = unk_id + self.pad_id = pad_id + self._index2id = [pad_id, unk_id] + # unknown indexes are mapped to dedicated 'unknown id' index + self._id2index = {pad_id: 0, unk_id: 1} + self._counter = Counter() + + def to_dict(self): + """Raw id -> index mapping""" + return self._id2index.copy() + + @property + def n_objs(self): + """Number of indexed IDs""" + return len(self._index2id) + + def is_known_id(self, id_): + return (id_ in self._id2index) and (id_ != self.unk_id) + + @classmethod + def from_dict(cls, d): + """ + Creates indexer from dictionary. + + :param d: id-to-index dictionary to update from + """ + assert set(d.values()) == set(range(len(d))), "Given dict values aren't indexes" + pad, unk = None, None + for key, value in d.items(): + if value == 0: + pad = key + if value == 1: + unk = key + indexer = cls(pad_id=pad, unk_id=unk) + indexer._id2index.update(d) + indexer._index2id = [0 for i in d] + for key, value in d.items(): + indexer._index2id[value] = key + return indexer + + def update_from_iter(self, iterable, min_occurrences=1): + """ + Builds mapping from given iterable. + If called several times, updates the mapping with new values, + preserving already allocated indexes. Note, that frequencies + are preserved between calls. + + :param iterable: some collection with data. + :param min_occurrences: frequency threshold for rare ids. + """ + self._counter.update(iterable) + + known_ids = set(self._index2id) + new_ids = [ + key + for key in self._counter + if self._counter[key] >= min_occurrences and key not in known_ids + ] + last_index = len(self._index2id) + self._index2id += new_ids + self._id2index.update( + {key: index + last_index for (index, key) in enumerate(new_ids)} + ) + + return self + + def index_np(self, arr: np.array): + """ + Transforms given array of IDs into array of indexes. + Previously unseen IDs will be silently replaces with unk. + + :param arr: numpy array of ID's. + :return: numpy array of the same shape filled with indexes. + """ + unk_index = self._id2index[self.unk_id] + vfunc = np.vectorize(lambda x: self._id2index.get(x, unk_index)) + return vfunc(arr) + + def get_id(self, arr: np.array): + """ + Transforms given array of indexes back into array of IDs. + Throws an exception if found incorrect index. + + :param arr: numpy array of indexes's. + :return: numpy array of the same shape filled with ids. + """ + return np.vectorize(lambda x: self._index2id[x])(arr)