Merge branch 'main' into bobik/hset_vector_crash_fix

This commit is contained in:
Volodymyr Yavdoshenko 2025-04-27 14:13:15 +03:00 committed by GitHub
commit d9233c81a9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 633 additions and 425 deletions

View file

@ -140,7 +140,7 @@ jobs:
echo "disk space is:"
df -h
- name: C++ Unit Tests
- name: C++ Unit Tests - IoUring
run: |
cd ${GITHUB_WORKSPACE}/build
echo Run ctest -V -L DFLY
@ -151,7 +151,14 @@ jobs:
# Run allocation tracker test separately without alsologtostderr because it generates a TON of logs.
FLAGS_fiber_safety_margin=4096 timeout 5m ./allocation_tracker_test
echo "Running tests with --force_epoll"
timeout 5m ./dragonfly_test
timeout 5m ./json_family_test --jsonpathv2=false
timeout 5m ./tiered_storage_test --vmodule=db_slice=2 --logtostderr
- name: C++ Unit Tests - Epoll
run: |
cd ${GITHUB_WORKSPACE}/build
# Create a rule that automatically prints stacktrace upon segfault
cat > ./init.gdb <<EOF
@ -166,18 +173,14 @@ jobs:
FLAGS_fiber_safety_margin=4096 FLAGS_force_epoll=true timeout 5m ./allocation_tracker_test
echo "Finished running tests with --force_epoll"
echo "Running tests with --cluster_mode=emulated"
- name: C++ Unit Tests - IoUring with cluster mode
run: |
FLAGS_fiber_safety_margin=4096 FLAGS_cluster_mode=emulated timeout 20m ctest -V -L DFLY
echo "Running tests with both --cluster_mode=emulated & --lock_on_hashtags"
- name: C++ Unit Tests - IoUring with cluster mode and FLAGS_lock_on_hashtags
run: |
FLAGS_fiber_safety_margin=4096 FLAGS_cluster_mode=emulated FLAGS_lock_on_hashtags=true timeout 20m ctest -V -L DFLY
timeout 5m ./dragonfly_test
timeout 5m ./json_family_test --jsonpathv2=false
timeout 5m ./tiered_storage_test --vmodule=db_slice=2 --logtostderr
- name: Upload unit logs on failure
if: failure()
uses: actions/upload-artifact@v4

View file

@ -24,16 +24,6 @@ jobs:
permissions:
pull-requests: write
checks: read
# services:
# redis:
# image: docker.dragonflydb.io/dragonflydb/dragonfly:${{ matrix.DRAGONFLY_VERSION }}
# ports:
# - 6380:6379
# options: >-
# --health-cmd "redis-cli ping"
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
steps:
- uses: actions/checkout@v4
@ -69,14 +59,8 @@ jobs:
- name: Run tests
working-directory: tests/fakeredis
run: |
poetry run pytest test/ \
--ignore test/test_hypothesis/test_hash.py \
--ignore test/test_hypothesis/test_set.py \
--ignore test/test_hypothesis/test_zset.py \
--ignore test/test_hypothesis/test_joint.py \
--ignore test/test_hypothesis/test_transaction.py \
--ignore test/test_mixins/test_bitmap_commands.py \
--junit-xml=results-tests.xml --html=report-tests.html -v
poetry run pytest -s test/ \
--junit-xml=results-tests.xml --html=report-tests.html -v
continue-on-error: true # For now to mark the flow as successful
- name: Show Dragonfly stats

View file

@ -82,14 +82,12 @@ typedef struct Header {
} Header;
static int getnum (lua_State *L, const char **fmt, int df) {
static int getnum (const char **fmt, int df) {
if (!isdigit(**fmt)) /* no number? */
return df; /* return default value */
else {
int a = 0;
do {
if (a > (INT_MAX / 10) || a * 10 > (INT_MAX - (**fmt - '0')))
luaL_error(L, "integral size overflow");
a = a*10 + *((*fmt)++) - '0';
} while (isdigit(**fmt));
return a;
@ -110,9 +108,9 @@ static size_t optsize (lua_State *L, char opt, const char **fmt) {
case 'f': return sizeof(float);
case 'd': return sizeof(double);
case 'x': return 1;
case 'c': return getnum(L, fmt, 1);
case 'c': return getnum(fmt, 1);
case 'i': case 'I': {
int sz = getnum(L, fmt, sizeof(int));
int sz = getnum(fmt, sizeof(int));
if (sz > MAXINTSIZE)
luaL_error(L, "integral size %d is larger than limit of %d",
sz, MAXINTSIZE);
@ -145,7 +143,7 @@ static void controloptions (lua_State *L, int opt, const char **fmt,
case '>': h->endian = BIG; return;
case '<': h->endian = LITTLE; return;
case '!': {
int a = getnum(L, fmt, MAXALIGN);
int a = getnum(fmt, MAXALIGN);
if (!isp2(a))
luaL_error(L, "alignment %d is not a power of 2", a);
h->align = a;

View file

@ -17,6 +17,10 @@ ABSL_FLAG(string, cluster_mode, "",
"Cluster mode supported. Possible values are "
"'emulated', 'yes' or ''");
ABSL_FLAG(bool, experimental_cluster_shard_by_slot, false,
"If true, cluster mode is enabled and sharding is done by slot. "
"Otherwise, sharding is done by hash tag.");
namespace dfly {
void UniqueSlotChecker::Add(std::string_view key) {
@ -43,16 +47,13 @@ optional<SlotId> UniqueSlotChecker::GetUniqueSlotId() const {
return slot_id_ > kMaxSlotNum ? optional<SlotId>() : slot_id_;
}
namespace {
enum class ClusterMode {
kUninitialized,
kNoCluster,
kEmulatedCluster,
kRealCluster,
};
namespace detail {
ClusterMode cluster_mode = ClusterMode::kUninitialized;
} // namespace
bool cluster_shard_by_slot = false;
} // namespace detail
using namespace detail;
void InitializeCluster() {
string cluster_mode_str = absl::GetFlag(FLAGS_cluster_mode);
@ -67,14 +68,10 @@ void InitializeCluster() {
LOG(ERROR) << "Invalid value for flag --cluster_mode. Exiting...";
exit(1);
}
}
bool IsClusterEnabled() {
return cluster_mode == ClusterMode::kRealCluster;
}
bool IsClusterEmulated() {
return cluster_mode == ClusterMode::kEmulatedCluster;
if (cluster_mode != ClusterMode::kNoCluster) {
cluster_shard_by_slot = absl::GetFlag(FLAGS_experimental_cluster_shard_by_slot);
}
}
SlotId KeySlot(std::string_view key) {
@ -82,10 +79,6 @@ SlotId KeySlot(std::string_view key) {
return crc16(tag.data(), tag.length()) & kMaxSlotNum;
}
bool IsClusterEnabledOrEmulated() {
return IsClusterEnabled() || IsClusterEmulated();
}
bool IsClusterShardedByTag() {
return IsClusterEnabledOrEmulated() || LockTagOptions::instance().enabled;
}

View file

@ -10,6 +10,20 @@
namespace dfly {
namespace detail {
enum class ClusterMode {
kUninitialized,
kNoCluster,
kEmulatedCluster,
kRealCluster,
};
extern ClusterMode cluster_mode;
extern bool cluster_shard_by_slot;
}; // namespace detail
using SlotId = std::uint16_t;
constexpr SlotId kMaxSlotNum = 0x3FFF;
@ -42,9 +56,23 @@ class UniqueSlotChecker {
SlotId KeySlot(std::string_view key);
void InitializeCluster();
bool IsClusterEnabled();
bool IsClusterEmulated();
bool IsClusterEnabledOrEmulated();
inline bool IsClusterEnabled() {
return detail::cluster_mode == detail::ClusterMode::kRealCluster;
}
inline bool IsClusterEmulated() {
return detail::cluster_mode == detail::ClusterMode::kEmulatedCluster;
}
inline bool IsClusterEnabledOrEmulated() {
return IsClusterEnabled() || IsClusterEmulated();
}
inline bool IsClusterShardedBySlot() {
return detail::cluster_shard_by_slot;
}
bool IsClusterShardedByTag();
} // namespace dfly

View file

@ -28,6 +28,7 @@ extern "C" {
#include "core/sorted_map.h"
#include "core/string_map.h"
#include "core/string_set.h"
#include "facade/cmd_arg_parser.h"
#include "server/blocking_controller.h"
#include "server/container_utils.h"
#include "server/engine_shard_set.h"
@ -38,7 +39,6 @@ extern "C" {
#include "server/server_state.h"
#include "server/string_family.h"
#include "server/transaction.h"
using namespace std;
ABSL_DECLARE_FLAG(string, dir);
@ -712,106 +712,57 @@ void DebugCmd::Migration(CmdArgList args, facade::SinkReplyBuilder* builder) {
return builder->SendError(UnknownSubCmd("MIGRATION", "DEBUG"));
}
enum PopulateFlag { FLAG_RAND, FLAG_TYPE, FLAG_ELEMENTS, FLAG_SLOT, FLAG_EXPIRE, FLAG_UNKNOWN };
// Populate arguments format:
// required: (total count) (key prefix) (val size)
// optional: [RAND | TYPE typename | ELEMENTS element num | SLOTS (key value)+ | EXPIRE start end]
optional<DebugCmd::PopulateOptions> DebugCmd::ParsePopulateArgs(CmdArgList args,
facade::SinkReplyBuilder* builder) {
if (args.size() < 2) {
builder->SendError(UnknownSubCmd("populate", "DEBUG"));
return nullopt;
}
CmdArgParser parser(args.subspan(1));
PopulateOptions options;
if (!absl::SimpleAtoi(ArgS(args, 1), &options.total_count)) {
builder->SendError(kUintErr);
options.total_count = parser.Next<uint64_t>();
options.prefix = parser.NextOrDefault<string_view>("key");
options.val_size = parser.NextOrDefault<uint32_t>(16);
while (parser.HasNext()) {
PopulateFlag flag = parser.MapNext("RAND", FLAG_RAND, "TYPE", FLAG_TYPE, "ELEMENTS",
FLAG_ELEMENTS, "SLOTS", FLAG_SLOT, "EXPIRE", FLAG_EXPIRE);
switch (flag) {
case FLAG_RAND:
options.populate_random_values = true;
break;
case FLAG_TYPE:
options.type = absl::AsciiStrToUpper(parser.Next<string_view>());
break;
case FLAG_ELEMENTS:
options.elements = parser.Next<uint32_t>();
break;
case FLAG_SLOT: {
auto [start, end] = parser.Next<FInt<0, 16383>, FInt<0, 16383>>();
options.slot_range = cluster::SlotRange{SlotId(start), SlotId(end)};
break;
}
case FLAG_EXPIRE: {
auto [min_ttl, max_ttl] = parser.Next<uint32_t, uint32_t>();
if (min_ttl >= max_ttl) {
builder->SendError(kExpiryOutOfRange);
(void)parser.Error();
return nullopt;
}
options.expire_ttl_range = std::make_pair(min_ttl, max_ttl);
break;
}
default:
LOG(FATAL) << "Unexpected flag in PopulateArgs. Args: " << args;
break;
}
}
if (parser.HasError()) {
builder->SendError(parser.Error()->MakeReply());
return nullopt;
}
if (args.size() > 2) {
options.prefix = ArgS(args, 2);
}
if (args.size() > 3) {
if (!absl::SimpleAtoi(ArgS(args, 3), &options.val_size)) {
builder->SendError(kUintErr);
return nullopt;
}
}
for (size_t index = 4; args.size() > index; ++index) {
string str = absl::AsciiStrToUpper(ArgS(args, index));
if (str == "RAND") {
options.populate_random_values = true;
} else if (str == "TYPE") {
if (args.size() < index + 2) {
builder->SendError(kSyntaxErr);
return nullopt;
}
++index;
options.type = absl::AsciiStrToUpper(ArgS(args, index));
} else if (str == "ELEMENTS") {
if (args.size() < index + 2) {
builder->SendError(kSyntaxErr);
return nullopt;
}
if (!absl::SimpleAtoi(ArgS(args, ++index), &options.elements)) {
builder->SendError(kSyntaxErr);
return nullopt;
}
} else if (str == "SLOTS") {
if (args.size() < index + 3) {
builder->SendError(kSyntaxErr);
return nullopt;
}
auto parse_slot = [](string_view slot_str) -> OpResult<uint32_t> {
uint32_t slot_id;
if (!absl::SimpleAtoi(slot_str, &slot_id)) {
return facade::OpStatus::INVALID_INT;
}
if (slot_id > kMaxSlotNum) {
return facade::OpStatus::INVALID_VALUE;
}
return slot_id;
};
auto start = parse_slot(ArgS(args, ++index));
if (start.status() != facade::OpStatus::OK) {
builder->SendError(start.status());
return nullopt;
}
auto end = parse_slot(ArgS(args, ++index));
if (end.status() != facade::OpStatus::OK) {
builder->SendError(end.status());
return nullopt;
}
options.slot_range = cluster::SlotRange{.start = static_cast<SlotId>(start.value()),
.end = static_cast<SlotId>(end.value())};
} else if (str == "EXPIRE") {
if (args.size() < index + 3) {
builder->SendError(kSyntaxErr);
return nullopt;
}
uint32_t start, end;
if (!absl::SimpleAtoi(ArgS(args, ++index), &start)) {
builder->SendError(kSyntaxErr);
return nullopt;
}
if (!absl::SimpleAtoi(ArgS(args, ++index), &end)) {
builder->SendError(kSyntaxErr);
return nullopt;
}
if (start >= end) {
builder->SendError(kExpiryOutOfRange);
return nullopt;
}
options.expire_ttl_range = std::make_pair(start, end);
} else {
builder->SendError(kSyntaxErr);
return nullopt;
}
}
return options;
}

View file

@ -261,6 +261,17 @@ __thread EngineShard* EngineShard::shard_ = nullptr;
uint64_t TEST_current_time_ms = 0;
ShardId Shard(string_view v, ShardId shard_num) {
// This cluster sharding is not necessary and may degrade keys distribution among shard threads.
// For example, if we have 3 shards, then no single-char keys will be assigned to shard 2 and
// 32 single char keys in range ['_' - '~'] will be assigned to shard 0.
// Yes, SlotId function does not have great distribution properties.
// On the other side, slot based sharding may help with pipeline squashing optimizations,
// because they rely on commands being single-sharded.
// TODO: once we improve our squashing logic, we can remove this.
if (IsClusterShardedBySlot()) {
return KeySlot(v) % shard_num;
}
if (IsClusterShardedByTag()) {
v = LockTagOptions::instance().Tag(v);
}

View file

@ -644,13 +644,14 @@ void OpScan(const OpArgs& op_args, const ScanOpts& scan_opts, uint64_t* cursor,
PrimeTable::Cursor cur = *cursor;
auto [prime_table, expire_table] = db_slice.GetTables(op_args.db_cntx.db_index);
size_t buckets_iterated = 0;
// 10k Traverses
const size_t limit = 10000;
const auto start = absl::Now();
// Don't allow it to monopolize cpu time.
const absl::Duration timeout = absl::Milliseconds(10);
do {
cur = prime_table->Traverse(
cur, [&](PrimeIterator it) { cnt += ScanCb(op_args, it, scan_opts, vec); });
} while (cur && cnt < scan_opts.limit && buckets_iterated++ < limit);
} while (cur && cnt < scan_opts.limit && (absl::Now() - start) < timeout);
VLOG(1) << "OpScan " << db_slice.shard_id() << " cursor: " << cur.value();
*cursor = cur.value();

View file

@ -255,14 +255,14 @@ OpResult<int> PFMergeInternal(CmdArgList args, Transaction* tx, SinkReplyBuilder
if (result.ok()) {
hlls[sid] = std::move(result.value());
} else {
success = false;
success.store(false, memory_order_relaxed);
}
return result.status();
return OpStatus::OK;
};
tx->Execute(std::move(cb), false);
if (!success) {
if (!success.load(memory_order_relaxed)) {
tx->Conclude();
return OpStatus::INVALID_VALUE;
}

View file

@ -194,9 +194,12 @@ TEST_F(HllFamilyTest, MergeOverlapping) {
}
TEST_F(HllFamilyTest, MergeInvalid) {
Run({"exists", "key1", "key4"});
ASSERT_EQ(GetDebugInfo().shards_count, 2); // ensure 2 shards
EXPECT_EQ(CheckedInt({"pfadd", "key1", "1", "2", "3"}), 1);
EXPECT_EQ(Run({"set", "key2", "..."}), "OK");
EXPECT_THAT(Run({"pfmerge", "key1", "key2"}), ErrArg(HllFamily::kInvalidHllErr));
EXPECT_EQ(Run({"set", "key4", "..."}), "OK");
EXPECT_THAT(Run({"pfmerge", "key1", "key4"}), ErrArg(HllFamily::kInvalidHllErr));
EXPECT_EQ(CheckedInt({"pfcount", "key1"}), 3);
}

View file

@ -1271,10 +1271,23 @@ void HSetFamily::HRandField(CmdArgList args, const CommandContext& cmd_cntx) {
auto* rb = static_cast<RedisReplyBuilder*>(cmd_cntx.rb);
OpResult<StringVec> result = cmd_cntx.tx->ScheduleSingleHopT(std::move(cb));
if (result) {
if ((result->size() == 1) && (args.size() == 1))
if (result->size() == 1 && args.size() == 1)
rb->SendBulkString(result->front());
else
rb->SendBulkStrArr(*result, facade::RedisReplyBuilder::ARRAY);
else if (with_values) {
const auto result_size = result->size();
DCHECK(result_size % 2 == 0)
<< "unexpected size of strings " << result_size << ", expected pairs";
SinkReplyBuilder::ReplyScope scope{rb};
const bool is_resp3 = rb->IsResp3();
rb->StartArray(is_resp3 ? result_size / 2 : result_size);
for (size_t i = 0; i < result_size; i += 2) {
if (is_resp3)
rb->StartArray(2);
rb->SendBulkString((*result)[i]);
rb->SendBulkString((*result)[i + 1]);
}
} else
rb->SendBulkStrArr(*result, RedisReplyBuilder::ARRAY);
} else if (result.status() == OpStatus::KEY_NOTFOUND) {
if (args.size() == 1)
rb->SendNull();

View file

@ -541,4 +541,31 @@ TEST_F(HSetFamilyTest, KeyRemovedWhenEmpty) {
test_cmd([&] { EXPECT_THAT(Run({"HSTRLEN", "a", "afield"}), IntArg(0)); }, "HSTRLEN");
}
TEST_F(HSetFamilyTest, HRandFieldRespFormat) {
absl::flat_hash_map<std::string, std::string> expected{
{"a", "1"},
{"b", "2"},
{"c", "3"},
};
Run({"HELLO", "3"});
EXPECT_THAT(Run({"HSET", "key", "a", "1", "b", "2", "c", "3"}), IntArg(3));
auto resp = Run({"HRANDFIELD", "key", "3", "WITHVALUES"});
EXPECT_THAT(resp, ArrLen(3));
for (const auto& v : resp.GetVec()) {
EXPECT_THAT(v, ArrLen(2));
const auto& kv = v.GetVec();
EXPECT_THAT(kv[0], AnyOf("a", "b", "c"));
EXPECT_THAT(kv[1], expected[kv[0].GetView()]);
}
Run({"HELLO", "2"});
resp = Run({"HRANDFIELD", "key", "3", "WITHVALUES"});
EXPECT_THAT(resp, ArrLen(6));
const auto& vec = resp.GetVec();
for (size_t i = 0; i < vec.size(); i += 2) {
EXPECT_THAT(vec[i], AnyOf("a", "b", "c"));
EXPECT_THAT(vec[i + 1], expected[vec[i].GetView()]);
}
}
} // namespace dfly

View file

@ -130,6 +130,9 @@ ABSL_FLAG(bool, info_replication_valkey_compatible, true,
ABSL_FLAG(bool, managed_service_info, false,
"Hides some implementation details from users when true (i.e. in managed service env)");
ABSL_FLAG(string, availability_zone, "",
"server availability zone, used by clients to read from local-zone replicas");
ABSL_DECLARE_FLAG(int32_t, port);
ABSL_DECLARE_FLAG(bool, cache_mode);
ABSL_DECLARE_FLAG(uint32_t, hz);
@ -2378,6 +2381,12 @@ string ServerFamily::FormatInfoMetrics(const Metrics& m, std::string_view sectio
append("multiplexing_api", multiplex_api);
append("tcp_port", GetFlag(FLAGS_port));
// Add availability_zone if it's not empty
const auto& az = GetFlag(FLAGS_availability_zone);
if (!az.empty()) {
append("availability_zone", az);
}
uint64_t uptime = time(NULL) - start_time_;
append("uptime_in_seconds", uptime);
append("uptime_in_days", uptime / (3600 * 24));
@ -2882,8 +2891,12 @@ void ServerFamily::Hello(CmdArgList args, const CommandContext& cmd_cntx) {
rb->SetRespVersion(RespVersion::kResp2);
}
// Define number of fields in the response - add availability_zone if flag is not empty
const auto& az = GetFlag(FLAGS_availability_zone);
const int fields_count = az.empty() ? 7 : 8;
SinkReplyBuilder::ReplyAggregator agg(rb);
rb->StartCollection(7, RedisReplyBuilder::MAP);
rb->StartCollection(fields_count, RedisReplyBuilder::MAP);
rb->SendBulkString("server");
rb->SendBulkString("redis");
rb->SendBulkString("version");
@ -2898,6 +2911,12 @@ void ServerFamily::Hello(CmdArgList args, const CommandContext& cmd_cntx) {
rb->SendBulkString(GetRedisMode());
rb->SendBulkString("role");
rb->SendBulkString((*ServerState::tlocal()).is_master ? "master" : "slave");
// Add availability_zone to the response if flag is explicitly set and not empty
if (!az.empty()) {
rb->SendBulkString("availability_zone");
rb->SendBulkString(az);
}
}
void ServerFamily::AddReplicaOf(CmdArgList args, const CommandContext& cmd_cntx) {

View file

@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
[[package]]
name = "async-timeout"
@ -6,6 +6,8 @@ version = "5.0.1"
description = "Timeout context manager for asyncio programs"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_full_version < \"3.11.3\""
files = [
{file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"},
{file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"},
@ -13,22 +15,23 @@ files = [
[[package]]
name = "attrs"
version = "24.3.0"
version = "25.3.0"
description = "Classes Without Boilerplate"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "attrs-24.3.0-py3-none-any.whl", hash = "sha256:ac96cd038792094f438ad1f6ff80837353805ac950cd2aa0e0625ef19850c308"},
{file = "attrs-24.3.0.tar.gz", hash = "sha256:8f5c07333d543103541ba7be0e2ce16eeee8130cb0b3f9238ab904ce1e85baff"},
{file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"},
{file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"},
]
[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-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"]
benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"]
tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""]
[[package]]
name = "colorama"
@ -36,6 +39,8 @@ version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["main"]
markers = "sys_platform == \"win32\""
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
@ -43,80 +48,82 @@ files = [
[[package]]
name = "coverage"
version = "7.6.10"
version = "7.8.0"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "coverage-7.6.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5c912978f7fbf47ef99cec50c4401340436d200d41d714c7a4766f377c5b7b78"},
{file = "coverage-7.6.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a01ec4af7dfeb96ff0078ad9a48810bb0cc8abcb0115180c6013a6b26237626c"},
{file = "coverage-7.6.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3b204c11e2b2d883946fe1d97f89403aa1811df28ce0447439178cc7463448a"},
{file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32ee6d8491fcfc82652a37109f69dee9a830e9379166cb73c16d8dc5c2915165"},
{file = "coverage-7.6.10-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675cefc4c06e3b4c876b85bfb7c59c5e2218167bbd4da5075cbe3b5790a28988"},
{file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f4f620668dbc6f5e909a0946a877310fb3d57aea8198bde792aae369ee1c23b5"},
{file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4eea95ef275de7abaef630c9b2c002ffbc01918b726a39f5a4353916ec72d2f3"},
{file = "coverage-7.6.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e2f0280519e42b0a17550072861e0bc8a80a0870de260f9796157d3fca2733c5"},
{file = "coverage-7.6.10-cp310-cp310-win32.whl", hash = "sha256:bc67deb76bc3717f22e765ab3e07ee9c7a5e26b9019ca19a3b063d9f4b874244"},
{file = "coverage-7.6.10-cp310-cp310-win_amd64.whl", hash = "sha256:0f460286cb94036455e703c66988851d970fdfd8acc2a1122ab7f4f904e4029e"},
{file = "coverage-7.6.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ea3c8f04b3e4af80e17bab607c386a830ffc2fb88a5484e1df756478cf70d1d3"},
{file = "coverage-7.6.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:507a20fc863cae1d5720797761b42d2d87a04b3e5aeb682ef3b7332e90598f43"},
{file = "coverage-7.6.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37a84878285b903c0fe21ac8794c6dab58150e9359f1aaebbeddd6412d53132"},
{file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a534738b47b0de1995f85f582d983d94031dffb48ab86c95bdf88dc62212142f"},
{file = "coverage-7.6.10-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d7a2bf79378d8fb8afaa994f91bfd8215134f8631d27eba3e0e2c13546ce994"},
{file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6713ba4b4ebc330f3def51df1d5d38fad60b66720948112f114968feb52d3f99"},
{file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ab32947f481f7e8c763fa2c92fd9f44eeb143e7610c4ca9ecd6a36adab4081bd"},
{file = "coverage-7.6.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7bbd8c8f1b115b892e34ba66a097b915d3871db7ce0e6b9901f462ff3a975377"},
{file = "coverage-7.6.10-cp311-cp311-win32.whl", hash = "sha256:299e91b274c5c9cdb64cbdf1b3e4a8fe538a7a86acdd08fae52301b28ba297f8"},
{file = "coverage-7.6.10-cp311-cp311-win_amd64.whl", hash = "sha256:489a01f94aa581dbd961f306e37d75d4ba16104bbfa2b0edb21d29b73be83609"},
{file = "coverage-7.6.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:27c6e64726b307782fa5cbe531e7647aee385a29b2107cd87ba7c0105a5d3853"},
{file = "coverage-7.6.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c56e097019e72c373bae32d946ecf9858fda841e48d82df7e81c63ac25554078"},
{file = "coverage-7.6.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7827a5bc7bdb197b9e066cdf650b2887597ad124dd99777332776f7b7c7d0d0"},
{file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204a8238afe787323a8b47d8be4df89772d5c1e4651b9ffa808552bdf20e1d50"},
{file = "coverage-7.6.10-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67926f51821b8e9deb6426ff3164870976fe414d033ad90ea75e7ed0c2e5022"},
{file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e78b270eadb5702938c3dbe9367f878249b5ef9a2fcc5360ac7bff694310d17b"},
{file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:714f942b9c15c3a7a5fe6876ce30af831c2ad4ce902410b7466b662358c852c0"},
{file = "coverage-7.6.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:abb02e2f5a3187b2ac4cd46b8ced85a0858230b577ccb2c62c81482ca7d18852"},
{file = "coverage-7.6.10-cp312-cp312-win32.whl", hash = "sha256:55b201b97286cf61f5e76063f9e2a1d8d2972fc2fcfd2c1272530172fd28c359"},
{file = "coverage-7.6.10-cp312-cp312-win_amd64.whl", hash = "sha256:e4ae5ac5e0d1e4edfc9b4b57b4cbecd5bc266a6915c500f358817a8496739247"},
{file = "coverage-7.6.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05fca8ba6a87aabdd2d30d0b6c838b50510b56cdcfc604d40760dae7153b73d9"},
{file = "coverage-7.6.10-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e80eba8801c386f72e0712a0453431259c45c3249f0009aff537a517b52942b"},
{file = "coverage-7.6.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a372c89c939d57abe09e08c0578c1d212e7a678135d53aa16eec4430adc5e690"},
{file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ec22b5e7fe7a0fa8509181c4aac1db48f3dd4d3a566131b313d1efc102892c18"},
{file = "coverage-7.6.10-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26bcf5c4df41cad1b19c84af71c22cbc9ea9a547fc973f1f2cc9a290002c8b3c"},
{file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e4630c26b6084c9b3cb53b15bd488f30ceb50b73c35c5ad7871b869cb7365fd"},
{file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2396e8116db77789f819d2bc8a7e200232b7a282c66e0ae2d2cd84581a89757e"},
{file = "coverage-7.6.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:79109c70cc0882e4d2d002fe69a24aa504dec0cc17169b3c7f41a1d341a73694"},
{file = "coverage-7.6.10-cp313-cp313-win32.whl", hash = "sha256:9e1747bab246d6ff2c4f28b4d186b205adced9f7bd9dc362051cc37c4a0c7bd6"},
{file = "coverage-7.6.10-cp313-cp313-win_amd64.whl", hash = "sha256:254f1a3b1eef5f7ed23ef265eaa89c65c8c5b6b257327c149db1ca9d4a35f25e"},
{file = "coverage-7.6.10-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2ccf240eb719789cedbb9fd1338055de2761088202a9a0b73032857e53f612fe"},
{file = "coverage-7.6.10-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0c807ca74d5a5e64427c8805de15b9ca140bba13572d6d74e262f46f50b13273"},
{file = "coverage-7.6.10-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bcfa46d7709b5a7ffe089075799b902020b62e7ee56ebaed2f4bdac04c508d8"},
{file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4e0de1e902669dccbf80b0415fb6b43d27edca2fbd48c74da378923b05316098"},
{file = "coverage-7.6.10-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7b444c42bbc533aaae6b5a2166fd1a797cdb5eb58ee51a92bee1eb94a1e1cb"},
{file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b330368cb99ef72fcd2dc3ed260adf67b31499584dc8a20225e85bfe6f6cfed0"},
{file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9a7cfb50515f87f7ed30bc882f68812fd98bc2852957df69f3003d22a2aa0abf"},
{file = "coverage-7.6.10-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f93531882a5f68c28090f901b1d135de61b56331bba82028489bc51bdd818d2"},
{file = "coverage-7.6.10-cp313-cp313t-win32.whl", hash = "sha256:89d76815a26197c858f53c7f6a656686ec392b25991f9e409bcef020cd532312"},
{file = "coverage-7.6.10-cp313-cp313t-win_amd64.whl", hash = "sha256:54a5f0f43950a36312155dae55c505a76cd7f2b12d26abeebbe7a0b36dbc868d"},
{file = "coverage-7.6.10-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:656c82b8a0ead8bba147de9a89bda95064874c91a3ed43a00e687f23cc19d53a"},
{file = "coverage-7.6.10-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ccc2b70a7ed475c68ceb548bf69cec1e27305c1c2606a5eb7c3afff56a1b3b27"},
{file = "coverage-7.6.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5e37dc41d57ceba70956fa2fc5b63c26dba863c946ace9705f8eca99daecdc4"},
{file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0aa9692b4fdd83a4647eeb7db46410ea1322b5ed94cd1715ef09d1d5922ba87f"},
{file = "coverage-7.6.10-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa744da1820678b475e4ba3dfd994c321c5b13381d1041fe9c608620e6676e25"},
{file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c0b1818063dc9e9d838c09e3a473c1422f517889436dd980f5d721899e66f315"},
{file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:59af35558ba08b758aec4d56182b222976330ef8d2feacbb93964f576a7e7a90"},
{file = "coverage-7.6.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7ed2f37cfce1ce101e6dffdfd1c99e729dd2ffc291d02d3e2d0af8b53d13840d"},
{file = "coverage-7.6.10-cp39-cp39-win32.whl", hash = "sha256:4bcc276261505d82f0ad426870c3b12cb177752834a633e737ec5ee79bbdff18"},
{file = "coverage-7.6.10-cp39-cp39-win_amd64.whl", hash = "sha256:457574f4599d2b00f7f637a0700a6422243b3565509457b2dbd3f50703e11f59"},
{file = "coverage-7.6.10-pp39.pp310-none-any.whl", hash = "sha256:fd34e7b3405f0cc7ab03d54a334c17a9e802897580d964bd8c2001f4b9fd488f"},
{file = "coverage-7.6.10.tar.gz", hash = "sha256:7fb105327c8f8f0682e29843e2ff96af9dcbe5bab8eeb4b398c6a33a16d80a23"},
{file = "coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe"},
{file = "coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28"},
{file = "coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3"},
{file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676"},
{file = "coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d"},
{file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a"},
{file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c"},
{file = "coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f"},
{file = "coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f"},
{file = "coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23"},
{file = "coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27"},
{file = "coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea"},
{file = "coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7"},
{file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5ff52d790c7e1628241ffbcaeb33e07d14b007b6eb00a19320c7b8a7024c040"},
{file = "coverage-7.8.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d39fc4817fd67b3915256af5dda75fd4ee10621a3d484524487e33416c6f3543"},
{file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b44674870709017e4b4036e3d0d6c17f06a0e6d4436422e0ad29b882c40697d2"},
{file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8f99eb72bf27cbb167b636eb1726f590c00e1ad375002230607a844d9e9a2318"},
{file = "coverage-7.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b571bf5341ba8c6bc02e0baeaf3b061ab993bf372d982ae509807e7f112554e9"},
{file = "coverage-7.8.0-cp311-cp311-win32.whl", hash = "sha256:e75a2ad7b647fd8046d58c3132d7eaf31b12d8a53c0e4b21fa9c4d23d6ee6d3c"},
{file = "coverage-7.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:3043ba1c88b2139126fc72cb48574b90e2e0546d4c78b5299317f61b7f718b78"},
{file = "coverage-7.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bbb5cc845a0292e0c520656d19d7ce40e18d0e19b22cb3e0409135a575bf79fc"},
{file = "coverage-7.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4dfd9a93db9e78666d178d4f08a5408aa3f2474ad4d0e0378ed5f2ef71640cb6"},
{file = "coverage-7.8.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f017a61399f13aa6d1039f75cd467be388d157cd81f1a119b9d9a68ba6f2830d"},
{file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0915742f4c82208ebf47a2b154a5334155ed9ef9fe6190674b8a46c2fb89cb05"},
{file = "coverage-7.8.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a40fcf208e021eb14b0fac6bdb045c0e0cab53105f93ba0d03fd934c956143a"},
{file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a1f406a8e0995d654b2ad87c62caf6befa767885301f3b8f6f73e6f3c31ec3a6"},
{file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:77af0f6447a582fdc7de5e06fa3757a3ef87769fbb0fdbdeba78c23049140a47"},
{file = "coverage-7.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f2d32f95922927186c6dbc8bc60df0d186b6edb828d299ab10898ef3f40052fe"},
{file = "coverage-7.8.0-cp312-cp312-win32.whl", hash = "sha256:769773614e676f9d8e8a0980dd7740f09a6ea386d0f383db6821df07d0f08545"},
{file = "coverage-7.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:e5d2b9be5b0693cf21eb4ce0ec8d211efb43966f6657807f6859aab3814f946b"},
{file = "coverage-7.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ac46d0c2dd5820ce93943a501ac5f6548ea81594777ca585bf002aa8854cacd"},
{file = "coverage-7.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:771eb7587a0563ca5bb6f622b9ed7f9d07bd08900f7589b4febff05f469bea00"},
{file = "coverage-7.8.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42421e04069fb2cbcbca5a696c4050b84a43b05392679d4068acbe65449b5c64"},
{file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:554fec1199d93ab30adaa751db68acec2b41c5602ac944bb19187cb9a41a8067"},
{file = "coverage-7.8.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aaeb00761f985007b38cf463b1d160a14a22c34eb3f6a39d9ad6fc27cb73008"},
{file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:581a40c7b94921fffd6457ffe532259813fc68eb2bdda60fa8cc343414ce3733"},
{file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f319bae0321bc838e205bf9e5bc28f0a3165f30c203b610f17ab5552cff90323"},
{file = "coverage-7.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04bfec25a8ef1c5f41f5e7e5c842f6b615599ca8ba8391ec33a9290d9d2db3a3"},
{file = "coverage-7.8.0-cp313-cp313-win32.whl", hash = "sha256:dd19608788b50eed889e13a5d71d832edc34fc9dfce606f66e8f9f917eef910d"},
{file = "coverage-7.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:a9abbccd778d98e9c7e85038e35e91e67f5b520776781d9a1e2ee9d400869487"},
{file = "coverage-7.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:18c5ae6d061ad5b3e7eef4363fb27a0576012a7447af48be6c75b88494c6cf25"},
{file = "coverage-7.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:95aa6ae391a22bbbce1b77ddac846c98c5473de0372ba5c463480043a07bff42"},
{file = "coverage-7.8.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e013b07ba1c748dacc2a80e69a46286ff145935f260eb8c72df7185bf048f502"},
{file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d766a4f0e5aa1ba056ec3496243150698dc0481902e2b8559314368717be82b1"},
{file = "coverage-7.8.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad80e6b4a0c3cb6f10f29ae4c60e991f424e6b14219d46f1e7d442b938ee68a4"},
{file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b87eb6fc9e1bb8f98892a2458781348fa37e6925f35bb6ceb9d4afd54ba36c73"},
{file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d1ba00ae33be84066cfbe7361d4e04dec78445b2b88bdb734d0d1cbab916025a"},
{file = "coverage-7.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f3c38e4e5ccbdc9198aecc766cedbb134b2d89bf64533973678dfcf07effd883"},
{file = "coverage-7.8.0-cp313-cp313t-win32.whl", hash = "sha256:379fe315e206b14e21db5240f89dc0774bdd3e25c3c58c2c733c99eca96f1ada"},
{file = "coverage-7.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2e4b6b87bb0c846a9315e3ab4be2d52fac905100565f4b92f02c445c8799e257"},
{file = "coverage-7.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa260de59dfb143af06dcf30c2be0b200bed2a73737a8a59248fcb9fa601ef0f"},
{file = "coverage-7.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:96121edfa4c2dfdda409877ea8608dd01de816a4dc4a0523356067b305e4e17a"},
{file = "coverage-7.8.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b8af63b9afa1031c0ef05b217faa598f3069148eeee6bb24b79da9012423b82"},
{file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89b1f4af0d4afe495cd4787a68e00f30f1d15939f550e869de90a86efa7e0814"},
{file = "coverage-7.8.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94ec0be97723ae72d63d3aa41961a0b9a6f5a53ff599813c324548d18e3b9e8c"},
{file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8a1d96e780bdb2d0cbb297325711701f7c0b6f89199a57f2049e90064c29f6bd"},
{file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f1d8a2a57b47142b10374902777e798784abf400a004b14f1b0b9eaf1e528ba4"},
{file = "coverage-7.8.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cf60dd2696b457b710dd40bf17ad269d5f5457b96442f7f85722bdb16fa6c899"},
{file = "coverage-7.8.0-cp39-cp39-win32.whl", hash = "sha256:be945402e03de47ba1872cd5236395e0f4ad635526185a930735f66710e1bd3f"},
{file = "coverage-7.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:90e7fbc6216ecaffa5a880cdc9c77b7418c1dcb166166b78dbc630d07f278cc3"},
{file = "coverage-7.8.0-pp39.pp310.pp311-none-any.whl", hash = "sha256:b8194fb8e50d556d5849753de991d390c5a1edeeba50f68e3a9253fbd8bf8ccd"},
{file = "coverage-7.8.0-py3-none-any.whl", hash = "sha256:dbf364b4c5e7bae9250528167dfe40219b62e2d573c854d74be213e1e52069f7"},
{file = "coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501"},
]
[package.dependencies]
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
[package.extras]
toml = ["tomli"]
toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
[[package]]
name = "exceptiongroup"
@ -124,6 +131,8 @@ version = "1.2.2"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
groups = ["main"]
markers = "python_version == \"3.10\""
files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
@ -134,13 +143,14 @@ test = ["pytest (>=6)"]
[[package]]
name = "fakeredis"
version = "2.26.2"
version = "2.28.1"
description = "Python implementation of redis API, can be used for testing purposes."
optional = false
python-versions = "<4.0,>=3.7"
groups = ["main"]
files = [
{file = "fakeredis-2.26.2-py3-none-any.whl", hash = "sha256:86d4129df001efc25793cb334008160fccc98425d9f94de47884a92b63988c14"},
{file = "fakeredis-2.26.2.tar.gz", hash = "sha256:3ee5003a314954032b96b1365290541346c9cc24aab071b52cc983bb99ecafbf"},
{file = "fakeredis-2.28.1-py3-none-any.whl", hash = "sha256:38c7c17fba5d5522af9d980a8f74a4da9900a3441e8f25c0fe93ea4205d695d1"},
{file = "fakeredis-2.28.1.tar.gz", hash = "sha256:5e542200b945aa0a7afdc0396efefe3cdabab61bc0f41736cc45f68960255964"},
]
[package.dependencies]
@ -160,13 +170,14 @@ probabilistic = ["pyprobables (>=0.6,<0.7)"]
[[package]]
name = "hypothesis"
version = "6.123.2"
version = "6.131.0"
description = "A library for property-based testing"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "hypothesis-6.123.2-py3-none-any.whl", hash = "sha256:0a8bf07753f1436f1b8697a13ea955f3fef3ef7b477c2972869b1d142bcdb30e"},
{file = "hypothesis-6.123.2.tar.gz", hash = "sha256:02c25552783764146b191c69eef69d8375827b58a75074055705ab8fdbc95fc5"},
{file = "hypothesis-6.131.0-py3-none-any.whl", hash = "sha256:734959017e3ee4ef8f0ecb4e5169c8f4cf96dc83a997d2edf01fb5350f5bf2f4"},
{file = "hypothesis-6.131.0.tar.gz", hash = "sha256:4b807daeeee47852edfd9818ba0e33df14902f1b78a5524f1a3fb71f80c7cec3"},
]
[package.dependencies]
@ -175,10 +186,10 @@ exceptiongroup = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
sortedcontainers = ">=2.1.0,<3.0.0"
[package.extras]
all = ["black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.78)", "django (>=4.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.18)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.19.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.2)"]
all = ["black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.85)", "django (>=4.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.20)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.19.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2025.2) ; sys_platform == \"win32\" or sys_platform == \"emscripten\"", "watchdog (>=4.0.0)"]
cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"]
codemods = ["libcst (>=0.3.16)"]
crosshair = ["crosshair-tool (>=0.0.78)", "hypothesis-crosshair (>=0.0.18)"]
crosshair = ["crosshair-tool (>=0.0.85)", "hypothesis-crosshair (>=0.0.20)"]
dateutil = ["python-dateutil (>=1.4)"]
django = ["django (>=4.2)"]
dpcontracts = ["dpcontracts (>=0.4)"]
@ -189,28 +200,31 @@ pandas = ["pandas (>=1.1)"]
pytest = ["pytest (>=4.6)"]
pytz = ["pytz (>=2014.1)"]
redis = ["redis (>=3.0.0)"]
zoneinfo = ["tzdata (>=2024.2)"]
watchdog = ["watchdog (>=4.0.0)"]
zoneinfo = ["tzdata (>=2025.2) ; sys_platform == \"win32\" or sys_platform == \"emscripten\""]
[[package]]
name = "iniconfig"
version = "2.0.0"
version = "2.1.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
{file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
]
[[package]]
name = "jinja2"
version = "3.1.5"
version = "3.1.6"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"},
{file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"},
{file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
{file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
]
[package.dependencies]
@ -225,6 +239,7 @@ version = "1.7.0"
description = "A final implementation of JSONPath for Python that aims to be standard compliant, including arithmetic and binary comparison operators and providing clear AST for metaprogramming."
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c"},
{file = "jsonpath_ng-1.7.0-py2-none-any.whl", hash = "sha256:898c93fc173f0c336784a3fa63d7434297544b7198124a68f9a3ef9597b0ae6e"},
@ -236,97 +251,107 @@ ply = "*"
[[package]]
name = "lupa"
version = "2.2"
version = "2.4"
description = "Python wrapper around Lua and LuaJIT"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "lupa-2.2-cp27-cp27m-macosx_11_0_x86_64.whl", hash = "sha256:4bb05e3fc8f794b4a1b8a38229c3b4ae47f83cfbe7f6b172032f66d3308a0934"},
{file = "lupa-2.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:13062395e716cebe25dfc6dc3738a9eb514bb052b52af25cf502c1fd74affd21"},
{file = "lupa-2.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:e673443dd7f7f0510bb9f4b0dc6bad6932d271b0afdbdc492fa71e9b9eab638d"},
{file = "lupa-2.2-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:3b47702b94e9e391052118cbde253f69a0af96ec776f48af74e72f30d740ccc9"},
{file = "lupa-2.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2242884a5078cd2507f15a162b5faf6f39a1f27654a1cc7db09cdb65b0b599b3"},
{file = "lupa-2.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8555526f03bb41d5aef16d105e8f51da1000d833e90d846448cf745ca6cd72e8"},
{file = "lupa-2.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a50807c6cc11d3ecf568d964be6708e26d4669d435c76fcb568a98d1dd6e8ae9"},
{file = "lupa-2.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c140dd19614e43b76b84295945878cea3cdf7ed34e133b1a8c0e3fa7efc9c6ac"},
{file = "lupa-2.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c725c1832b0c6095583a6a57273e6f33a6b55230f90bcacdf06934ce21ef04e9"},
{file = "lupa-2.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:18a302810735da688d21e8397c696e68b89dbe3c45a3fdc3406f5c0e55887467"},
{file = "lupa-2.2-cp310-cp310-win32.whl", hash = "sha256:a4f03aa308d949a3f2e4e755ffc6a698d3ea02fccd34014fab496efb99b3d4f4"},
{file = "lupa-2.2-cp310-cp310-win_amd64.whl", hash = "sha256:8494802f789174cd26176e6b408e60e468cda348d4f767562d06991604813f61"},
{file = "lupa-2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:95ee903ab71c3e6498bcd3bca60938a961c84fae47cdf23389a48c73e15dbad2"},
{file = "lupa-2.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:011dbc81a790693b5457a0d761b032a8acdcc2945e32ca6ef34a7698bda0b09a"},
{file = "lupa-2.2-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:8c89d8e99f684dfedccbf2f0dbdcc28deb73c4ff0545452f43ec02330dacfe0c"},
{file = "lupa-2.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26c3edea3ce6465364af6cc1c134b7f23a3ff919e5e499720acbff01b14b9931"},
{file = "lupa-2.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cd6afa3f6c998ac55f90b0665266c19100387de55d25af25ef4a35197d29d52"},
{file = "lupa-2.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b79bef7f48696bf70eff165afa49778470607dce6420b497eb82cfae1af6947"},
{file = "lupa-2.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:08e2bfa98725f7495cef30d42d87fff82795b9b9e76b740521828784b778ade7"},
{file = "lupa-2.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:0318ceb4d1782776bae7495a3bd3d50e57f80115ecbeff1e95d87a4e9411acf2"},
{file = "lupa-2.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9180dc7ee5c580cee41d9afac0b7c738cf7f6badf4a1398a6e1921dff155619c"},
{file = "lupa-2.2-cp311-cp311-win32.whl", hash = "sha256:82077fe962c6e9ae1652e826f58e6250d1daa13c446ba1f4d6b68f16df65db0b"},
{file = "lupa-2.2-cp311-cp311-win_amd64.whl", hash = "sha256:e2d2b9a6a4ef109b75668e26204f122196f33907ce3ccc80322ca70f84f81598"},
{file = "lupa-2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8cd872e16e736a3ecb800e70b4f36a66c794b7d339247712244a515561da4ff5"},
{file = "lupa-2.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:6e8027ad53daa511e4a049eb0eb9f71b46fd2c5be6897fc68d75288b04086d4d"},
{file = "lupa-2.2-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:0a7bd2841fd41b718d415162ec53b7d00079c27b1c5c1a2f2d0fb8080dd64d73"},
{file = "lupa-2.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:63eff3aa68791b5c9a400f89f18018f4f63b8619adaa603fcd09392b87ca6b9b"},
{file = "lupa-2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ab43356bb269ca4f03d25200b7559581cd791fbc631104c3e7d186d3c37221f"},
{file = "lupa-2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:556779c0c28a2948749817ffd62dec882c834a6445aeff5d31ae862e14eebb21"},
{file = "lupa-2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:42fd611a099ab1804a8d23154d4c7b2221557c94d34f8964da0dc03760f15d3d"},
{file = "lupa-2.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:63d5ae8ccbafe0aa0034da32f18fc692963df1b5e1ebf91e76f504de1d5aecff"},
{file = "lupa-2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3d3d9e5991861d8ee28709d94e673b89bdea10188b34a155835ba2dbbc7d26a7"},
{file = "lupa-2.2-cp312-cp312-win32.whl", hash = "sha256:58a3621579b26ad5a524c1c41623ec551160653e915cf4aa41453f4339821b89"},
{file = "lupa-2.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e8ff117eca26f5cedcd2b2467cf56d0c64cfcb804b5083a36d818b57edc4036"},
{file = "lupa-2.2-cp36-cp36m-macosx_11_0_x86_64.whl", hash = "sha256:afe2b90c65f61f7d5ad55cdbfbb89cb50e5ab4d6184ea975befc51ffdc20dc8f"},
{file = "lupa-2.2-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c597ea2dc203767dcb5a853cf885a7238b0639f5b7cb5c6ad5dbe5d2b39e25c6"},
{file = "lupa-2.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8149dcbe9953e8cad991949dec41bf6dbaa8a2d613e4b024f98e510b0aab4fa4"},
{file = "lupa-2.2-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92e1c6a1f380bc829618d0e95c15612b6e2604baa8ffd42547451e9d842837ae"},
{file = "lupa-2.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:56be246cf7126f980c13b79a03ad43361dee5a65f8be8c4e2feb58a2bdcc5a2a"},
{file = "lupa-2.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:da3460b920d4520ae8a3927b92c22402592fe2e31f08492c3c0ba9b8eadee302"},
{file = "lupa-2.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:211d3371d9836d87b2097f520492241cd5e06b29ca8777739c4fe30a1df4c76c"},
{file = "lupa-2.2-cp36-cp36m-win32.whl", hash = "sha256:617fc3532f224619e15d45adb9c9af8f4690e36cad332d68d49e78463e51d528"},
{file = "lupa-2.2-cp36-cp36m-win_amd64.whl", hash = "sha256:50b2f0f8bfcacd68c9ae0a2872ff4b90c2df0490f193253c922283a295f23b6a"},
{file = "lupa-2.2-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:f3de07b7f19296a702c8710f44b221aefe6563461e209198862cd1f06401b13d"},
{file = "lupa-2.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eed6529c89ea475cbc403ed6e8670f1adf9eb2eb34b7610690d9827d35759a3c"},
{file = "lupa-2.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cc171b352c187a012bbc5c20692236843e8c123c60569be872cb72bb7edcbd4"},
{file = "lupa-2.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:704ed8f5a91133a8d62cba2d6fe4f2e43c7ee6f3998484d31abcfc4a57bedd1e"},
{file = "lupa-2.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d2aa0fba09a045f5bcc638ede0f614fcd36339da58b7415a1e66e3590781a4a5"},
{file = "lupa-2.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b2b911d3890fa93ae3f83c5d806008c3b551941813b39e7605def137a9b9b064"},
{file = "lupa-2.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:00bcae88a2123f0cfd34f7206cc2d88008d905ebc065d41797827d046404b09e"},
{file = "lupa-2.2-cp37-cp37m-win32.whl", hash = "sha256:225bbe9e58881bb92f96c6b43587168ed329b2b37c3236a9883efa681aec9f5a"},
{file = "lupa-2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:57662d9653e157872caeaa622d966aa1da7bb8fe8646b63fb1194a3cdb98c417"},
{file = "lupa-2.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc728fbe6d4e668ad8bec979ef86675387ca640e319ec029e0fc8f2bc9c3d224"},
{file = "lupa-2.2-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:33a2beebe078e13770eff5d12a22d98a425fff89f87af2155c32769adc0114f1"},
{file = "lupa-2.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd1e95d8a399ff379d09358490171965aaa25007ed06488b972df08f1b3df509"},
{file = "lupa-2.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a63d1bc6a473813c707cf5badbfba081bf7cfbd761d58e1812c9a65a477146f9"},
{file = "lupa-2.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df6e1bdd13f6fbdab2212bf08c24c232653832673c21c10ba576f89770e58686"},
{file = "lupa-2.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:26f2617544e4b8cf2a4c1873e6f4feb7e547f4c06bfd088a24547d37f68a3945"},
{file = "lupa-2.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:189856225402eab6dc467b77190c5beddc5c004a9cdc5855e7517206f3b380ca"},
{file = "lupa-2.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2563d55538ebecab1d8768c77e1972f7768440b8e41aff4466352b942aa50dd1"},
{file = "lupa-2.2-cp38-cp38-win32.whl", hash = "sha256:6c7e418bd39b9e2717654ed52ea55b681247d95139da958603e0766ed138b190"},
{file = "lupa-2.2-cp38-cp38-win_amd64.whl", hash = "sha256:3facbd310fc73d3bcdb8cb363df80524ee52ac25b7566d0f0fb8b300b04c3bdb"},
{file = "lupa-2.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cda04e655af89824a92b4ca168524e0f526b78da5f39f66103cc3b6a924ef60c"},
{file = "lupa-2.2-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:c49d1962478fa6a94b468e0dd6f725034ee690f41ae03217ff4672f370a7a099"},
{file = "lupa-2.2-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:6bddf06f4f4b2257701e12690c5e951eb6a02b88633b7a43cc160172ff3a88b5"},
{file = "lupa-2.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10c3bb414fc3a4ba9ac3e57a17ffd4c3d0db6da78c53b6792de5a964b5539e42"},
{file = "lupa-2.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10c2c81bc96f2091210aaf046ef22f920581a3e161b3961121171e02595ca6fb"},
{file = "lupa-2.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11193c9e7fe1b82d921991c68a33f5b08c8e0c16d67d173768fc80f8c75d9d52"},
{file = "lupa-2.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9e149fafd20e748818a0b718abc42f099a3cc6debc7c6932564d7e475291f0e2"},
{file = "lupa-2.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2518128f38a4608bbc5375404082a3c22c86037639842fb7b1fc2b4f5d2a41e3"},
{file = "lupa-2.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:756fc6aa5ca3a6b7764c474ef061760c5d38e2dd96c21567ab3c7d4f5ed2c3a7"},
{file = "lupa-2.2-cp39-cp39-win32.whl", hash = "sha256:9b2b7148a77f60b7b193aec2bd820e89c1ecaab9838ca81c8212e2f972df1a1d"},
{file = "lupa-2.2-cp39-cp39-win_amd64.whl", hash = "sha256:93216d7ae8bb373a8a388b058960a00eaaa6a01e5e2306a13e65db1024181a62"},
{file = "lupa-2.2-pp310-pypy310_pp73-macosx_11_0_x86_64.whl", hash = "sha256:e4cd8c6f725a5629551ac08979d0631af6bed2564cf87dcae489bcb53bdab808"},
{file = "lupa-2.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95d712728d36262e0bcffea2ad4b1c3ee6122e4eb16f5a70c2f4750f34580148"},
{file = "lupa-2.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:47eb46153810e868c543ffc53a3369700998a3e617cfcebf49133a79e6f56432"},
{file = "lupa-2.2-pp37-pypy37_pp73-macosx_11_0_x86_64.whl", hash = "sha256:283066c6ef9141a66924854a78619ff16bc2efd324484807be58ca9a8e9b617a"},
{file = "lupa-2.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7141e395325f150321c3caa69178dc70224512e0483e2165d3d1ca375608abb7"},
{file = "lupa-2.2-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:502248085d3d2dc74e642f97773367a1929daa24fcf039dd5048acdd5b49a8f9"},
{file = "lupa-2.2-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:4cdeb4a942068882c9e3751520b6de1b6c21d7c2526a2040755b62c7cb46308f"},
{file = "lupa-2.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bfd7e62f3149d10fa3485f4d5143f74b295787708b1974f7fad74b65fb911fa1"},
{file = "lupa-2.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4c78b3b7137212a9ef881adca3168a376445da3a7dc322b2416c90a73c81db2c"},
{file = "lupa-2.2-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:ecd1b3a4d8db553c4eaed742843f4b7d77bca795ec9f4292385709bcf691e8a3"},
{file = "lupa-2.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:36db930207c15656b9989721ea41ba8c039abd088cc7242bb690aa72a4978e68"},
{file = "lupa-2.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:8ccba6f5cd8bdecf4000531298e6edd803547340752b80fe5b74911fa6119cc8"},
{file = "lupa-2.2.tar.gz", hash = "sha256:665a006bcf8d9aacdfdb953824b929d06a0c55910a662b59be2f157ab4c8924d"},
{file = "lupa-2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:518822e047b2c65146cf09efb287f28c2eb3ced38bcc661f881f33bcd9e2ba1f"},
{file = "lupa-2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:15ce18c8b7642dd5b8f491c6e19fea6079f24f52e543c698622e5eb80b17b952"},
{file = "lupa-2.4-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:aea832d79931b512827ab6af68b1d20099d290c7bd94b98306bc9d639a719c6f"},
{file = "lupa-2.4-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d7f7dc548c35c0384aa54e3a8e0953dead10975e7d5ff9516ba09a36127f449"},
{file = "lupa-2.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e166d81e6e39a7fedd5dd1d6560483bb7b0db18e1fe4153cc92088a1a81d9035"},
{file = "lupa-2.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a35e974e9dce96217dda3db89a22384093fdaa3ea7a3d8aaf6e548767634c34"},
{file = "lupa-2.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:bc4bfd7abc63940e71d46ef22080ff02315b5c7619341daca5ea37f6a595edc6"},
{file = "lupa-2.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b38ce88bfef9677b94bd5ab67d1359dd87fa7a78189909e28e90ada65bb5064b"},
{file = "lupa-2.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:815071e5ef2d313b5e69f5671a343580643e2794cc5f38e22f75995116df11e8"},
{file = "lupa-2.4-cp310-cp310-win32.whl", hash = "sha256:98c3160f5d1e5b9e976f836ca9a97e51ad3b52043680f117ba3d6c535309fef0"},
{file = "lupa-2.4-cp310-cp310-win_amd64.whl", hash = "sha256:f1a0cee956c929f09aa8af36d2b28f1a39170ef8673deaf7b80a5dd8a30d1c54"},
{file = "lupa-2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ae945bb9b6fd84bfa4bd3a3caabe54d05d2514da16e1f45d304208c58819ebd"},
{file = "lupa-2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:dae6006214974192775d76bee156cee42632320f93f9756d2763f4aa90090026"},
{file = "lupa-2.4-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:fdcf8ae011e2e631dd1737cdf705219eb797063f0455761c7046c2554f1d3f8c"},
{file = "lupa-2.4-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db0b331de8dcdc6540e6a62500fcbfb1e3d9887c6ff5fb146b8713018ea7c102"},
{file = "lupa-2.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63c74c457e52d6532795e60e3f3ad87ae38a833d2a427abd55d98032701b0d39"},
{file = "lupa-2.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:795d047b85363b8f9123cb87bd590d177f7c31a631cc6e0a9de2dbb7f92cf6d5"},
{file = "lupa-2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4b2a360db05c66cf4cca0e07fe322a3b2fe2209a46f8e9d8ff2f4b93b5368b35"},
{file = "lupa-2.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c6f38b65bb16ce9c92c6d993c60aca1d700326a513ce294635a67a1553689e64"},
{file = "lupa-2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fd0266968ade202b45747e932fb2e1823587eee2b0983733841325a0ade272ed"},
{file = "lupa-2.4-cp311-cp311-win32.whl", hash = "sha256:8a917b550db751419bd7ec426e26605ad8934a540d376d253b6c6ab1570ce58a"},
{file = "lupa-2.4-cp311-cp311-win_amd64.whl", hash = "sha256:c8ceb7beb0d6f42d8a20bfa880f986f29ba8ad162ac678d62a9b2628e8ee6946"},
{file = "lupa-2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bbf9b26bd8e4f28e794e3572bfcff4489a137747de26bdfe3df33b88370f39cc"},
{file = "lupa-2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:085f104ec8e4a848177c16691724da45d0bb8c79deef331fd21c36bdc53e941b"},
{file = "lupa-2.4-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:81f3a4d471e2eb4e4db3ae9367d1144298f94ff8213c701eee8f9e8100f80b4a"},
{file = "lupa-2.4-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c803c8a5692145024c20ce8ee82826b8840fd806565fa8134621b361f66451d8"},
{file = "lupa-2.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6732f4051f982695a87db69539fd9b4c2bddf51ee43cdcc1a2c379ca6af6c5b2"},
{file = "lupa-2.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb1213a20a52e8e2c90f473d15a8a9c885eaf291d3536faf5414e3a5c3f8e6"},
{file = "lupa-2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:34992e172096e2209d5a55364774e90311ef30fe002ca6ab9e617211c08651de"},
{file = "lupa-2.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:1b4cfa0fd7f666ad1b56643b7f43925445ccf6f68a75ae715c155bc56dbc843d"},
{file = "lupa-2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41286859dc564098f8cc3d707d8f6a8934540127761498752c4fa25aea38d89b"},
{file = "lupa-2.4-cp312-cp312-win32.whl", hash = "sha256:bb41e63ca36ba4eafb346fcea2daede74484ef2b70affd934e7d265d30d32dcd"},
{file = "lupa-2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a89ed97ea51c093cfa0fd00669e4d9fdda8b1bd9abb756339ea8c96cb7e890f7"},
{file = "lupa-2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:12b30ea0586579ecde0e13bb372010326178ff309f52b5e39f6df843bd815ba7"},
{file = "lupa-2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:0fce2487f9d9199e0d78478ecd1ba47d1779850588a8e0b7def4f3adf25e943c"},
{file = "lupa-2.4-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:ed71a89d500191f7d0ad5a0b988298e4d9fde8445fbac940e0996e214760a5c5"},
{file = "lupa-2.4-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41f2b0d0b44e1c94814f69ba82ef25b7e47a7f3edcd47d220a11ee3b64514452"},
{file = "lupa-2.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f16fbaa68ec999ee5e8935d517df8d8a6bfcaa8fb2fe5b9c60131be15590d0c0"},
{file = "lupa-2.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4842759d027db108f605dc895c9afc4011d12eac448e0d092a4d0b21e79ba1c5"},
{file = "lupa-2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52efeef1e632c5edff61bd6d79b0f393e515ea2a464f6f0d4276ecc565279f04"},
{file = "lupa-2.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2b32202a1244b6c7aaa6d2a611b5a842de4b166703388db66265b37074e255fd"},
{file = "lupa-2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ba0649579b0698ce4841106ec7eee657995b8c13e9f5e16bbf93e8afb387d59b"},
{file = "lupa-2.4-cp313-cp313-win32.whl", hash = "sha256:18e12e714a2f633bf3583f23ec07904a0584e351889eff7f98439d520255a204"},
{file = "lupa-2.4-cp313-cp313-win_amd64.whl", hash = "sha256:203a11122bd11366e5b836590ea11bf2ebfb79bfdaf0ffd44b6646cea51cb255"},
{file = "lupa-2.4-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07f55b6c30f9e03f63ca7c4037b146110194ab0f89021a9923b817a01aa1c3bc"},
{file = "lupa-2.4-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2d5c732f4fe8a4f1577f49e7a31045294019c731208ecee6f194bb03ee4c186"},
{file = "lupa-2.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90a41c0f2744be3b055dec0b9f65cd87c52fb7a86891df43292369ee8e4ea111"},
{file = "lupa-2.4-cp36-cp36m-musllinux_1_2_aarch64.whl", hash = "sha256:34994926045e66fea6b93b2caab3ac66f5de4218055fd4dd2b98198b2c3765ee"},
{file = "lupa-2.4-cp36-cp36m-musllinux_1_2_i686.whl", hash = "sha256:b250cd39639fff9a842a138f18343c579a993e56c9dea8914398e5c9775f6b0d"},
{file = "lupa-2.4-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:1247453e4b95dfbf88a13065e49815992db16485398760951425a29df7b5e2dc"},
{file = "lupa-2.4-cp36-cp36m-win32.whl", hash = "sha256:0f95747c40156a77b4336f1bb42f1e29e42cfb46c57b978b50db6980025b528c"},
{file = "lupa-2.4-cp36-cp36m-win_amd64.whl", hash = "sha256:4e12cfc3005fcd2a5424449a7d989d1820b7e17a06d65dfe769255278122b69e"},
{file = "lupa-2.4-cp37-cp37m-macosx_11_0_x86_64.whl", hash = "sha256:31e522dcd53cb2a8c53161465f3d20dc9672241b2c4f5384ebda07f30d35d7f7"},
{file = "lupa-2.4-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:710067765c252328ba2d521a3ab7dfef3a6b89293b9ed24254587db5210612ca"},
{file = "lupa-2.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c3feb9d8af4c5cda2f1523ce6b40cadc96b8de275d84f7d64e1a35b8ecd7f62"},
{file = "lupa-2.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89d802cd78da75262477148ef5aea14c8da76f356329f69b44bc3b31dd3d64a1"},
{file = "lupa-2.4-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:e84b388356fe392d787e6a8aed182bd5b807de8965aa9ef6f10d0eb5e47ddca5"},
{file = "lupa-2.4-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f70d9d7e2fd38a3124461cb3a2d10494c4fbea0ee9fa801e6066b79f0a75e5f0"},
{file = "lupa-2.4-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:7ca47a1ac55c8f5cc0043b9fee195b2f6f3b9435fde71a0e035546b9410731e9"},
{file = "lupa-2.4-cp37-cp37m-win32.whl", hash = "sha256:829bfb692fee181d275c0d24dafe2c2273794f438469d0fd32f0127652f57e7a"},
{file = "lupa-2.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ea439dbd6c3e9895f986fff57a4617140239ad3f0b60ca4ccff0b32b3401b8d5"},
{file = "lupa-2.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:76bae9285a26d1a1cacb630d1db57e829f3f91d1e8c0760acabd0e9d04eb65f3"},
{file = "lupa-2.4-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:27cafb9bbe5a4869a50dcb7aca068e1cc68e233d54cd6093116ffb868f7083e3"},
{file = "lupa-2.4-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1737a54ac93b0bfe22762506665b7ac433fd161a596aee342e4dae106198349"},
{file = "lupa-2.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03fca7715493efc98db21686e225942dba3ca1683c6c501e47384702871d7c79"},
{file = "lupa-2.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:579fae5adf99f6872379c585def71e502312072ec8bdf04244dc6c875f2b10c4"},
{file = "lupa-2.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:073bf02f31fa60cff0952b0f4c41a635b3a63d75b4d6afdf2380520efad78241"},
{file = "lupa-2.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:6ed59e6ed08c4ddae4bbf317b37af5ee2253c5ff14dc3914a5f3d3c128535d90"},
{file = "lupa-2.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a1c9fed2ee9ce6c117fe78f987617a8890c09d19476ec97aa64ce2c6cbb507f0"},
{file = "lupa-2.4-cp38-cp38-win32.whl", hash = "sha256:9c803d22bdfd0e0de7b43793b10d1e235defdbfbb99dbf12405dfb7e34d004d6"},
{file = "lupa-2.4-cp38-cp38-win_amd64.whl", hash = "sha256:a468c6fe8334af1a5c5881e54afc39c3ebbef0e1d4af1a9ceaf04a4c95edfb9a"},
{file = "lupa-2.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:74a3747bcd53b9f1b6adf44343a614cf0d03a4f11d2e9dee08900a2c18f1266a"},
{file = "lupa-2.4-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:7bb03be049222056ae344b73a2a3c6d842c55c3a69b5c5acea0f9f5a0f1dddc1"},
{file = "lupa-2.4-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:79ff99c6a3493c2eb69a932e034d0e67fa03ef50e235c0804393ca6040ab9a90"},
{file = "lupa-2.4-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:599764acf3db817b1623ef82988c85d0c361b564108918658079eca1dcd2cc8b"},
{file = "lupa-2.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6218c0dead8d85ff716969347273af3abf29fa520e07a0fc88079a8cefd58faf"},
{file = "lupa-2.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a4f6483c55a6449bd95b0c0b17683b0fde6970b578da4f5de37892884b4d353"},
{file = "lupa-2.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:00f7fb8ae883a25bc17058dae19635da32dd79b3c43470f4267d57f7bd2d5a93"},
{file = "lupa-2.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0df511db2bf0a4e7c8bb5c0092a83e0c217a175f10dba59297b2b903b02e243f"},
{file = "lupa-2.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:761491befe07097a07f7a1f0a6595076ca04c8b2db6071e8dedbbbf4cf1d5591"},
{file = "lupa-2.4-cp39-cp39-win32.whl", hash = "sha256:b53f91cbcd2673a25754bc65b4224ffa3e9cd580a4c7cf2659db7ca432d1b69b"},
{file = "lupa-2.4-cp39-cp39-win_amd64.whl", hash = "sha256:ff91e00c077b7e3fc2c5a8b4bcc1f62eaf403f435fc801f32dd610f20332dc0a"},
{file = "lupa-2.4-pp310-pypy310_pp73-macosx_11_0_x86_64.whl", hash = "sha256:889329d0e8e12a1e2529b0258ee69bb1f2ea94aa673b1782f9e12aa55ff3c960"},
{file = "lupa-2.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84d58aedec8996065e3fc6d397c1434e86176feda09ce7a73227506fc89d1c48"},
{file = "lupa-2.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2708eb13b7c0696d9c9e02eea1717c4a24812395d18e6500547ae440da8d7963"},
{file = "lupa-2.4-pp37-pypy37_pp73-macosx_11_0_x86_64.whl", hash = "sha256:834f81a582eabb2242599a9ed222f14d4b17ffff986d42ef8e62cae3e45912c0"},
{file = "lupa-2.4-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5beeb9ee39877302b85226b81fa8038f3a46aba9393c64d08f349bf0455efb73"},
{file = "lupa-2.4-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:6e758c5d7c1ed9adca15791d24c78b27f67fa9b0df0126f4334001c94e2742a2"},
{file = "lupa-2.4-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:eb122ed5a987e579b7fc41382946f1185b78672a2aded1263752b98a0aa11f06"},
{file = "lupa-2.4-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03fc9263ed07229aaa09fa93a2f485f6b9ce5a2364e80088c8c96376bada65ad"},
{file = "lupa-2.4-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a1a5206eb870b5d21285041fe111b8b41b2da789bbf8a50bc45600be24d7a415"},
{file = "lupa-2.4-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:cc521f6d228749fd57649a956f9543a729e462d7693540d4397e6b9f378e3196"},
{file = "lupa-2.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdda690d24aa55e00971bc8443a7d8a28aade14eb01603aed65b345c9dcd92e3"},
{file = "lupa-2.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:71e9cfa60042b3de4dd68f00a2c94dd45e03d3583fb0fc802d9fbbb3b32dd2f7"},
{file = "lupa-2.4.tar.gz", hash = "sha256:5300d21f81aa1bd4d45f55e31dddba3b879895696068a3f84cfcb5fd9148aacd"},
]
[[package]]
@ -335,6 +360,7 @@ version = "3.0.2"
description = "Safely add untrusted strings to HTML/XML markup."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"},
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"},
@ -405,6 +431,7 @@ version = "24.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
@ -416,6 +443,7 @@ version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
@ -431,6 +459,7 @@ version = "3.11"
description = "Python Lex & Yacc"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"},
{file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"},
@ -442,6 +471,7 @@ version = "0.6.1"
description = "Probabilistic data structures in python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pyprobables-0.6.1-py3-none-any.whl", hash = "sha256:090d0c973f9e160f15927e8eb911dabf126285a7a1ecd478b7a9e04149e28392"},
{file = "pyprobables-0.6.1.tar.gz", hash = "sha256:64b4d165d51beff05e716c01231c8a5503297844e58adee8771e5e7af130321d"},
@ -449,13 +479,14 @@ files = [
[[package]]
name = "pytest"
version = "8.3.4"
version = "8.3.5"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
{file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
{file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"},
{file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"},
]
[package.dependencies]
@ -475,6 +506,7 @@ version = "0.24.0"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"},
{file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"},
@ -493,6 +525,7 @@ version = "5.0.0"
description = "Pytest plugin for measuring coverage."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"},
{file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"},
@ -511,6 +544,7 @@ version = "4.1.1"
description = "pytest plugin for generating HTML reports"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pytest_html-4.1.1-py3-none-any.whl", hash = "sha256:c8152cea03bd4e9bee6d525573b67bbc6622967b72b9628dda0ea3e2a0b5dd71"},
{file = "pytest_html-4.1.1.tar.gz", hash = "sha256:70a01e8ae5800f4a074b56a4cb1025c8f4f9b038bba5fe31e3c98eb996686f07"},
@ -531,6 +565,7 @@ version = "3.1.1"
description = "pytest plugin for test session metadata"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b"},
{file = "pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8"},
@ -548,6 +583,7 @@ version = "3.14.0"
description = "Thin-wrapper around the mock package for easier use with pytest"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"},
{file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"},
@ -565,6 +601,7 @@ version = "2.3.1"
description = "pytest plugin to abort hanging tests"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "pytest-timeout-2.3.1.tar.gz", hash = "sha256:12397729125c6ecbdaca01035b9e5239d4db97352320af155b3f5de1ba5165d9"},
{file = "pytest_timeout-2.3.1-py3-none-any.whl", hash = "sha256:68188cb703edfc6a18fad98dc25a3c61e9f24d644b0b70f33af545219fc7813e"},
@ -579,6 +616,7 @@ version = "5.2.1"
description = "Python client for Redis database and key-value store"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"},
{file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"},
@ -597,6 +635,7 @@ version = "2.4.0"
description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"},
{file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
@ -608,6 +647,8 @@ version = "2.2.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_full_version <= \"3.11.0a6\""
files = [
{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"},
@ -645,16 +686,18 @@ files = [
[[package]]
name = "typing-extensions"
version = "4.12.2"
version = "4.13.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
groups = ["main"]
markers = "python_version == \"3.10\""
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
{file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"},
{file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"},
]
[metadata]
lock-version = "2.0"
lock-version = "2.1"
python-versions = "^3.10"
content-hash = "56a32fa3694dd2074c8be21fcc1296ed70ad38977cdf912a93b6e5f0b3ed7e93"

View file

@ -25,7 +25,7 @@ pytest = "^8.3"
pytest-timeout = "^2.3.1"
pytest-asyncio = "^0.24"
pytest-cov = "^5.0"
pytest-mock = "^3.14"
pytest-mock = "^3.14"
pytest-html = "^4.1"
[tool.pytest.ini_options]
@ -45,4 +45,5 @@ generate_report_on_test = true
render_collapsed = "failed,error"
addopts = [
"--self-contained-html",
"--import-mode=importlib",
]

View file

@ -0,0 +1,33 @@
import hypothesis.strategies as st
from .. import test_hypothesis as tests
from ..test_hypothesis.base import BaseTest, common_commands, commands
from ..test_hypothesis.test_string import string_commands
bad_commands = (
# redis-py splits the command on spaces, and hangs if that ends up being an empty list
commands(
st.text().filter(lambda x: bool(x.split())), st.lists(st.binary() | st.text())
)
)
class TestJoint(BaseTest):
create_command_strategy = (
tests.TestString.create_command_strategy
| tests.TestHash.create_command_strategy
| tests.TestList.create_command_strategy
| tests.TestSet.create_command_strategy
| tests.TestZSet.create_command_strategy
)
command_strategy = (
tests.TestServer.server_commands
| tests.TestConnection.connection_commands
| string_commands
| tests.TestHash.hash_commands
| tests.TestList.list_commands
| tests.TestSet.set_commands
| tests.TestZSet.zset_commands
| common_commands
| bad_commands
)

View file

@ -0,0 +1,19 @@
__all__ = [
"TestConnection",
"TestHash",
"TestList",
"TestServer",
"TestSet",
"TestString",
"TestTransaction",
"TestZSet",
]
from .test_connection import TestConnection
from .test_hash import TestHash
from .test_list import TestList
from .test_server import TestServer
from .test_set import TestSet
from .test_string import TestString
from .test_transaction import TestTransaction
from .test_zset import TestZSet

View file

@ -1,5 +1,6 @@
import functools
import math
import string
import sys
from typing import Any, List, Tuple, Type, Optional
@ -34,11 +35,15 @@ fields = sample_attr("fields")
values = sample_attr("values")
scores = sample_attr("scores")
eng_text = st.builds(
lambda x: x.encode(), st.text(alphabet=string.ascii_letters, min_size=1)
)
ints = st.integers(min_value=MIN_INT, max_value=MAX_INT)
int_as_bytes = st.builds(lambda x: str(_default_normalize(x)).encode(), ints)
float_as_bytes = st.builds(
lambda x: repr(_default_normalize(x)).encode(), st.floats(width=32)
floats = st.floats(
width=32, allow_nan=False, allow_subnormal=False, allow_infinity=False
)
float_as_bytes = st.builds(lambda x: repr(_default_normalize(x)).encode(), floats)
counts = st.integers(min_value=-3, max_value=3) | ints
# Redis has an integer overflow bug in swapdb, so we confine the numbers to
# a limited range (https://github.com/antirez/redis/issues/5737).
@ -51,8 +56,8 @@ patterns = st.text(
) | st.binary().filter(lambda x: b"\0" not in x)
# Redis has integer overflow bugs in time computations, which is why we set a maximum.
expires_seconds = st.integers(min_value=100000, max_value=MAX_INT)
expires_ms = st.integers(min_value=100000000, max_value=MAX_INT)
expires_seconds = st.integers(min_value=5, max_value=1_000)
expires_ms = st.integers(min_value=5_000, max_value=50_000)
class WrappedException:
@ -98,6 +103,8 @@ def _sort_list(lst):
def _normalize_if_number(x):
if isinstance(x, list):
return [_normalize_if_number(i) for i in x]
try:
res = float(x)
return x if math.isnan(res) else res
@ -149,6 +156,7 @@ class Command:
b"sinter",
b"sunion",
b"smembers",
b"hexpire",
}
if command in unordered:
return _sort_list
@ -159,7 +167,7 @@ class Command:
def testable(self) -> bool:
"""Whether this command is suitable for a test.
The fuzzer can create commands with behaviour that is non-deterministic, not supported, or which hits redis bugs.
The fuzzer can create commands with behavior that is non-deterministic, not supported, or which hits redis bugs.
"""
N = len(self.args)
if N == 0:
@ -201,20 +209,6 @@ common_commands = (
| commands(st.just("sort"), keys, *zero_or_more("asc", "desc", "alpha"))
)
attrs = st.fixed_dictionaries(
{
"keys": st.lists(st.binary(), min_size=2, max_size=5, unique=True),
"fields": st.lists(st.binary(), min_size=2, max_size=5, unique=True),
"values": st.lists(
st.binary() | int_as_bytes | float_as_bytes,
min_size=2,
max_size=5,
unique=True,
),
"scores": st.lists(st.floats(width=32), min_size=2, max_size=5, unique=True),
}
)
@hypothesis.settings(max_examples=1000)
class CommonMachine(hypothesis.stateful.RuleBasedStateMachine):
@ -291,6 +285,16 @@ class CommonMachine(hypothesis.stateful.RuleBasedStateMachine):
for n, r, f in zip(self.transaction_normalize, real_result, fake_result):
assert n(f) == n(r)
self.transaction_normalize = []
elif isinstance(fake_result, list):
assert len(fake_result) == len(real_result), (
f"Discrepancy when running command {command}, fake({fake_result}) != real({real_result})",
)
for i in range(len(fake_result)):
assert fake_result[i] == real_result[i] or (
type(fake_result[i]) is float
and fake_result[i] == pytest.approx(real_result[i])
), f"Discrepancy when running command {command}, fake({fake_result}) != real({real_result})"
else:
assert fake_result == real_result or (
type(fake_result) is float and fake_result == pytest.approx(real_result)
@ -307,7 +311,26 @@ class CommonMachine(hypothesis.stateful.RuleBasedStateMachine):
):
self.transaction_normalize = []
@initialize(attrs=attrs)
@initialize(
attrs=st.fixed_dictionaries(
dict(
keys=st.lists(eng_text, min_size=2, max_size=5, unique=True),
fields=st.lists(eng_text, min_size=2, max_size=5, unique=True),
values=st.lists(
eng_text | int_as_bytes | float_as_bytes,
min_size=2,
max_size=5,
unique=True,
),
scores=st.lists(
floats,
min_size=2,
max_size=5,
unique=True,
),
)
)
)
def init_attrs(self, attrs):
for key, value in attrs.items():
setattr(self, key, value)

View file

@ -1,6 +1,6 @@
import hypothesis.strategies as st
from test.test_hypothesis.base import BaseTest, commands, values, common_commands
from .base import BaseTest, commands, values, common_commands
class TestConnection(BaseTest):

View file

@ -1,6 +1,6 @@
import hypothesis.strategies as st
from test.test_hypothesis.base import (
from .base import (
BaseTest,
commands,
values,
@ -37,7 +37,7 @@ class TestHash(BaseTest):
expires_seconds,
st.just("fields"),
st.just(2),
st.lists(fields, min_size=2, max_size=2),
st.lists(fields, min_size=2, max_size=2, unique=True),
)
)
create_command_strategy = commands(

View file

@ -1,38 +0,0 @@
import hypothesis.strategies as st
from test.test_hypothesis.base import BaseTest, common_commands, commands
from test.test_hypothesis.test_connection import TestConnection
from test.test_hypothesis.test_hash import TestHash
from test.test_hypothesis.test_list import TestList
from test.test_hypothesis.test_server import TestServer
from test.test_hypothesis.test_set import TestSet
from test.test_hypothesis.test_string import TestString, string_commands
from test.test_hypothesis.test_zset import TestZSet
bad_commands = (
# redis-py splits the command on spaces, and hangs if that ends up being an empty list
commands(
st.text().filter(lambda x: bool(x.split())), st.lists(st.binary() | st.text())
)
)
class TestJoint(BaseTest):
create_command_strategy = (
TestString.create_command_strategy
| TestHash.create_command_strategy
| TestList.create_command_strategy
| TestSet.create_command_strategy
| TestZSet.create_command_strategy
)
command_strategy = (
TestServer.server_commands
| TestConnection.connection_commands
| string_commands
| TestHash.hash_commands
| TestList.list_commands
| TestSet.set_commands
| TestZSet.zset_commands
| common_commands
| bad_commands
)

View file

@ -1,6 +1,6 @@
import hypothesis.strategies as st
from test.test_hypothesis.base import (
from .base import (
BaseTest,
commands,
values,

View file

@ -1,13 +1,13 @@
import hypothesis.strategies as st
from test.test_hypothesis.base import (
from .base import (
BaseTest,
commands,
common_commands,
keys,
values,
)
from test.test_hypothesis.test_string import string_commands
from .test_string import string_commands
class TestServer(BaseTest):

View file

@ -1,6 +1,6 @@
import hypothesis.strategies as st
from test.test_hypothesis.base import (
from .base import (
BaseTest,
commands,
keys,

View file

@ -1,6 +1,6 @@
import hypothesis.strategies as st
from test.test_hypothesis.base import (
from .base import (
BaseTest,
commands,
values,

View file

@ -1,6 +1,6 @@
import hypothesis.strategies as st
from test.test_hypothesis.base import (
from .base import (
BaseTest,
commands,
values,
@ -12,7 +12,7 @@ from test.test_hypothesis.base import (
expires_seconds,
expires_ms,
)
from test.test_hypothesis.test_string import TestString
from .test_string import TestString
class TestTransaction(BaseTest):

View file

@ -2,7 +2,7 @@ import operator
import hypothesis.strategies as st
from test.test_hypothesis.base import (
from .base import (
BaseTest,
commands,
keys,

View file

@ -204,13 +204,6 @@ def test_bitpos_wrong_arguments(r: redis.Redis):
raw_command(r, "bitpos", key)
def test_bitfield_empty(r: redis.Redis):
key = "key:bitfield"
assert r.bitfield(key).execute() == []
for overflow in ("wrap", "sat", "fail"):
assert raw_command(r, "bitfield", key, "overflow", overflow) == []
def test_bitfield_wrong_arguments(r: redis.Redis):
key = "key:bitfield:wrong:args"
with pytest.raises(redis.ResponseError):

View file

@ -13,6 +13,7 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gookit/color v1.4.2 // indirect
github.com/influxdata/tdigest v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect

View file

@ -9,8 +9,11 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk=
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
github.com/influxdata/tdigest v0.0.1 h1:XpFptwYmnEKUqmkcDjrzffswZ3nvNeevbUSLPP/ZzIY=
github.com/influxdata/tdigest v0.0.1/go.mod h1:Z0kXnxzbTC2qrx4NaIzYkE1k66+6oEDQTvL95hQFh5Y=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -27,11 +30,15 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -19,20 +19,62 @@ var fPace = flag.Bool("pace", true, "whether to pace the traffic according to th
var fSkip = flag.Uint("skip", 0, "skip N records")
func RenderTable(area *pterm.AreaPrinter, files []string, workers []FileWorker) {
tableData := pterm.TableData{{"file", "parsed", "processed", "delayed", "clients"}}
tableData := pterm.TableData{{"file", "parsed", "processed", "delayed", "clients", "avg(us)", "p50(us)", "p75(us)", "p90(us)", "p99(us)"}}
for i := range workers {
workers[i].latencyMu.Lock()
avg := 0.0
if workers[i].latencyCount > 0 {
avg = workers[i].latencySum / float64(workers[i].latencyCount)
}
p50 := workers[i].latencyDigest.Quantile(0.5)
p75 := workers[i].latencyDigest.Quantile(0.75)
p90 := workers[i].latencyDigest.Quantile(0.9)
p99 := workers[i].latencyDigest.Quantile(0.99)
workers[i].latencyMu.Unlock()
tableData = append(tableData, []string{
files[i],
fmt.Sprint(atomic.LoadUint64(&workers[i].parsed)),
fmt.Sprint(atomic.LoadUint64(&workers[i].processed)),
fmt.Sprint(atomic.LoadUint64(&workers[i].delayed)),
fmt.Sprint(atomic.LoadUint64(&workers[i].clients)),
fmt.Sprintf("%.0f", avg),
fmt.Sprintf("%.0f", p50),
fmt.Sprintf("%.0f", p75),
fmt.Sprintf("%.0f", p90),
fmt.Sprintf("%.0f", p99),
})
}
content, _ := pterm.DefaultTable.WithHasHeader().WithBoxed().WithData(tableData).Srender()
area.Update(content)
}
// RenderPipelineRangesTable renders the latency digests for each pipeline range
func RenderPipelineRangesTable(area *pterm.AreaPrinter, files []string, workers []FileWorker) {
tableData := pterm.TableData{{"file", "Pipeline Range", "p50(us)", "p75(us)", "p90(us)", "p99(us)"}}
for i := range workers {
workers[i].latencyMu.Lock()
for _, rng := range pipelineRanges {
if digest, ok := workers[i].perRange[rng.label]; ok {
p50 := digest.Quantile(0.5)
p75 := digest.Quantile(0.75)
p90 := digest.Quantile(0.9)
p99 := digest.Quantile(0.99)
tableData = append(tableData, []string{
files[i],
rng.label,
fmt.Sprintf("%.0f", p50),
fmt.Sprintf("%.0f", p75),
fmt.Sprintf("%.0f", p90),
fmt.Sprintf("%.0f", p99),
})
}
}
workers[i].latencyMu.Unlock()
}
content, _ := pterm.DefaultTable.WithHasHeader().WithBoxed().WithData(tableData).Srender()
area.Update(content)
}
func Run(files []string) {
timeOffset := time.Now().Add(500 * time.Millisecond).Sub(DetermineBaseTime(files))
fmt.Println("Offset -> ", timeOffset)
@ -64,6 +106,8 @@ func Run(files []string) {
}
RenderTable(area, files, workers) // to show last stats
areaPipelineRanges, _ := pterm.DefaultArea.WithCenter().Start()
RenderPipelineRangesTable(areaPipelineRanges, files, workers) // to render per pipeline-range latency digests
}
func Print(files []string) {

View file

@ -10,6 +10,7 @@ import (
"sync/atomic"
"time"
"github.com/influxdata/tdigest"
"github.com/redis/go-redis/v9"
)
@ -47,6 +48,18 @@ type ClientWorker struct {
pipe redis.Pipeliner
}
// Pipeline length ranges for summary
var pipelineRanges = []struct {
label string
min int
max int // inclusive, except last
}{
{"0-29", 0, 29},
{"30-79", 30, 79},
{"80-199", 80, 199},
{"200+", 200, 1 << 30},
}
// Handles a single file and distributes messages to clients
type FileWorker struct {
clientGroup sync.WaitGroup
@ -56,6 +69,33 @@ type FileWorker struct {
delayed uint64
parsed uint64
clients uint64
latencyDigest *tdigest.TDigest
latencyMu sync.Mutex
latencySum float64 // sum of all batch latencies (microseconds)
latencyCount uint64 // number of batches
// per-pipeline-range latency digests
perRange map[string]*tdigest.TDigest
}
// Helper function to track latency and update digests
func trackLatency(worker *FileWorker, batchLatency float64, size int) {
worker.latencyMu.Lock()
defer worker.latencyMu.Unlock()
worker.latencyDigest.Add(batchLatency, 1)
worker.latencySum += batchLatency
worker.latencyCount++
// Add to per-range digest
if worker.perRange != nil {
for _, rng := range pipelineRanges {
if size >= rng.min && size <= rng.max {
worker.perRange[rng.label].Add(batchLatency, 1)
break
}
}
}
}
func (c *ClientWorker) Run(pace bool, worker *FileWorker) {
@ -79,13 +119,19 @@ func (c *ClientWorker) Run(pace bool, worker *FileWorker) {
if msg.HasMore == 0 {
size := c.pipe.Len()
start := time.Now()
c.pipe.Exec(context.Background())
batchLatency := float64(time.Since(start).Microseconds())
trackLatency(worker, batchLatency, size)
c.processed += uint(size)
}
}
if size := c.pipe.Len(); size >= 0 {
start := time.Now()
c.pipe.Exec(context.Background())
batchLatency := float64(time.Since(start).Microseconds())
trackLatency(worker, batchLatency, size)
c.processed += uint(size)
}
@ -106,6 +152,11 @@ func NewClient(w *FileWorker, pace bool) *ClientWorker {
}
func (w *FileWorker) Run(file string, wg *sync.WaitGroup) {
w.latencyDigest = tdigest.NewWithCompression(1000)
w.perRange = make(map[string]*tdigest.TDigest)
for _, rng := range pipelineRanges {
w.perRange[rng.label] = tdigest.NewWithCompression(500)
}
clients := make(map[uint32]*ClientWorker, 0)
recordId := uint64(0)
err := parseRecords(file, func(r Record) bool {