diff --git a/pyproject.toml b/pyproject.toml
index 5bac96d..2336c45 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,14 +15,21 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Operating System :: Microsoft :: Windows",
]
-version = "0.0.2rc16"
+version = "0.0.2rc17"
readme = {file = "README.md", content-type = "text/markdown"}
dependencies = [
"aind_behavior_services>=0.13.2",
"pydantic-settings",
+ "aind-behavior-dynamic-foraging-curricula"
]
+[tool.uv.sources]
+aind-behavior-dynamic-foraging-curricula = { workspace = true }
+
+[tool.uv.workspace]
+members = ["workspaces/*"]
+
[project.urls]
Documentation = "https://allenneuraldynamics.github.io/Aind.Behavior.DynamicForaging/"
Repository = "https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging/"
diff --git a/schema/aind_behavior_dynamic_foraging.json b/schema/aind_behavior_dynamic_foraging.json
index 588550b..295857a 100644
--- a/schema/aind_behavior_dynamic_foraging.json
+++ b/schema/aind_behavior_dynamic_foraging.json
@@ -9,8 +9,8 @@
"type": "string"
},
"version": {
- "const": "0.0.2-rc16",
- "default": "0.0.2-rc16",
+ "const": "0.0.2-rc17",
+ "default": "0.0.2-rc17",
"title": "Version",
"type": "string"
},
@@ -152,8 +152,8 @@
"description": "Parameters of the task logic"
},
"version": {
- "const": "0.0.2-rc16",
- "default": "0.0.2-rc16",
+ "const": "0.0.2-rc17",
+ "default": "0.0.2-rc17",
"title": "Version",
"type": "string"
},
@@ -3165,7 +3165,7 @@
"rate": 1.0
},
"truncation_parameters": {
- "max": 2.0,
+ "max": 1.0,
"min": 1.0,
"truncation_mode": "exclude"
},
diff --git a/src/Extensions/AindBehaviorDynamicForaging.Generated.cs b/src/Extensions/AindBehaviorDynamicForaging.Generated.cs
index 7b61731..6d548a8 100644
--- a/src/Extensions/AindBehaviorDynamicForaging.Generated.cs
+++ b/src/Extensions/AindBehaviorDynamicForaging.Generated.cs
@@ -50,7 +50,7 @@ public partial class AindDynamicForagingRig
public AindDynamicForagingRig()
{
_aindBehaviorServicesPkgVersion = "0.13.2";
- _version = "0.0.2-rc16";
+ _version = "0.0.2-rc17";
_triggeredCameraController = new CameraControllerSpinnakerCamera();
_harpBehavior = new HarpBehavior();
_harpClockGenerator = new HarpWhiteRabbit();
@@ -433,7 +433,7 @@ public AindDynamicForagingTaskLogic()
_name = "AindDynamicForaging";
_description = "";
_taskParameters = new AindDynamicForagingTaskParameters();
- _version = "0.0.2-rc16";
+ _version = "0.0.2-rc17";
}
protected AindDynamicForagingTaskLogic(AindDynamicForagingTaskLogic other)
diff --git a/src/Extensions/HarpBehavior.bonsai b/src/Extensions/HarpBehavior.bonsai
index a0623e7..90b1385 100644
--- a/src/Extensions/HarpBehavior.bonsai
+++ b/src/Extensions/HarpBehavior.bonsai
@@ -19,7 +19,7 @@
On
true
On
- Disabled
+ Enabled
false
COMx
diff --git a/src/Extensions/Logging.bonsai b/src/Extensions/Logging.bonsai
index 5a56970..26f8a32 100644
--- a/src/Extensions/Logging.bonsai
+++ b/src/Extensions/Logging.bonsai
@@ -771,62 +771,48 @@
-
- DumpMetadata
+
+ AdditionalSoftwareEvents
- RngSeedValue
+ ManipulatorPosition
+
+
+
+ 1
+
- RngSeed
+ InitialManipulatorPosition
SoftwareEvent
- EndExperiment
-
-
-
- ExperimentCompleted
-
+ RngSeedValue
- EndSession
+ RngSeed
SoftwareEvent
-
-
-
-
-
-
-
-
-
-
-
- AdditionalSoftwareEvents
-
-
- ManipulatorPosition
+ EndExperiment
-
- 1
+
+ ExperimentCompleted
- InitialManipulatorPosition
+ EndSession
@@ -837,9 +823,23 @@
+
+
+
+
+
+
+ Wait for logger to initialize
+
+
+
+
+ PT0.5S
+
+
@@ -850,6 +850,8 @@
+
+
diff --git a/src/Extensions/TaskEngine.bonsai b/src/Extensions/TaskEngine.bonsai
index 3043982..5157a6f 100644
--- a/src/Extensions/TaskEngine.bonsai
+++ b/src/Extensions/TaskEngine.bonsai
@@ -472,21 +472,21 @@
ThisTrial
-
- EnableFastRetract
-
Item2
+
+ EnableFastRetract
+
-
-
-
+
+
+
@@ -600,22 +600,22 @@
ThisTrial
-
- EnableFastRetract
-
Item2
+
+ EnableFastRetract
+
-
-
-
+
+
+
@@ -693,15 +693,15 @@
ThisTrial
-
- SecondaryReinforcer
-
Item2
+
+ SecondaryReinforcer
+
IsNotNull
it != null
@@ -709,9 +709,9 @@
-
-
-
+
+
+
@@ -787,15 +787,15 @@
ThisTrial
-
- SecondaryReinforcer
-
Item2
+
+ SecondaryReinforcer
+
IsNull
it == null
@@ -803,9 +803,9 @@
-
-
-
+
+
+
@@ -984,36 +984,6 @@
ThisTrial
-
- InterTrialIntervalDuration
-
-
-
-
-
-
-
-
-
-
-
- PT0S
-
-
-
-
-
-
-
- trial_generator
-
-
-
-
-
-
- ThisTrial
-
@@ -1053,22 +1023,34 @@
GlobalTrialOutcome
-
- TrialOutcome
+
+ thisTrialOutcome
+
+
+ ThisTrial
-
+
+ 1
+
+
+
+ InterTrialIntervalDuration
-
+
+
+
+
+
+
-
- update
- true
+
+ PT5S
-
+
SampleNextTrial
@@ -1080,24 +1062,72 @@
trial_generator
+
+
+
+
+ thisTrialOutcome
+
+
+ TrialOutcome
+
+
+
+
+
+
+
- next
+ update
true
-
-
-
-
+
+ SampleNextTrial
+
+
+
+
+
+
+
+ trial_generator
+
+
+
+
+ next
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
@@ -1154,9 +1184,6 @@
-
- ThisTrial
-
MoveSpout
@@ -1181,21 +1208,15 @@
-
-
-
-
- Item2
-
- GlobalTrialOutcome
+ thisTrialOutcome
-
+
Label
- new( Item1 as NextTrial, Item2 as ThisTrialOutcome)
+ new(Item2 as NextTrial, Item3 as ThisTrialOutcome)
@@ -1220,45 +1241,36 @@
-
+
-
-
+
+
-
-
-
+
+
+
-
-
+
-
-
-
-
-
+
+
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
@@ -1267,6 +1279,12 @@
1
+
+ NextTrial
+
+
+ ThisTrial
+
@@ -1287,22 +1305,14 @@
+
+
-
- EndOnNull
- it.NextTrial == null
-
-
-
- 1
-
-
-
@@ -1330,9 +1340,6 @@
-
-
-
diff --git a/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/warmup_trial_generator.py b/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/warmup_trial_generator.py
index df35ad1..a57f011 100644
--- a/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/warmup_trial_generator.py
+++ b/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/warmup_trial_generator.py
@@ -43,7 +43,7 @@ class WarmupTrialGeneratorSpec(BlockBasedTrialGeneratorSpec):
block_len: Distribution = Field(
default=ExponentialDistribution(
distribution_parameters=ExponentialDistributionParameters(rate=1),
- truncation_parameters=TruncationParameters(min=1, max=2),
+ truncation_parameters=TruncationParameters(min=1, max=1),
),
description="Distribution describing block length.",
)
diff --git a/src/main.bonsai b/src/main.bonsai
index fac1562..733c848 100644
--- a/src/main.bonsai
+++ b/src/main.bonsai
@@ -7,6 +7,7 @@
xmlns:p2="clr-namespace:;assembly=Extensions"
xmlns:p3="clr-namespace:System.Reactive;assembly=System.Reactive.Core"
xmlns:p4="clr-namespace:AllenNeuralDynamics.AindBehaviorServices.DataTypes;assembly=AllenNeuralDynamics.AindBehaviorServices"
+ xmlns:scr="clr-namespace:Bonsai.Scripting.Expressions;assembly=Bonsai.Scripting.Expressions"
xmlns="https://bonsai-rx.org/2018/workflow">
@@ -332,9 +333,19 @@
+
+ isNull
+ it == null
+
+
+
+ 1
+
+
+
- PT1S
+ PT2S
@@ -371,13 +382,16 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/uv.lock b/uv.lock
index a896652..a1ca616 100644
--- a/uv.lock
+++ b/uv.lock
@@ -13,6 +13,12 @@ resolution-markers = [
"python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'win32'",
]
+[manifest]
+members = [
+ "aind-behavior-dynamic-foraging",
+ "aind-behavior-dynamic-foraging-curricula",
+]
+
[[package]]
name = "accessible-pygments"
version = "0.0.5"
@@ -27,23 +33,24 @@ wheels = [
[[package]]
name = "aind-behavior-curriculum"
-version = "0.0.37"
+version = "0.0.38"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jinja2" },
{ name = "pydantic" },
{ name = "semver" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/f0/ac/cb5073f94c6b41b88c6e91d8bf41f7c140d659a0eb1191bc0a91cace60db/aind_behavior_curriculum-0.0.37.tar.gz", hash = "sha256:a6d8fd58b4d172655bc445eefefe8ba6a7966f2b4303afe138dce3c07ec45a13", size = 139105, upload-time = "2025-12-05T22:51:07.253Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/49/ad/812f8cd33857366d7b1da81443ed60777992e591438c7104ad9fe0d4ac9f/aind_behavior_curriculum-0.0.38.tar.gz", hash = "sha256:11cce6a455ee3a2c0464e5f0ff170c44f1f9caf50cd3f1bf9bd9df7a41248e0d", size = 139193, upload-time = "2026-03-09T10:56:32.906Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e5/83/c51f680b86136dca811816746568de0feadbbe03d27a3213500a744ff047/aind_behavior_curriculum-0.0.37-py3-none-any.whl", hash = "sha256:349e973d52a450523b03a4b73b8371f72c80e9d43702eb62005f250e51663ca7", size = 48014, upload-time = "2025-12-05T22:51:06.351Z" },
+ { url = "https://files.pythonhosted.org/packages/74/36/53daf76d7a3a27245fdee89e7900bbb13b9db0327af22d5f3e10c3273720/aind_behavior_curriculum-0.0.38-py3-none-any.whl", hash = "sha256:ec77803fc0cad1c9f430bad3f1d2404dd324ce24ee7970b8d8337de2d6ad2d3c", size = 48048, upload-time = "2026-03-09T10:56:31.299Z" },
]
[[package]]
name = "aind-behavior-dynamic-foraging"
-version = "0.0.2rc16"
+version = "0.0.2rc17"
source = { editable = "." }
dependencies = [
+ { name = "aind-behavior-dynamic-foraging-curricula" },
{ name = "aind-behavior-services" },
{ name = "pydantic-settings" },
]
@@ -72,6 +79,7 @@ docs = [
[package.metadata]
requires-dist = [
+ { name = "aind-behavior-dynamic-foraging-curricula", editable = "workspaces/aind-behavior-dynamic-foraging-curricula" },
{ name = "aind-behavior-services", specifier = ">=0.13.2" },
{ name = "contraqctor", marker = "extra == 'data'", specifier = ">=0.5.3" },
{ name = "pydantic-settings" },
@@ -94,6 +102,55 @@ docs = [
{ name = "sphinx-jsonschema" },
]
+[[package]]
+name = "aind-behavior-dynamic-foraging-curricula"
+version = "0.2.1"
+source = { editable = "workspaces/aind-behavior-dynamic-foraging-curricula" }
+dependencies = [
+ { name = "aind-behavior-curriculum" },
+ { name = "aind-behavior-dynamic-foraging" },
+ { name = "numpy" },
+ { name = "pydantic-settings" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "codespell" },
+ { name = "pytest" },
+ { name = "pytest-cov" },
+ { name = "ruff" },
+]
+docs = [
+ { name = "mkdocs" },
+ { name = "mkdocs-material" },
+ { name = "mkdocstrings", extra = ["python"] },
+ { name = "pymdown-extensions" },
+ { name = "ruff" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "aind-behavior-curriculum", specifier = ">=0.0.38" },
+ { name = "aind-behavior-dynamic-foraging", editable = "." },
+ { name = "numpy", specifier = ">=2.4.2" },
+ { name = "pydantic-settings" },
+]
+
+[package.metadata.requires-dev]
+dev = [
+ { name = "codespell" },
+ { name = "pytest" },
+ { name = "pytest-cov" },
+ { name = "ruff" },
+]
+docs = [
+ { name = "mkdocs" },
+ { name = "mkdocs-material" },
+ { name = "mkdocstrings", extras = ["python"] },
+ { name = "pymdown-extensions" },
+ { name = "ruff" },
+]
+
[[package]]
name = "aind-behavior-services"
version = "0.13.2"
@@ -289,6 +346,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" },
]
+[[package]]
+name = "backrefs"
+version = "6.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4e/a6/e325ec73b638d3ede4421b5445d4a0b8b219481826cc079d510100af356c/backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49", size = 7012303, upload-time = "2026-02-16T19:10:15.828Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1b/39/3765df263e08a4df37f4f43cb5aa3c6c17a4bdd42ecfe841e04c26037171/backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8", size = 381075, upload-time = "2026-02-16T19:10:04.322Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be", size = 392874, upload-time = "2026-02-16T19:10:06.314Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90", size = 398787, upload-time = "2026-02-16T19:10:08.274Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b", size = 400747, upload-time = "2026-02-16T19:10:09.791Z" },
+ { url = "https://files.pythonhosted.org/packages/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7", size = 412602, upload-time = "2026-02-16T19:10:12.317Z" },
+ { url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" },
+]
+
[[package]]
name = "beautifulsoup4"
version = "4.14.3"
@@ -599,6 +670,110 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/9a/94fa67d2aecf7703a68be18b97b8d1af09ec9c26c73b2c21d5e881e46353/contraqctor-0.5.3-py3-none-any.whl", hash = "sha256:9306521efd6165007409319be39d4a8d9e286028693237584dbfc9f87e974292", size = 67638, upload-time = "2025-11-10T17:31:18.482Z" },
]
+[[package]]
+name = "coverage"
+version = "7.13.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b4/ad/b59e5b451cf7172b8d1043dc0fa718f23aab379bc1521ee13d4bd9bfa960/coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053", size = 219278, upload-time = "2026-02-09T12:56:31.673Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/17/0cb7ca3de72e5f4ef2ec2fa0089beafbcaaaead1844e8b8a63d35173d77d/coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11", size = 219783, upload-time = "2026-02-09T12:56:33.104Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/63/325d8e5b11e0eaf6d0f6a44fad444ae58820929a9b0de943fa377fe73e85/coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa", size = 250200, upload-time = "2026-02-09T12:56:34.474Z" },
+ { url = "https://files.pythonhosted.org/packages/76/53/c16972708cbb79f2942922571a687c52bd109a7bd51175aeb7558dff2236/coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7", size = 252114, upload-time = "2026-02-09T12:56:35.749Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/c2/7ab36d8b8cc412bec9ea2d07c83c48930eb4ba649634ba00cb7e4e0f9017/coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00", size = 254220, upload-time = "2026-02-09T12:56:37.796Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/4d/cf52c9a3322c89a0e6febdfbc83bb45c0ed3c64ad14081b9503adee702e7/coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef", size = 256164, upload-time = "2026-02-09T12:56:39.016Z" },
+ { url = "https://files.pythonhosted.org/packages/78/e9/eb1dd17bd6de8289df3580e967e78294f352a5df8a57ff4671ee5fc3dcd0/coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903", size = 250325, upload-time = "2026-02-09T12:56:40.668Z" },
+ { url = "https://files.pythonhosted.org/packages/71/07/8c1542aa873728f72267c07278c5cc0ec91356daf974df21335ccdb46368/coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f", size = 251913, upload-time = "2026-02-09T12:56:41.97Z" },
+ { url = "https://files.pythonhosted.org/packages/74/d7/c62e2c5e4483a748e27868e4c32ad3daa9bdddbba58e1bc7a15e252baa74/coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299", size = 249974, upload-time = "2026-02-09T12:56:43.323Z" },
+ { url = "https://files.pythonhosted.org/packages/98/9f/4c5c015a6e98ced54efd0f5cf8d31b88e5504ecb6857585fc0161bb1e600/coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505", size = 253741, upload-time = "2026-02-09T12:56:45.155Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/59/0f4eef89b9f0fcd9633b5d350016f54126ab49426a70ff4c4e87446cabdc/coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6", size = 249695, upload-time = "2026-02-09T12:56:46.636Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/2c/b7476f938deb07166f3eb281a385c262675d688ff4659ad56c6c6b8e2e70/coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9", size = 250599, upload-time = "2026-02-09T12:56:48.13Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/34/c3420709d9846ee3785b9f2831b4d94f276f38884032dca1457fa83f7476/coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9", size = 221780, upload-time = "2026-02-09T12:56:50.479Z" },
+ { url = "https://files.pythonhosted.org/packages/61/08/3d9c8613079d2b11c185b865de9a4c1a68850cfda2b357fae365cf609f29/coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f", size = 222715, upload-time = "2026-02-09T12:56:51.815Z" },
+ { url = "https://files.pythonhosted.org/packages/18/1a/54c3c80b2f056164cc0a6cdcb040733760c7c4be9d780fe655f356f433e4/coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f", size = 221385, upload-time = "2026-02-09T12:56:53.194Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" },
+ { url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" },
+ { url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" },
+ { url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" },
+ { url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" },
+ { url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" },
+ { url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" },
+ { url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" },
+ { url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" },
+ { url = "https://files.pythonhosted.org/packages/db/23/aad45061a31677d68e47499197a131eea55da4875d16c1f42021ab963503/coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9", size = 219474, upload-time = "2026-02-09T12:57:19.332Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/70/9b8b67a0945f3dfec1fd896c5cefb7c19d5a3a6d74630b99a895170999ae/coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac", size = 219844, upload-time = "2026-02-09T12:57:20.66Z" },
+ { url = "https://files.pythonhosted.org/packages/97/fd/7e859f8fab324cef6c4ad7cff156ca7c489fef9179d5749b0c8d321281c2/coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea", size = 250832, upload-time = "2026-02-09T12:57:22.007Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/dc/b2442d10020c2f52617828862d8b6ee337859cd8f3a1f13d607dddda9cf7/coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b", size = 253434, upload-time = "2026-02-09T12:57:23.339Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/88/6728a7ad17428b18d836540630487231f5470fb82454871149502f5e5aa2/coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525", size = 254676, upload-time = "2026-02-09T12:57:24.774Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/bc/21244b1b8cedf0dff0a2b53b208015fe798d5f2a8d5348dbfece04224fff/coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242", size = 256807, upload-time = "2026-02-09T12:57:26.125Z" },
+ { url = "https://files.pythonhosted.org/packages/97/a0/ddba7ed3251cff51006737a727d84e05b61517d1784a9988a846ba508877/coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148", size = 251058, upload-time = "2026-02-09T12:57:27.614Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/55/e289addf7ff54d3a540526f33751951bf0878f3809b47f6dfb3def69c6f7/coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a", size = 252805, upload-time = "2026-02-09T12:57:29.066Z" },
+ { url = "https://files.pythonhosted.org/packages/13/4e/cc276b1fa4a59be56d96f1dabddbdc30f4ba22e3b1cd42504c37b3313255/coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23", size = 250766, upload-time = "2026-02-09T12:57:30.522Z" },
+ { url = "https://files.pythonhosted.org/packages/94/44/1093b8f93018f8b41a8cf29636c9292502f05e4a113d4d107d14a3acd044/coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80", size = 254923, upload-time = "2026-02-09T12:57:31.946Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/55/ea2796da2d42257f37dbea1aab239ba9263b31bd91d5527cdd6db5efe174/coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea", size = 250591, upload-time = "2026-02-09T12:57:33.842Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/fa/7c4bb72aacf8af5020675aa633e59c1fbe296d22aed191b6a5b711eb2bc7/coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a", size = 252364, upload-time = "2026-02-09T12:57:35.743Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/38/a8d2ec0146479c20bbaa7181b5b455a0c41101eed57f10dd19a78ab44c80/coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d", size = 222010, upload-time = "2026-02-09T12:57:37.25Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/0c/dbfafbe90a185943dcfbc766fe0e1909f658811492d79b741523a414a6cc/coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd", size = 222818, upload-time = "2026-02-09T12:57:38.734Z" },
+ { url = "https://files.pythonhosted.org/packages/04/d1/934918a138c932c90d78301f45f677fb05c39a3112b96fd2c8e60503cdc7/coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af", size = 221438, upload-time = "2026-02-09T12:57:40.223Z" },
+ { url = "https://files.pythonhosted.org/packages/52/57/ee93ced533bcb3e6df961c0c6e42da2fc6addae53fb95b94a89b1e33ebd7/coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d", size = 220165, upload-time = "2026-02-09T12:57:41.639Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/e0/969fc285a6fbdda49d91af278488d904dcd7651b2693872f0ff94e40e84a/coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12", size = 220516, upload-time = "2026-02-09T12:57:44.215Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/b8/9531944e16267e2735a30a9641ff49671f07e8138ecf1ca13db9fd2560c7/coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b", size = 261804, upload-time = "2026-02-09T12:57:45.989Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/f3/e63df6d500314a2a60390d1989240d5f27318a7a68fa30ad3806e2a9323e/coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9", size = 263885, upload-time = "2026-02-09T12:57:47.42Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/67/7654810de580e14b37670b60a09c599fa348e48312db5b216d730857ffe6/coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092", size = 266308, upload-time = "2026-02-09T12:57:49.345Z" },
+ { url = "https://files.pythonhosted.org/packages/37/6f/39d41eca0eab3cc82115953ad41c4e77935286c930e8fad15eaed1389d83/coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9", size = 267452, upload-time = "2026-02-09T12:57:50.811Z" },
+ { url = "https://files.pythonhosted.org/packages/50/6d/39c0fbb8fc5cd4d2090811e553c2108cf5112e882f82505ee7495349a6bf/coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26", size = 261057, upload-time = "2026-02-09T12:57:52.447Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/a2/60010c669df5fa603bb5a97fb75407e191a846510da70ac657eb696b7fce/coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2", size = 263875, upload-time = "2026-02-09T12:57:53.938Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/d9/63b22a6bdbd17f1f96e9ed58604c2a6b0e72a9133e37d663bef185877cf6/coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940", size = 261500, upload-time = "2026-02-09T12:57:56.012Z" },
+ { url = "https://files.pythonhosted.org/packages/70/bf/69f86ba1ad85bc3ad240e4c0e57a2e620fbc0e1645a47b5c62f0e941ad7f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c", size = 265212, upload-time = "2026-02-09T12:57:57.5Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/f2/5f65a278a8c2148731831574c73e42f57204243d33bedaaf18fa79c5958f/coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0", size = 260398, upload-time = "2026-02-09T12:57:59.027Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/80/6e8280a350ee9fea92f14b8357448a242dcaa243cb2c72ab0ca591f66c8c/coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b", size = 262584, upload-time = "2026-02-09T12:58:01.129Z" },
+ { url = "https://files.pythonhosted.org/packages/22/63/01ff182fc95f260b539590fb12c11ad3e21332c15f9799cb5e2386f71d9f/coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9", size = 222688, upload-time = "2026-02-09T12:58:02.736Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/43/89de4ef5d3cd53b886afa114065f7e9d3707bdb3e5efae13535b46ae483d/coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd", size = 223746, upload-time = "2026-02-09T12:58:05.362Z" },
+ { url = "https://files.pythonhosted.org/packages/35/39/7cf0aa9a10d470a5309b38b289b9bb07ddeac5d61af9b664fe9775a4cb3e/coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997", size = 222003, upload-time = "2026-02-09T12:58:06.952Z" },
+ { url = "https://files.pythonhosted.org/packages/92/11/a9cf762bb83386467737d32187756a42094927150c3e107df4cb078e8590/coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601", size = 219522, upload-time = "2026-02-09T12:58:08.623Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/28/56e6d892b7b052236d67c95f1936b6a7cf7c3e2634bf27610b8cbd7f9c60/coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689", size = 219855, upload-time = "2026-02-09T12:58:10.176Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/69/233459ee9eb0c0d10fcc2fe425a029b3fa5ce0f040c966ebce851d030c70/coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c", size = 250887, upload-time = "2026-02-09T12:58:12.503Z" },
+ { url = "https://files.pythonhosted.org/packages/06/90/2cdab0974b9b5bbc1623f7876b73603aecac11b8d95b85b5b86b32de5eab/coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129", size = 253396, upload-time = "2026-02-09T12:58:14.615Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/15/ea4da0f85bf7d7b27635039e649e99deb8173fe551096ea15017f7053537/coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552", size = 254745, upload-time = "2026-02-09T12:58:16.162Z" },
+ { url = "https://files.pythonhosted.org/packages/99/11/bb356e86920c655ca4d61daee4e2bbc7258f0a37de0be32d233b561134ff/coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a", size = 257055, upload-time = "2026-02-09T12:58:17.892Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/0f/9ae1f8cb17029e09da06ca4e28c9e1d5c1c0a511c7074592e37e0836c915/coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356", size = 250911, upload-time = "2026-02-09T12:58:19.495Z" },
+ { url = "https://files.pythonhosted.org/packages/89/3a/adfb68558fa815cbc29747b553bc833d2150228f251b127f1ce97e48547c/coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71", size = 252754, upload-time = "2026-02-09T12:58:21.064Z" },
+ { url = "https://files.pythonhosted.org/packages/32/b1/540d0c27c4e748bd3cd0bd001076ee416eda993c2bae47a73b7cc9357931/coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5", size = 250720, upload-time = "2026-02-09T12:58:22.622Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/95/383609462b3ffb1fe133014a7c84fc0dd01ed55ac6140fa1093b5af7ebb1/coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98", size = 254994, upload-time = "2026-02-09T12:58:24.548Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/ba/1761138e86c81680bfc3c49579d66312865457f9fe405b033184e5793cb3/coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5", size = 250531, upload-time = "2026-02-09T12:58:26.271Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/8e/05900df797a9c11837ab59c4d6fe94094e029582aab75c3309a93e6fb4e3/coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0", size = 252189, upload-time = "2026-02-09T12:58:27.807Z" },
+ { url = "https://files.pythonhosted.org/packages/00/bd/29c9f2db9ea4ed2738b8a9508c35626eb205d51af4ab7bf56a21a2e49926/coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb", size = 222258, upload-time = "2026-02-09T12:58:29.441Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/4d/1f8e723f6829977410efeb88f73673d794075091c8c7c18848d273dc9d73/coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505", size = 223073, upload-time = "2026-02-09T12:58:31.026Z" },
+ { url = "https://files.pythonhosted.org/packages/51/5b/84100025be913b44e082ea32abcf1afbf4e872f5120b7a1cab1d331b1e13/coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2", size = 221638, upload-time = "2026-02-09T12:58:32.599Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/e4/c884a405d6ead1370433dad1e3720216b4f9fd8ef5b64bfd984a2a60a11a/coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056", size = 220246, upload-time = "2026-02-09T12:58:34.181Z" },
+ { url = "https://files.pythonhosted.org/packages/81/5c/4d7ed8b23b233b0fffbc9dfec53c232be2e695468523242ea9fd30f97ad2/coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc", size = 220514, upload-time = "2026-02-09T12:58:35.704Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/6f/3284d4203fd2f28edd73034968398cd2d4cb04ab192abc8cff007ea35679/coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9", size = 261877, upload-time = "2026-02-09T12:58:37.864Z" },
+ { url = "https://files.pythonhosted.org/packages/09/aa/b672a647bbe1556a85337dc95bfd40d146e9965ead9cc2fe81bde1e5cbce/coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf", size = 264004, upload-time = "2026-02-09T12:58:39.492Z" },
+ { url = "https://files.pythonhosted.org/packages/79/a1/aa384dbe9181f98bba87dd23dda436f0c6cf2e148aecbb4e50fc51c1a656/coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55", size = 266408, upload-time = "2026-02-09T12:58:41.852Z" },
+ { url = "https://files.pythonhosted.org/packages/53/5e/5150bf17b4019bc600799f376bb9606941e55bd5a775dc1e096b6ffea952/coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72", size = 267544, upload-time = "2026-02-09T12:58:44.093Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/ed/f1de5c675987a4a7a672250d2c5c9d73d289dbf13410f00ed7181d8017dd/coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a", size = 260980, upload-time = "2026-02-09T12:58:45.721Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/e3/fe758d01850aa172419a6743fe76ba8b92c29d181d4f676ffe2dae2ba631/coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6", size = 263871, upload-time = "2026-02-09T12:58:47.334Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/76/b829869d464115e22499541def9796b25312b8cf235d3bb00b39f1675395/coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3", size = 261472, upload-time = "2026-02-09T12:58:48.995Z" },
+ { url = "https://files.pythonhosted.org/packages/14/9e/caedb1679e73e2f6ad240173f55218488bfe043e38da577c4ec977489915/coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750", size = 265210, upload-time = "2026-02-09T12:58:51.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/10/0dd02cb009b16ede425b49ec344aba13a6ae1dc39600840ea6abcb085ac4/coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39", size = 260319, upload-time = "2026-02-09T12:58:53.081Z" },
+ { url = "https://files.pythonhosted.org/packages/92/8e/234d2c927af27c6d7a5ffad5bd2cf31634c46a477b4c7adfbfa66baf7ebb/coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0", size = 262638, upload-time = "2026-02-09T12:58:55.258Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/64/e5547c8ff6964e5965c35a480855911b61509cce544f4d442caa759a0702/coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea", size = 223040, upload-time = "2026-02-09T12:58:56.936Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/96/38086d58a181aac86d503dfa9c47eb20715a79c3e3acbdf786e92e5c09a8/coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932", size = 224148, upload-time = "2026-02-09T12:58:58.645Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/72/8d10abd3740a0beb98c305e0c3faf454366221c0f37a8bcf8f60020bb65a/coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b", size = 222172, upload-time = "2026-02-09T12:59:00.396Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" },
+]
+
+[package.optional-dependencies]
+toml = [
+ { name = "tomli", marker = "python_full_version <= '3.11'" },
+]
+
[[package]]
name = "cryptography"
version = "46.0.5"
@@ -782,6 +957,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/b2/50e9b292b5cac13e9e81272c7171301abc753a60460d21505b606e15cf21/furo-2025.12.19-py3-none-any.whl", hash = "sha256:bb0ead5309f9500130665a26bee87693c41ce4dbdff864dbfb6b0dae4673d24f", size = 339262, upload-time = "2025-12-19T17:34:38.905Z" },
]
+[[package]]
+name = "ghp-import"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" },
+]
+
[[package]]
name = "gitdb"
version = "4.0.12"
@@ -806,6 +993,14 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/09/e21df6aef1e1ffc0c816f0522ddc3f6dcded766c3261813131c78a704470/gitpython-3.1.46-py3-none-any.whl", hash = "sha256:79812ed143d9d25b6d176a10bb511de0f9c67b1fa641d82097b0ab90398a2058", size = 208620, upload-time = "2026-01-01T15:37:30.574Z" },
]
+[[package]]
+name = "griffelib"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" },
+]
+
[[package]]
name = "harp-python"
version = "0.4.1"
@@ -846,6 +1041,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" },
]
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
[[package]]
name = "jinja2"
version = "3.1.6"
@@ -1071,6 +1275,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6c/77/d7f491cbc05303ac6801651aabeb262d43f319288c1ea96c66b1d2692ff3/lxml-6.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:27220da5be049e936c3aca06f174e8827ca6445a4353a1995584311487fc4e3e", size = 3518768, upload-time = "2025-09-22T04:04:57.097Z" },
]
+[[package]]
+name = "markdown"
+version = "3.10.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" },
+]
+
[[package]]
name = "markdown-it-py"
version = "4.0.0"
@@ -1242,6 +1455,134 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
]
+[[package]]
+name = "mergedeep"
+version = "1.3.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" },
+]
+
+[[package]]
+name = "mkdocs"
+version = "1.6.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "ghp-import" },
+ { name = "jinja2" },
+ { name = "markdown" },
+ { name = "markupsafe" },
+ { name = "mergedeep" },
+ { name = "mkdocs-get-deps" },
+ { name = "packaging" },
+ { name = "pathspec" },
+ { name = "pyyaml" },
+ { name = "pyyaml-env-tag" },
+ { name = "watchdog" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" },
+]
+
+[[package]]
+name = "mkdocs-autorefs"
+version = "1.4.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown" },
+ { name = "markupsafe" },
+ { name = "mkdocs" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" },
+]
+
+[[package]]
+name = "mkdocs-get-deps"
+version = "0.2.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mergedeep" },
+ { name = "platformdirs" },
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" },
+]
+
+[[package]]
+name = "mkdocs-material"
+version = "9.7.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "babel" },
+ { name = "backrefs" },
+ { name = "colorama" },
+ { name = "jinja2" },
+ { name = "markdown" },
+ { name = "mkdocs" },
+ { name = "mkdocs-material-extensions" },
+ { name = "paginate" },
+ { name = "pygments" },
+ { name = "pymdown-extensions" },
+ { name = "requests" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/74/76/5c202fecdc45d53e83e03a85bae70c48b6c81e9f87f0bc19a9e9c723bdc0/mkdocs_material-9.7.5.tar.gz", hash = "sha256:f76bdab532bad1d9c57ca7187b37eccf64dd12e1586909307f8856db3be384ea", size = 4097749, upload-time = "2026-03-10T15:43:22.809Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/45/e1/e8080dcfa95cca267662a6f4afe29237452bdeb5a2a6555ac83646d21915/mkdocs_material-9.7.5-py3-none-any.whl", hash = "sha256:7cf9df2ff121fd098ff6e05c732b0be3699afca9642e2dfe4926c40eb5873eec", size = 9305251, upload-time = "2026-03-10T15:43:19.089Z" },
+]
+
+[[package]]
+name = "mkdocs-material-extensions"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" },
+]
+
+[[package]]
+name = "mkdocstrings"
+version = "1.0.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jinja2" },
+ { name = "markdown" },
+ { name = "markupsafe" },
+ { name = "mkdocs" },
+ { name = "mkdocs-autorefs" },
+ { name = "pymdown-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/46/62/0dfc5719514115bf1781f44b1d7f2a0923fcc01e9c5d7990e48a05c9ae5d/mkdocstrings-1.0.3.tar.gz", hash = "sha256:ab670f55040722b49bb45865b2e93b824450fb4aef638b00d7acb493a9020434", size = 100946, upload-time = "2026-02-07T14:31:40.973Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-py3-none-any.whl", hash = "sha256:0d66d18430c2201dc7fe85134277382baaa15e6b30979f3f3bdbabd6dbdb6046", size = 35523, upload-time = "2026-02-07T14:31:39.27Z" },
+]
+
+[package.optional-dependencies]
+python = [
+ { name = "mkdocstrings-python" },
+]
+
+[[package]]
+name = "mkdocstrings-python"
+version = "2.0.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "griffelib" },
+ { name = "mkdocs-autorefs" },
+ { name = "mkdocstrings" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" },
+]
+
[[package]]
name = "ms-active-directory"
version = "1.14.1"
@@ -1394,6 +1735,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
+[[package]]
+name = "paginate"
+version = "0.5.7"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" },
+]
+
[[package]]
name = "pandas"
version = "3.0.0"
@@ -1454,6 +1804,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/3f/a80ac00acbc6b35166b42850e98a4f466e2c0d9c64054161ba9620f95680/pandas-3.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1c39eab3ad38f2d7a249095f0a3d8f8c22cc0f847e98ccf5bbe732b272e2d9fa", size = 9441003, upload-time = "2026-01-21T15:52:02.281Z" },
]
+[[package]]
+name = "pathspec"
+version = "1.0.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
+]
+
[[package]]
name = "pillow"
version = "12.1.0"
@@ -1541,6 +1900,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/71/64e9b1c7f04ae0027f788a248e6297d7fcc29571371fe7d45495a78172c0/pillow-12.1.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:75af0b4c229ac519b155028fa1be632d812a519abba9b46b20e50c6caa184f19", size = 7029809, upload-time = "2026-01-02T09:13:26.541Z" },
]
+[[package]]
+name = "platformdirs"
+version = "4.9.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
[[package]]
name = "prompt-toolkit"
version = "3.0.52"
@@ -1797,6 +2174,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ac/04/6cf0687780c68e7fb0525e7210ec5477987c0481904f600c2e5d81bbb7dd/pykeepass-4.1.1.post1-py3-none-any.whl", hash = "sha256:4cfd54f376cb1f58dd8f11fbe7923282bc7dd97ffdf1bb622004a6e718bfe379", size = 55584, upload-time = "2025-03-06T00:41:57.201Z" },
]
+[[package]]
+name = "pymdown-extensions"
+version = "10.21"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "markdown" },
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ba/63/06673d1eb6d8f83c0ea1f677d770e12565fb516928b4109c9e2055656a9e/pymdown_extensions-10.21.tar.gz", hash = "sha256:39f4a020f40773f6b2ff31d2cd2546c2c04d0a6498c31d9c688d2be07e1767d5", size = 853363, upload-time = "2026-02-15T20:44:06.748Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6f/2c/5b079febdc65e1c3fb2729bf958d18b45be7113828528e8a0b5850dd819a/pymdown_extensions-10.21-py3-none-any.whl", hash = "sha256:91b879f9f864d49794c2d9534372b10150e6141096c3908a455e45ca72ad9d3f", size = 268877, upload-time = "2026-02-15T20:44:05.464Z" },
+]
+
[[package]]
name = "pyotp"
version = "2.9.0"
@@ -1815,6 +2205,36 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
]
+[[package]]
+name = "pytest"
+version = "9.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
+]
+
+[[package]]
+name = "pytest-cov"
+version = "7.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "coverage", extra = ["toml"] },
+ { name = "pluggy" },
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
+]
+
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
@@ -1900,6 +2320,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
]
+[[package]]
+name = "pyyaml-env-tag"
+version = "1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyyaml" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" },
+]
+
[[package]]
name = "questionary"
version = "2.1.1"
@@ -2345,6 +2777,60 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" },
]
+[[package]]
+name = "tomli"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
+ { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
+ { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
+ { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
+ { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
+ { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
+ { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
+ { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
+ { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
+ { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
+ { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
+ { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
+ { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
+ { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
+ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
+]
+
[[package]]
name = "typenames"
version = "2.1.0"
@@ -2408,6 +2894,33 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
]
+[[package]]
+name = "watchdog"
+version = "6.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" },
+ { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" },
+ { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" },
+ { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" },
+ { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" },
+ { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" },
+ { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" },
+ { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" },
+ { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" },
+ { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" },
+]
+
[[package]]
name = "wcwidth"
version = "0.6.0"
diff --git a/workspaces/aind-behavior-dynamic-foraging-curricula/README.md b/workspaces/aind-behavior-dynamic-foraging-curricula/README.md
new file mode 100644
index 0000000..b5aab7f
--- /dev/null
+++ b/workspaces/aind-behavior-dynamic-foraging-curricula/README.md
@@ -0,0 +1,228 @@
+# Aind.Behavior.DynamicForaging.Curricula
+
+
+[](LICENSE)
+[](https://github.com/astral-sh/ruff)
+[](https://github.com/astral-sh/uv)
+
+
+A repository of curricula for [Dynamic foraging task](https://github.com/AllenNeuralDynamics/Aind.Behavior.DynamicForaging).
+
+## CLI Reference
+
+Curricula are modules of the main package: `aind_behavior_dynamic_foraging_curricula.`.
+
+All curricula are available via the `curriculum` CLI entry point. The following commands are available:
+
+### Getting Help
+
+Display all available commands:
+
+```bash
+uv run curriculum -h
+```
+
+Get help for a specific command:
+
+```bash
+uv run curriculum -h
+```
+
+### `list` - List Available Curricula
+
+Lists all curricula available in this repository.
+
+**Usage:**
+
+```bash
+uv run curriculum list
+```
+
+**Example output:**
+```
+Available curricula:
+ - depletion
+ - depletion_stops_offset
+ - depletion_stops_rate
+ - operant_conditioning
+ - single_site_matching
+ - template
+```
+
+### `init` - Initialize a Curriculum
+
+Creates an initial trainer state for enrolling a subject in a curriculum. This generates the starting point for curriculum execution.
+
+**Required Arguments:**
+- `--curriculum `: The curriculum to enroll in (required)
+
+**Optional Arguments:**
+- `--output `: Path to save the enrollment trainer state as a JSON file
+- `--stage `: If provided, enroll at a specific stage instead of the first stage
+
+**Examples:**
+
+Initialize the depletion curriculum (starts at first stage):
+
+```bash
+uv run curriculum init --curriculum depletion --output initial_state.json
+```
+
+Initialize at a specific stage:
+
+```bash
+uv run curriculum init --curriculum depletion --stage stage_one_odor_no_depletion --output initial_state.json
+```
+
+Print to stdout without saving:
+
+```bash
+uv run curriculum init --curriculum operant_conditioning
+```
+
+### `run` - Run a Curriculum
+
+Evaluates a curriculum based on session data and the current trainer state, producing a suggestion for the next stage.
+
+**Required Arguments:**
+- `--data-directory `: Path to the session data directory for calculating metrics
+- `--input-trainer-state `: Path to the current trainer state JSON file
+
+**Optional Arguments:**
+- `--curriculum `: Forces the use of a specific curriculum, bypassing automatic detection
+- `--output-suggestion `: Directory path to save the suggestion as `suggestion.json`
+- `--mute-suggestion`: Disables printing the suggestion to stdout (useful when only saving to file)
+
+**Examples:**
+
+Run curriculum with automatic detection:
+
+```bash
+uv run curriculum run \
+ --data-directory /path/to/session/data \
+ --input-trainer-state current_state.json \
+ --output-suggestion /path/to/output
+```
+
+Force a specific curriculum:
+
+```bash
+uv run curriculum run \
+ --data-directory /path/to/session/data \
+ --input-trainer-state current_state.json \
+ --curriculum depletion \
+ --output-suggestion /path/to/output
+```
+
+Run without saving (print to stdout only):
+
+```bash
+uv run curriculum run \
+ --data-directory /path/to/session/data \
+ --input-trainer-state current_state.json
+```
+
+Run and save without printing:
+
+```bash
+uv run curriculum run \
+ --data-directory /path/to/session/data \
+ --input-trainer-state current_state.json \
+ --output-suggestion /path/to/output \
+ --mute-suggestion
+```
+
+Quick demo with template curriculum:
+
+```bash
+uv run curriculum run \
+ --data-directory "demo" \
+ --input-trainer-state "foo.json" \
+ --curriculum "template"
+```
+
+### `version` - Show Package Version
+
+Displays the version of this package.
+
+**Usage:**
+
+```bash
+uv run curriculum version
+```
+
+**Example output:**
+```
+0.2.0
+```
+
+### `dsl-version` - Show DSL Version
+
+Displays the version of the underlying `aind-behavior-curriculum` DSL library.
+
+**Usage:**
+
+```bash
+uv run curriculum dsl-version
+```
+
+**Example output:**
+```
+0.0.37
+```
+
+## Typical Workflow
+
+1. **List available curricula:**
+ ```bash
+ uv run curriculum list
+ ```
+
+2. **Initialize a subject in a curriculum:**
+ ```bash
+ uv run curriculum init --curriculum depletion --output trainer_state.json
+ ```
+
+3. **After a training session, evaluate progress:**
+ ```bash
+ uv run curriculum run \
+ --data-directory /path/to/session/data \
+ --input-trainer-state trainer_state.json \
+ --output-suggestion /path/to/output
+ ```
+
+4. **Use the suggestion output for the next session:**
+ The `suggestion.json` file contains the updated trainer state and can be used as `--input-trainer-state` for the next session.
+
+
+## Style guide
+
+To keep things clear, I suggest the following naming convention:
+
+* **Policies** should start with `p_` (e.g., `p_identity_policy`)
+* **Policy transitions** should start with `pt_`
+* **Stages** should start with `s_` (e.g., `s_stage1`)
+* **Stage transitions** should start with `st_` and should be named after the stages they transition between (e.g., `st_s_stage1_s_stage2`)
+
+Define the following modules:
+
+* **metrics**: Defines (or imports) metrics classes and how to calculate them from data
+* **stages**: Defines the different stages of the Dynamic foraging task. This includes task settings and, optionally, policies
+* **curriculum**: Defines the transitions between the stages and generate entry point to the application
+
+## Contributors
+
+Contributions to this repository are welcome! However, please ensure that your code adheres to the recommended DevOps practices below:
+
+### Linting
+
+We use [ruff](https://docs.astral.sh/ruff/) as our primary linting tool.
+
+### Testing
+
+Attempt to add tests when new features are added.
+To run the currently available tests, run `uv run pytest` from the root of the repository.
+
+### Lock files
+
+We use [uv](https://docs.astral.sh/uv/) to manage our lock files and therefore encourage everyone to use uv as a package manager as well.
\ No newline at end of file
diff --git a/workspaces/aind-behavior-dynamic-foraging-curricula/examples/couple_baiting_curriculum.py b/workspaces/aind-behavior-dynamic-foraging-curricula/examples/couple_baiting_curriculum.py
new file mode 100644
index 0000000..513ebca
--- /dev/null
+++ b/workspaces/aind-behavior-dynamic-foraging-curricula/examples/couple_baiting_curriculum.py
@@ -0,0 +1,25 @@
+from aind_behavior_dynamic_foraging_curricula.coupled_baiting import TRAINER
+from aind_behavior_dynamic_foraging_curricula.metrics import DynamicForagingMetrics
+
+
+def main():
+ trainer_state = TRAINER.create_enrollment()
+
+ # starts at stage_1_warmup
+ current_stage = trainer_state.stage
+ print(f"Current stage: {current_stage.name}") # stage_1_warmup
+
+ metrics = DynamicForagingMetrics(
+ session_total=1,
+ session_at_current_stage=1,
+ finished_trials=[250],
+ foraging_efficiency=[0.65],
+ )
+
+ # evaluate
+ new_trainer_state = TRAINER.evaluate(trainer_state, metrics)
+ print(f"Next stage: {new_trainer_state.stage.name}") # stage_2, since finished_trials >= 200 and efficiency >= 0.6
+
+
+if __name__ == "__main__":
+ main()
diff --git a/workspaces/aind-behavior-dynamic-foraging-curricula/pyproject.toml b/workspaces/aind-behavior-dynamic-foraging-curricula/pyproject.toml
new file mode 100644
index 0000000..b924526
--- /dev/null
+++ b/workspaces/aind-behavior-dynamic-foraging-curricula/pyproject.toml
@@ -0,0 +1,75 @@
+[build-system]
+requires = ["uv_build>=0.8.22"]
+build-backend = "uv_build"
+
+[project]
+name = "aind-behavior-dynamic-foraging-curricula"
+description = "A library of curricula for the Dynamic Foraging task."
+authors = [
+ {name = "Bruno Cruz", email = "bruno.cruz@alleninstitute.org"},
+ {name = "Micah Woodard", email = "micah.woodard@alleninstitute.org"}
+ ]
+license = "MIT"
+version = "0.2.1"
+requires-python = ">=3.11"
+classifiers = [
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
+ "Operating System :: Microsoft :: Windows",
+]
+readme = {file = "README.md", content-type = "text/markdown"}
+
+dependencies = [
+ "aind-behavior-curriculum >= 0.0.38",
+ "numpy>=2.4.2",
+ "pydantic-settings",
+ "aind-behavior-dynamic-foraging"
+]
+
+[tool.uv.sources]
+aind-behavior-dynamic-foraging = { workspace = true }
+
+[dependency-groups]
+
+dev = [
+ 'ruff',
+ 'pytest',
+ 'pytest-cov',
+ 'codespell',
+]
+
+docs = [
+ 'mkdocs',
+ 'mkdocs-material',
+ 'mkdocstrings[python]',
+ 'pymdown-extensions',
+ 'ruff',
+]
+
+[tool.uv]
+default-groups = ['dev']
+
+[tool.ruff]
+line-length = 120
+target-version = 'py311'
+
+[tool.ruff.lint]
+extend-select = ['Q', 'RUF100', 'C90', 'I']
+extend-ignore = []
+mccabe = { max-complexity = 14 }
+pydocstyle = { convention = 'google' }
+
+[tool.codespell]
+skip = '.git,*.pdf,*.svg,uv.lock'
+ignore-words-list = 'nd'
+
+[tool.pytest.ini_options]
+addopts = "--strict-markers --tb=short --cov=src --cov-report=term-missing --cov-fail-under=70"
+testpaths = ["tests"]
+python_files = ["test_*.py"]
+python_classes = ["Test*"]
+python_functions = ["test_*"]
+
+[project.scripts]
+curriculum = "aind_behavior_dynamic_foraging_curricula.cli:main"
diff --git a/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/__init__.py b/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/__init__.py
new file mode 100644
index 0000000..98cffdf
--- /dev/null
+++ b/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/__init__.py
@@ -0,0 +1,30 @@
+import re
+from importlib.metadata import PackageNotFoundError, version
+
+
+def pep440_to_semver(ver: str) -> str:
+ """
+ Convert a PEP 440 version to a SemVer-compatible string.
+
+ Examples:
+ 1.2.3rc2 -> 1.2.3-rc2
+ 1.2.3a1 -> 1.2.3-a1
+ 1.2.3b1 -> 1.2.3-b1
+ 1.2.3.dev4 -> 1.2.3-dev4
+ 1.2.3.post1 -> 1.2.3+post1
+ """
+ # pre-release: a, b, rc -> -aN, -bN, -rcN
+ ver = re.sub(r"(?<=\d)(a|b|rc)(\d+)", r"-\1\2", ver)
+ # dev release: .devN -> -devN
+ ver = re.sub(r"\.dev(\d+)", r"-dev\1", ver)
+ # post release: .postN -> +postN
+ ver = re.sub(r"\.post(\d+)", r"+post\1", ver)
+ return ver
+
+
+try:
+ __version__ = version(__name__)
+except PackageNotFoundError:
+ __version__ = "0.0.0"
+
+__semver__ = pep440_to_semver(__version__)
diff --git a/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/cli.py b/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/cli.py
new file mode 100644
index 0000000..f7be0ef
--- /dev/null
+++ b/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/cli.py
@@ -0,0 +1,168 @@
+import importlib
+import logging
+import os
+import typing as t
+from pathlib import Path
+
+import aind_behavior_curriculum
+from pydantic import BaseModel, Field, RootModel, SerializeAsAny
+from pydantic_settings import BaseSettings, CliApp, CliImplicitFlag, CliSubCommand
+
+from . import __version__
+from .utils import model_from_json_file
+
+TModel = t.TypeVar("TModel", bound=BaseModel)
+TTrainerState = t.TypeVar("TTrainerState", bound=aind_behavior_curriculum.TrainerState)
+TMetrics = t.TypeVar("TMetrics", bound=aind_behavior_curriculum.Metrics)
+TCurriculum = t.TypeVar("TCurriculum", bound=aind_behavior_curriculum.Curriculum)
+
+curricula_logger = logging.Logger(__name__)
+
+
+class Version(RootModel):
+ root: t.Any
+
+ def cli_cmd(self) -> None:
+ print(__version__)
+
+
+class DslVersion(RootModel):
+ root: t.Any
+
+ def cli_cmd(self) -> None:
+ print(aind_behavior_curriculum.__version__)
+
+
+class ListKnownCurricula(RootModel):
+ root: t.Any
+
+ def cli_cmd(self) -> None:
+ print("Available curricula:")
+ for curriculum in _KNOWN_CURRICULA:
+ print(f" - {curriculum}")
+
+
+class CurriculumCliArgs(BaseSettings):
+ data_directory: os.PathLike = Field(description="Path to the session data directory.")
+ input_trainer_state: os.PathLike = Field(description="Path to a deserialized trainer state.")
+ mute_suggestion: CliImplicitFlag[bool] = Field(default=False, description="Disables the suggestion output")
+ output_suggestion: t.Optional[os.PathLike] = Field(
+ default=None,
+ description="A path to save the suggestion. If not provided, the suggestion will not be serialized to a file.",
+ )
+ curriculum: t.Optional[str] = Field(
+ default=None, description="Forces the use of a specific curriculum, bypassing any automatic detection."
+ )
+
+ def cli_cmd(self) -> None:
+ try:
+ if self.curriculum:
+ curriculum_name = self.curriculum
+ else:
+ annonymous_trainer_state = model_from_json_file(
+ self.input_trainer_state,
+ aind_behavior_curriculum.TrainerState[aind_behavior_curriculum.Curriculum[t.Any]],
+ )
+ if (curriculum := annonymous_trainer_state.curriculum) is None:
+ curricula_logger.error("Trainer state does not have a curriculum.")
+ raise ValueError("Trainer state does not have a curriculum.")
+ curriculum_name = curriculum.pkg_location
+
+ curriculum_name = curriculum_name.replace(str(__package__) + ".", "")
+ if curriculum_name not in _KNOWN_CURRICULA:
+ curricula_logger.error(f"Unknown curriculum: {curriculum_name}. Available: {list(_KNOWN_CURRICULA)}")
+ raise ValueError(f"Unknown curriculum: {curriculum_name}. Available: {list(_KNOWN_CURRICULA)}")
+
+ else:
+ module = importlib.import_module(f"{__package__}.{curriculum_name}")
+ runner: t.Callable[[CurriculumCliArgs], CurriculumSuggestion] = getattr(module, "run_curriculum")
+
+ suggestion = runner(self)
+ suggestion.dsl_version = aind_behavior_curriculum.__version__
+
+ if not self.mute_suggestion:
+ print(suggestion.model_dump_json())
+
+ if self.output_suggestion is not None:
+ with open(Path(self.output_suggestion) / "suggestion.json", "w", encoding="utf-8") as file:
+ file.write(suggestion.model_dump_json(indent=2))
+
+ except Exception as e:
+ curricula_logger.error(f"Error occurred while running curriculum: {e}")
+ raise e
+
+
+class CurriculumInitCliArgs(BaseSettings):
+ curriculum: str = Field(description="The curriculum to enroll the model in.")
+ output: t.Optional[os.PathLike] = Field(
+ default=None,
+ description="Path to save the enrollment curriculum. If not provided, the curriculum will not be serialized to a file.",
+ )
+ stage: t.Optional[str] = Field(
+ default=None,
+ description="If provided, the enrollment will be for a specific stage in the curriculum.",
+ )
+
+ def cli_cmd(self) -> None:
+ if self.curriculum not in _KNOWN_CURRICULA:
+ curricula_logger.error(f"Unknown curriculum: {self.curriculum}. Available: {list(_KNOWN_CURRICULA)}")
+ raise ValueError(f"Unknown curriculum: {self.curriculum}. Available: {list(_KNOWN_CURRICULA)}")
+
+ module = importlib.import_module(f"{__package__}.{self.curriculum}")
+ trainer: aind_behavior_curriculum.Trainer = getattr(module, "TRAINER")
+ if self.stage is None:
+ init_state = trainer.create_enrollment()
+ else:
+ try:
+ stages = trainer.curriculum.see_stages()
+ stage = [s for s in stages if s.name == self.stage][0]
+ except IndexError:
+ curricula_logger.error(f"Unknown stage: {self.stage}")
+ curricula_logger.error(self._print_available_stages(trainer.curriculum))
+ raise ValueError(f"Unknown stage: {self.stage}. Available: {[s.name for s in stages]}")
+ else:
+ init_state = trainer.create_trainer_state(
+ stage=stage, is_on_curriculum=True, active_policies=stage.start_policies
+ )
+
+ if self.output is not None:
+ with open(Path(self.output), "w", encoding="utf-8") as file:
+ file.write(init_state.model_dump_json(indent=2))
+
+ print(init_state.model_dump_json())
+
+ def _print_available_stages(self, curriculum: aind_behavior_curriculum.Curriculum) -> None:
+ print("Available stages:")
+ for stage in curriculum.see_stages():
+ print(f" - {stage.name}")
+
+
+class CurriculumAppCliArgs(BaseSettings, cli_prog_name="curriculum", cli_kebab_case=True):
+ run: CliSubCommand[CurriculumCliArgs]
+ init: CliSubCommand[CurriculumInitCliArgs]
+ version: CliSubCommand[Version]
+ dsl_version: CliSubCommand[DslVersion]
+ list: CliSubCommand[ListKnownCurricula]
+
+ def cli_cmd(self) -> None:
+ CliApp.run_subcommand(self)
+
+
+class CurriculumSuggestion(BaseModel, t.Generic[TTrainerState, TMetrics]):
+ trainer_state: SerializeAsAny[TTrainerState] = Field(description="The TrainerState suggestion.")
+ metrics: SerializeAsAny[TMetrics] = Field(description="The calculated metrics.")
+ version: str = Field(default=__version__, description="The version of the curriculum.")
+ dsl_version: str = Field(
+ default=aind_behavior_curriculum.__version__, description="The version of the curriculum library."
+ )
+
+
+_KNOWN_CURRICULA = [p.stem for p in Path(__file__).parent.iterdir() if p.is_dir() and not p.name.startswith("_")]
+
+
+def main():
+ CliApp.run(CurriculumAppCliArgs)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/__init__.py b/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/__init__.py
new file mode 100644
index 0000000..7e54700
--- /dev/null
+++ b/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/__init__.py
@@ -0,0 +1,3 @@
+from .curriculum import CURRICULUM, CURRICULUM_NAME, PKG_LOCATION, TRAINER, run_curriculum
+
+__all__ = ["CURRICULUM_NAME", "CURRICULUM", "TRAINER", "PKG_LOCATION", "run_curriculum"]
diff --git a/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/curriculum.py b/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/curriculum.py
new file mode 100644
index 0000000..95563ee
--- /dev/null
+++ b/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/curriculum.py
@@ -0,0 +1,124 @@
+from typing import Any, Type, TypeVar
+
+import numpy as np
+import pydantic
+from aind_behavior_curriculum import Curriculum, Metrics, StageTransition, Trainer, TrainerState, create_curriculum
+from aind_behavior_dynamic_foraging.task_logic import (
+ AindDynamicForagingTaskLogic,
+)
+
+from .. import __semver__
+from ..cli import CurriculumCliArgs, CurriculumSuggestion
+from ..metrics import DynamicForagingMetrics
+from ..utils import metrics_from_dataset_path, trainer_state_from_file
+from .stages import s_final, s_graduated, s_stage_1, s_stage_1_warmup, s_stage_2, s_stage_3
+
+CURRICULUM_NAME = "CoupledBaiting"
+PKG_LOCATION = ".".join(__name__.split(".")[:-1])
+
+TModel = TypeVar("TModel", bound=pydantic.BaseModel)
+
+
+# --- STAGE TRANSITIONS ---
+
+
+# warmup
+@StageTransition
+def st_stage_1_warmup_to_stage_1(metrics: DynamicForagingMetrics) -> bool:
+ return metrics.sessions_at_current_stage >= 1
+
+
+@StageTransition
+def st_stage_1_warmup_to_stage_2(metrics: DynamicForagingMetrics) -> bool:
+ return metrics.finished_trials_per_session[-1] >= 200 and metrics.foraging_efficiency_per_session[-1] >= 0.6
+
+
+# stage 1
+@StageTransition
+def st_stage_1_to_stage_2(metrics: DynamicForagingMetrics) -> bool:
+ return metrics.foraging_efficiency_per_session[-1] >= 0.6 and metrics.finished_trials_per_session[-1] >= 200
+
+
+# stage 2
+@StageTransition
+def st_stage_2_to_stage_3(metrics: DynamicForagingMetrics) -> bool:
+ return metrics.foraging_efficiency_per_session[-1] >= 0.65 and metrics.finished_trials_per_session[-1] >= 300
+
+
+@StageTransition
+def st_stage_2_to_stage_1(metrics: DynamicForagingMetrics) -> bool:
+ return metrics.foraging_efficiency_per_session[-1] < 0.55 or metrics.finished_trials_per_session[-1] < 200
+
+
+# stage 3
+@StageTransition
+def st_stage_3_to_final(metrics: DynamicForagingMetrics) -> bool:
+ return metrics.foraging_efficiency_per_session[-1] >= 0.7 and metrics.finished_trials_per_session[-1] >= 400
+
+
+@StageTransition
+def st_stage_3_to_stage_2(metrics: DynamicForagingMetrics) -> bool:
+ return metrics.foraging_efficiency_per_session[-1] < 0.65 or metrics.finished_trials_per_session[-1] < 300
+
+
+# stage final
+@StageTransition
+def st_final_to_graduated(metrics: DynamicForagingMetrics) -> bool:
+ return (
+ metrics.total_sessions >= 10
+ and metrics.sessions_at_current_stage >= 5
+ and np.mean(metrics.finished_trials_per_session[-5:]) >= 450
+ and np.mean(metrics.foraging_efficiency_per_session[-5:]) >= 0.7
+ )
+
+
+@StageTransition
+def st_final_to_stage_3(metrics: DynamicForagingMetrics) -> bool:
+ return (
+ np.mean(metrics.foraging_efficiency_per_session[-5:]) < 0.60
+ or np.mean(metrics.finished_trials_per_session[-5:]) < 300
+ )
+
+
+# --- CURRICULUM ---
+
+curriculum_class: Type[Curriculum[AindDynamicForagingTaskLogic]] = create_curriculum(
+ CURRICULUM_NAME, __semver__, (AindDynamicForagingTaskLogic,), pkg_location=PKG_LOCATION
+)
+CURRICULUM = curriculum_class()
+
+# add stages
+CURRICULUM.add_stage(s_stage_1_warmup)
+CURRICULUM.add_stage(s_stage_1)
+CURRICULUM.add_stage(s_stage_2)
+CURRICULUM.add_stage(s_stage_3)
+CURRICULUM.add_stage(s_final)
+CURRICULUM.add_stage(s_graduated)
+
+# add stage transitions
+# warmup
+CURRICULUM.add_stage_transition(
+ s_stage_1_warmup, s_stage_2, st_stage_1_warmup_to_stage_2
+) # add 2 first to take priority
+
+CURRICULUM.add_stage_transition(s_stage_1_warmup, s_stage_1, st_stage_1_warmup_to_stage_1)
+# stage 1
+CURRICULUM.add_stage_transition(s_stage_1, s_stage_2, st_stage_1_to_stage_2)
+# stage 2
+CURRICULUM.add_stage_transition(s_stage_2, s_stage_3, st_stage_2_to_stage_3)
+CURRICULUM.add_stage_transition(s_stage_2, s_stage_1, st_stage_2_to_stage_1)
+# stage 3
+CURRICULUM.add_stage_transition(s_stage_3, s_final, st_stage_3_to_final)
+CURRICULUM.add_stage_transition(s_stage_3, s_stage_2, st_stage_3_to_stage_2)
+# final
+CURRICULUM.add_stage_transition(s_final, s_graduated, st_final_to_graduated)
+CURRICULUM.add_stage_transition(s_final, s_stage_3, st_final_to_stage_3)
+
+TRAINER = Trainer(CURRICULUM)
+
+
+def run_curriculum(args: CurriculumCliArgs) -> CurriculumSuggestion[TrainerState[Any], Any]:
+ trainer_state = trainer_state_from_file(args.input_trainer_state, TRAINER)
+ metrics: Metrics = metrics_from_dataset_path(args.data_directory, trainer_state)
+ trainer_state = TRAINER.evaluate(trainer_state, metrics)
+ return CurriculumSuggestion(trainer_state=trainer_state, metrics=metrics, version=__semver__)
diff --git a/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/stages.py b/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/stages.py
new file mode 100644
index 0000000..401b045
--- /dev/null
+++ b/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/coupled_baiting/stages.py
@@ -0,0 +1,340 @@
+from aind_behavior_curriculum import MetricsProvider, Stage
+from aind_behavior_dynamic_foraging.task_logic import (
+ AindDynamicForagingTaskLogic,
+ AindDynamicForagingTaskParameters,
+ RewardSize,
+)
+from aind_behavior_dynamic_foraging.task_logic.trial_generators import (
+ CoupledTrialGeneratorSpec,
+ TrialGeneratorCompositeSpec,
+ WarmupTrialGeneratorSpec,
+)
+from aind_behavior_dynamic_foraging.task_logic.trial_generators.block_based_trial_generator import (
+ RewardProbabilityParameters,
+)
+from aind_behavior_dynamic_foraging.task_logic.trial_generators.coupled_trial_generator import (
+ BehaviorStabilityParameters,
+ CoupledTrialGenerationEndConditions,
+)
+from aind_behavior_dynamic_foraging.task_logic.trial_generators.warmup_trial_generator import (
+ WarmupTrialGenerationEndConditions,
+)
+from aind_behavior_services.task.distributions import (
+ ExponentialDistribution,
+ ExponentialDistributionParameters,
+ TruncationParameters,
+ UniformDistribution,
+ UniformDistributionParameters,
+)
+
+from ..metrics import metrics_from_dataset
+
+# --- STAGES ---
+
+s_stage_1_warmup = Stage(
+ name="stage_1_warmup",
+ task=AindDynamicForagingTaskLogic(
+ task_parameters=AindDynamicForagingTaskParameters(
+ reward_size=RewardSize(right_value_volume=4.0, left_value_volume=4.0),
+ lick_spout_retraction=False,
+ trial_generator=TrialGeneratorCompositeSpec(
+ generators=[
+ WarmupTrialGeneratorSpec(
+ trial_generation_end_parameters=WarmupTrialGenerationEndConditions(
+ min_trial=50,
+ max_choice_bias=0.1,
+ min_response_rate=0.8,
+ evaluation_window=20,
+ ),
+ reward_probability_parameters=RewardProbabilityParameters(
+ base_reward_sum=1,
+ family=3,
+ pairs_n=1,
+ ),
+ block_len=ExponentialDistribution(
+ distribution_parameters=ExponentialDistributionParameters(rate=1),
+ truncation_parameters=TruncationParameters(min=1, max=1),
+ ),
+ inter_trial_interval_duration=ExponentialDistribution(
+ distribution_parameters=ExponentialDistributionParameters(rate=1 / 3),
+ truncation_parameters=TruncationParameters(min=1, max=7),
+ ),
+ quiescent_duration=UniformDistribution(
+ distribution_parameters=UniformDistributionParameters(min=0.1, max=0.1),
+ ),
+ min_block_reward=1,
+ is_baiting=True,
+ response_duration=5.0,
+ reward_consumption_duration=1.0,
+ kernel_size=2,
+ extend_block_on_no_response=True,
+ ),
+ CoupledTrialGeneratorSpec(
+ trial_generation_end_parameters=CoupledTrialGenerationEndConditions(
+ max_trial=1000,
+ max_time=75,
+ min_time=30,
+ ignore_win=20000,
+ ignore_ratio_threshold=1,
+ ),
+ behavior_stability_parameters=BehaviorStabilityParameters(
+ behavior_evaluation_mode="end",
+ behavior_stability_fraction=0.5,
+ min_consecutive_stable_trials=5,
+ ),
+ reward_probability_parameters=RewardProbabilityParameters(
+ base_reward_sum=0.8,
+ family=3,
+ pairs_n=1,
+ ),
+ block_len=ExponentialDistribution(
+ distribution_parameters=ExponentialDistributionParameters(rate=1 / 5),
+ truncation_parameters=TruncationParameters(min=10, max=20),
+ ),
+ inter_trial_interval_duration=ExponentialDistribution(
+ distribution_parameters=ExponentialDistributionParameters(rate=1 / 3),
+ truncation_parameters=TruncationParameters(min=1, max=7),
+ ),
+ quiescent_duration=UniformDistribution(
+ distribution_parameters=UniformDistributionParameters(min=0.1, max=0.1),
+ ),
+ min_block_reward=0,
+ is_baiting=True,
+ extend_block_on_no_response=True,
+ response_duration=5.0,
+ reward_consumption_duration=1.0,
+ kernel_size=2,
+ ),
+ ]
+ ),
+ )
+ ),
+ metrics_provider=MetricsProvider(metrics_from_dataset),
+)
+
+s_stage_1 = Stage(
+ name="stage_1",
+ task=AindDynamicForagingTaskLogic(
+ task_parameters=AindDynamicForagingTaskParameters(
+ reward_size=RewardSize(right_value_volume=2.0, left_value_volume=2.0),
+ lick_spout_retraction=False,
+ trial_generator=CoupledTrialGeneratorSpec(
+ trial_generation_end_parameters=CoupledTrialGenerationEndConditions(
+ max_trial=1000,
+ max_time=75,
+ min_time=30,
+ ignore_win=20000,
+ ignore_ratio_threshold=1,
+ ),
+ behavior_stability_parameters=BehaviorStabilityParameters(
+ behavior_evaluation_mode="end",
+ behavior_stability_fraction=0.5,
+ min_consecutive_stable_trials=5,
+ ),
+ reward_probability_parameters=RewardProbabilityParameters(
+ base_reward_sum=0.8,
+ family=3,
+ pairs_n=1,
+ ),
+ block_len=ExponentialDistribution(
+ distribution_parameters=ExponentialDistributionParameters(rate=1 / 5),
+ truncation_parameters=TruncationParameters(min=10, max=20),
+ ),
+ inter_trial_interval_duration=ExponentialDistribution(
+ distribution_parameters=ExponentialDistributionParameters(rate=1 / 3),
+ truncation_parameters=TruncationParameters(min=1, max=7),
+ ),
+ quiescent_duration=UniformDistribution(
+ distribution_parameters=UniformDistributionParameters(min=0.1, max=0.1),
+ ),
+ min_block_reward=0,
+ is_baiting=False,
+ extend_block_on_no_response=True,
+ response_duration=5.0,
+ reward_consumption_duration=1.0,
+ kernel_size=2,
+ ),
+ )
+ ),
+ metrics_provider=MetricsProvider(metrics_from_dataset),
+)
+
+s_stage_2 = Stage(
+ name="stage_2",
+ task=AindDynamicForagingTaskLogic(
+ task_parameters=AindDynamicForagingTaskParameters(
+ reward_size=RewardSize(right_value_volume=2.0, left_value_volume=2.0),
+ lick_spout_retraction=False,
+ trial_generator=CoupledTrialGeneratorSpec(
+ trial_generation_end_parameters=CoupledTrialGenerationEndConditions(
+ max_trial=1000,
+ max_time=75,
+ min_time=30,
+ ignore_win=30,
+ ignore_ratio_threshold=0.83,
+ ),
+ behavior_stability_parameters=BehaviorStabilityParameters(
+ behavior_evaluation_mode="end",
+ behavior_stability_fraction=0.6,
+ min_consecutive_stable_trials=5,
+ ),
+ reward_probability_parameters=RewardProbabilityParameters(
+ base_reward_sum=0.6,
+ family=1,
+ pairs_n=1,
+ ),
+ block_len=ExponentialDistribution(
+ distribution_parameters=ExponentialDistributionParameters(rate=1 / 10),
+ truncation_parameters=TruncationParameters(min=10, max=40),
+ ),
+ inter_trial_interval_duration=ExponentialDistribution(
+ distribution_parameters=ExponentialDistributionParameters(rate=1 / 5),
+ truncation_parameters=TruncationParameters(min=1, max=10),
+ ),
+ quiescent_duration=UniformDistribution(
+ distribution_parameters=UniformDistributionParameters(min=0.3, max=0.3),
+ ),
+ min_block_reward=0,
+ is_baiting=True,
+ extend_block_on_no_response=True,
+ response_duration=3.0,
+ reward_consumption_duration=1.0,
+ kernel_size=2,
+ ),
+ )
+ ),
+ metrics_provider=MetricsProvider(metrics_from_dataset),
+)
+
+s_stage_3 = Stage(
+ name="stage_3",
+ task=AindDynamicForagingTaskLogic(
+ task_parameters=AindDynamicForagingTaskParameters(
+ reward_size=RewardSize(right_value_volume=2.0, left_value_volume=2.0),
+ lick_spout_retraction=False,
+ trial_generator=CoupledTrialGeneratorSpec(
+ trial_generation_end_parameters=CoupledTrialGenerationEndConditions(
+ max_trial=1000,
+ max_time=75,
+ min_time=30,
+ ignore_win=30,
+ ignore_ratio_threshold=0.83,
+ ),
+ behavior_stability_parameters=BehaviorStabilityParameters(
+ behavior_evaluation_mode="end",
+ behavior_stability_fraction=0.6,
+ min_consecutive_stable_trials=5,
+ ),
+ reward_probability_parameters=RewardProbabilityParameters(
+ base_reward_sum=0.45,
+ family=1,
+ pairs_n=1,
+ ),
+ block_len=ExponentialDistribution(
+ distribution_parameters=ExponentialDistributionParameters(rate=1 / 20),
+ truncation_parameters=TruncationParameters(min=20, max=60),
+ ),
+ inter_trial_interval_duration=ExponentialDistribution(
+ distribution_parameters=ExponentialDistributionParameters(rate=1 / 3),
+ truncation_parameters=TruncationParameters(min=1, max=15),
+ ),
+ quiescent_duration=UniformDistribution(
+ distribution_parameters=UniformDistributionParameters(min=0.5, max=0.5),
+ ),
+ min_block_reward=0,
+ is_baiting=True,
+ extend_block_on_no_response=True,
+ response_duration=2.0,
+ reward_consumption_duration=1.0,
+ kernel_size=2,
+ ),
+ )
+ ),
+ metrics_provider=MetricsProvider(metrics_from_dataset),
+)
+
+s_final = Stage(
+ name="final",
+ task=AindDynamicForagingTaskLogic(
+ task_parameters=AindDynamicForagingTaskParameters(
+ reward_size=RewardSize(right_value_volume=2.0, left_value_volume=2.0),
+ lick_spout_retraction=False,
+ trial_generator=CoupledTrialGeneratorSpec(
+ trial_generation_end_parameters=CoupledTrialGenerationEndConditions(
+ max_trial=1000,
+ max_time=75,
+ min_time=30,
+ ignore_win=30,
+ ignore_ratio_threshold=0.83,
+ ),
+ behavior_stability_parameters=None,
+ reward_probability_parameters=RewardProbabilityParameters(
+ base_reward_sum=0.45,
+ family=1,
+ pairs_n=4,
+ ),
+ block_len=ExponentialDistribution(
+ distribution_parameters=ExponentialDistributionParameters(rate=1 / 20),
+ truncation_parameters=TruncationParameters(min=20, max=60),
+ ),
+ inter_trial_interval_duration=ExponentialDistribution(
+ distribution_parameters=ExponentialDistributionParameters(rate=1 / 3),
+ truncation_parameters=TruncationParameters(min=1, max=30),
+ ),
+ quiescent_duration=UniformDistribution(
+ distribution_parameters=UniformDistributionParameters(min=1.0, max=1.0),
+ ),
+ min_block_reward=0,
+ is_baiting=True,
+ extend_block_on_no_response=True,
+ response_duration=1.0,
+ reward_consumption_duration=3.0,
+ kernel_size=2,
+ ),
+ )
+ ),
+ metrics_provider=MetricsProvider(metrics_from_dataset),
+)
+
+s_graduated = Stage(
+ name="graduated",
+ task=AindDynamicForagingTaskLogic(
+ task_parameters=AindDynamicForagingTaskParameters(
+ reward_size=RewardSize(right_value_volume=2.0, left_value_volume=2.0),
+ lick_spout_retraction=False,
+ trial_generator=CoupledTrialGeneratorSpec(
+ trial_generation_end_parameters=CoupledTrialGenerationEndConditions(
+ max_trial=1000,
+ max_time=75,
+ min_time=30,
+ ignore_win=30,
+ ignore_ratio_threshold=0.83,
+ ),
+ behavior_stability_parameters=None,
+ reward_probability_parameters=RewardProbabilityParameters(
+ base_reward_sum=0.45,
+ family=1,
+ pairs_n=4,
+ ),
+ block_len=ExponentialDistribution(
+ distribution_parameters=ExponentialDistributionParameters(rate=1 / 20),
+ truncation_parameters=TruncationParameters(min=20, max=60),
+ ),
+ inter_trial_interval_duration=ExponentialDistribution(
+ distribution_parameters=ExponentialDistributionParameters(rate=1 / 3),
+ truncation_parameters=TruncationParameters(min=1, max=30),
+ ),
+ quiescent_duration=UniformDistribution(
+ distribution_parameters=UniformDistributionParameters(min=1.0, max=1.0),
+ ),
+ min_block_reward=0,
+ is_baiting=True,
+ extend_block_on_no_response=True,
+ response_duration=1.0,
+ reward_consumption_duration=3.0,
+ kernel_size=2,
+ ),
+ )
+ ),
+ metrics_provider=MetricsProvider(metrics_from_dataset),
+)
diff --git a/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/metrics.py b/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/metrics.py
new file mode 100644
index 0000000..43da247
--- /dev/null
+++ b/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/metrics.py
@@ -0,0 +1,35 @@
+import os
+from typing import List
+
+from aind_behavior_curriculum import Metrics
+from pydantic import Field
+
+
+class DynamicForagingMetrics(Metrics):
+ """Metrics for dynamic foraging"""
+
+ foraging_efficiency_per_session: List[float] = Field(
+ min_length=1, description="Full history of foraging efficiency per session"
+ )
+ finished_trials_per_session: List[int] = Field(
+ min_length=1, description="Full history of trials finished per session"
+ )
+ total_sessions: int = Field(ge=0, description="Total sessions completed.")
+ sessions_at_current_stage: int = Field(ge=0, escription="Last consecutive sessions at current stage.")
+
+
+def metrics_from_dataset(data_directory: os.PathLike) -> DynamicForagingMetrics:
+ """TODO: query docdb for metrics from the previous session
+ https://github.com/AllenNeuralDynamics/aind-physio-arch/blob/dyf-curriculum-doc/doc/curriculum/architecture.md
+
+ could maybe do UPath do be able to do local and remote files.
+
+ docdb_path = https://api.allenneuraldynamics.org/v1/metadata_index/data_assets
+ """
+
+ return DynamicForagingMetrics(
+ foraging_efficiency_per_session=[0],
+ finished_trials_per_session=[0],
+ total_sessions=0,
+ sessions_at_current_stage=0,
+ )
diff --git a/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/utils.py b/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/utils.py
new file mode 100644
index 0000000..d298f33
--- /dev/null
+++ b/workspaces/aind-behavior-dynamic-foraging-curricula/src/aind_behavior_dynamic_foraging_curricula/utils.py
@@ -0,0 +1,28 @@
+import os
+from pathlib import Path
+from typing import Any, TypeVar
+
+import pydantic
+from aind_behavior_curriculum import Curriculum, Metrics, Trainer, TrainerState
+
+TModel = TypeVar("TModel", bound=pydantic.BaseModel)
+TCurriculum = TypeVar("TCurriculum", bound=Curriculum)
+
+
+def model_from_json_file(json_path: os.PathLike | str, model: type[TModel]) -> TModel:
+ with open(Path(json_path), "r", encoding="utf-8") as file:
+ return model.model_validate_json(file.read())
+
+
+def trainer_state_from_file(path: str | os.PathLike, trainer: Trainer[TCurriculum]) -> TrainerState[TCurriculum]:
+ return model_from_json_file(path, trainer.trainer_state_model)
+
+
+def metrics_from_dataset_path(dataset_path: str | os.PathLike, trainer_state: TrainerState[Any]) -> Metrics:
+ stage = trainer_state.stage
+ if stage is None:
+ raise ValueError("Trainer state does not have a stage")
+ if stage.metrics_provider is None:
+ raise ValueError("Stage does not have a metrics provider")
+ metrics_provider = stage.metrics_provider
+ return metrics_provider.callable(dataset_path)
diff --git a/workspaces/aind-behavior-dynamic-foraging-curricula/tests/test_coupled_baiting.py b/workspaces/aind-behavior-dynamic-foraging-curricula/tests/test_coupled_baiting.py
new file mode 100644
index 0000000..2550465
--- /dev/null
+++ b/workspaces/aind-behavior-dynamic-foraging-curricula/tests/test_coupled_baiting.py
@@ -0,0 +1,181 @@
+import unittest
+
+from aind_behavior_dynamic_foraging_curricula.coupled_baiting import CURRICULUM, TRAINER
+from aind_behavior_dynamic_foraging_curricula.coupled_baiting.stages import (
+ s_final,
+ s_graduated,
+ s_stage_1,
+ s_stage_1_warmup,
+ s_stage_2,
+ s_stage_3,
+)
+from aind_behavior_dynamic_foraging_curricula.metrics import DynamicForagingMetrics
+
+
+def make_metrics(
+ foraging_efficiency_per_session: list[float] = None,
+ finished_trials_per_session: list[int] = None,
+ total_sessions: int = 1,
+ sessions_at_current_stage: int = 1,
+) -> DynamicForagingMetrics:
+ return DynamicForagingMetrics(
+ foraging_efficiency_per_session=finished_trials_per_session or [0.0],
+ finished_trials_per_session=finished_trials_per_session or [0],
+ total_sessions=total_sessions,
+ sessions_at_current_stage=sessions_at_current_stage,
+ )
+
+
+class TestCurriculumStructure(unittest.TestCase):
+ def test_all_stages_in_curriculum(self):
+ stages = CURRICULUM.see_stages()
+ stage_names = [s.name for s in stages]
+ self.assertIn("stage_1_warmup", stage_names)
+ self.assertIn("stage_1", stage_names)
+ self.assertIn("stage_2", stage_names)
+ self.assertIn("stage_3", stage_names)
+ self.assertIn("final", stage_names)
+ self.assertIn("graduated", stage_names)
+
+ def test_enrollment_starts_at_stage_1_warmup(self):
+ trainer_state = TRAINER.create_enrollment()
+ self.assertEqual(trainer_state.stage.name, "stage_1_warmup")
+
+
+class TestWarmupTransitions(unittest.TestCase):
+ def setUp(self):
+ self.trainer_state = TRAINER.create_trainer_state(stage=s_stage_1_warmup)
+
+ def test_warmup_to_stage_2_on_good_performance(self):
+ metrics = make_metrics(finished_trials_per_session=[250], foraging_efficiency_per_session=[0.65])
+ updated = TRAINER.evaluate(self.trainer_state, metrics)
+ self.assertEqual(updated.stage.name, "stage_2")
+
+ def test_warmup_to_stage_1_after_first_session(self):
+ metrics = make_metrics(
+ finished_trials_per_session=[100], foraging_efficiency_per_session=[0.4], sessions_at_current_stage=1
+ )
+ updated = TRAINER.evaluate(self.trainer_state, metrics)
+ self.assertEqual(updated.stage.name, "stage_1")
+
+ def test_warmup_no_transition_on_first_session_bad_performance(self):
+ metrics = make_metrics(
+ finished_trials_per_session=[100], foraging_efficiency_per_session=[0.4], sessions_at_current_stage=0
+ )
+ updated = TRAINER.evaluate(self.trainer_state, metrics)
+ self.assertEqual(updated.stage.name, "stage_1_warmup")
+
+
+class TestStage1Transitions(unittest.TestCase):
+ def setUp(self):
+ self.trainer_state = TRAINER.create_trainer_state(stage=s_stage_1)
+
+ def test_stage_1_to_stage_2_on_good_performance(self):
+ metrics = make_metrics(finished_trials_per_session=[200], foraging_efficiency_per_session=[0.6])
+ updated = TRAINER.evaluate(self.trainer_state, metrics)
+ self.assertEqual(updated.stage.name, "stage_2")
+
+ def test_stage_1_no_transition_on_poor_performance(self):
+ metrics = make_metrics(finished_trials_per_session=[100], foraging_efficiency_per_session=[0.4])
+ updated = TRAINER.evaluate(self.trainer_state, metrics)
+ self.assertEqual(updated.stage.name, "stage_1")
+
+
+class TestStage2Transitions(unittest.TestCase):
+ def setUp(self):
+ self.trainer_state = TRAINER.create_trainer_state(stage=s_stage_2)
+
+ def test_stage_2_to_stage_3_on_good_performance(self):
+ metrics = make_metrics(finished_trials_per_session=[300], foraging_efficiency_per_session=[0.65])
+ updated = TRAINER.evaluate(self.trainer_state, metrics)
+ self.assertEqual(updated.stage.name, "stage_3")
+
+ def test_stage_2_rollback_to_stage_1_on_poor_trials(self):
+ metrics = make_metrics(finished_trials_per_session=[150], foraging_efficiency_per_session=[0.6])
+ updated = TRAINER.evaluate(self.trainer_state, metrics)
+ self.assertEqual(updated.stage.name, "stage_1")
+
+ def test_stage_2_rollback_to_stage_1_on_poor_efficiency(self):
+ metrics = make_metrics(finished_trials_per_session=[199], foraging_efficiency_per_session=[0.5])
+ updated = TRAINER.evaluate(self.trainer_state, metrics)
+ self.assertEqual(updated.stage.name, "stage_1")
+
+ def test_stage_2_no_transition_on_middle_performance(self):
+ metrics = make_metrics(finished_trials_per_session=[250], foraging_efficiency_per_session=[0.6])
+ updated = TRAINER.evaluate(self.trainer_state, metrics)
+ self.assertEqual(updated.stage.name, "stage_2")
+
+
+class TestStage3Transitions(unittest.TestCase):
+ def setUp(self):
+ self.trainer_state = TRAINER.create_trainer_state(stage=s_stage_3)
+
+ def test_stage_3_to_final_on_good_performance(self):
+ metrics = make_metrics(finished_trials_per_session=[400], foraging_efficiency_per_session=[0.7])
+ updated = TRAINER.evaluate(self.trainer_state, metrics)
+ self.assertEqual(updated.stage.name, "final")
+
+ def test_stage_3_rollback_to_stage_2_on_poor_trials(self):
+ metrics = make_metrics(finished_trials_per_session=[250], foraging_efficiency_per_session=[0.7])
+ updated = TRAINER.evaluate(self.trainer_state, metrics)
+ self.assertEqual(updated.stage.name, "stage_2")
+
+ def test_stage_3_rollback_to_stage_2_on_poor_efficiency(self):
+ metrics = make_metrics(finished_trials_per_session=[299], foraging_efficiency_per_session=[0.6])
+ updated = TRAINER.evaluate(self.trainer_state, metrics)
+ self.assertEqual(updated.stage.name, "stage_2")
+
+ def test_stage_3_no_transition_on_middle_performance(self):
+ metrics = make_metrics(finished_trials_per_session=[350], foraging_efficiency_per_session=[0.67])
+ updated = TRAINER.evaluate(self.trainer_state, metrics)
+ self.assertEqual(updated.stage.name, "stage_3")
+
+
+class TestFinalTransitions(unittest.TestCase):
+ def setUp(self):
+ self.trainer_state = TRAINER.create_trainer_state(stage=s_final)
+
+ def test_final_to_graduated_on_excellent_performance(self):
+ metrics = make_metrics(
+ finished_trials_per_session=[450] * 5,
+ foraging_efficiency_per_session=[0.70] * 5,
+ total_sessions=10,
+ sessions_at_current_stage=5,
+ )
+ updated = TRAINER.evaluate(self.trainer_state, metrics)
+ self.assertEqual(updated.stage.name, "graduated")
+
+ def test_final_rollback_to_stage_3_on_poor_performance(self):
+ metrics = make_metrics(
+ finished_trials_per_session=[250] * 5,
+ foraging_efficiency_per_session=[0.55] * 5,
+ total_sessions=10,
+ sessions_at_current_stage=5,
+ )
+ updated = TRAINER.evaluate(self.trainer_state, metrics)
+ self.assertEqual(updated.stage.name, "stage_3")
+
+ def test_final_no_graduation_without_enough_sessions(self):
+ metrics = make_metrics(
+ finished_trials_per_session=[450] * 5,
+ foraging_efficiency_per_session=[0.70] * 5,
+ total_sessions=5,
+ sessions_at_current_stage=3,
+ )
+ updated = TRAINER.evaluate(self.trainer_state, metrics)
+ self.assertNotEqual(updated.stage.name, "graduated")
+
+ def test_graduated_is_absorbing(self):
+ trainer_state = TRAINER.create_trainer_state(stage=s_graduated)
+ metrics = make_metrics(
+ finished_trials_per_session=[500] * 5,
+ foraging_efficiency_per_session=[0.9] * 5,
+ total_sessions=20,
+ sessions_at_current_stage=10,
+ )
+ updated = TRAINER.evaluate(trainer_state, metrics)
+ self.assertEqual(updated.stage.name, "graduated")
+
+
+if __name__ == "__main__":
+ unittest.main()