From d17481765fd0b8a3e940cc0cb6fffb1352b84707 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Mon, 23 Mar 2026 21:31:13 -0400 Subject: [PATCH 01/40] run some formatting --- comprehensiveconfig/json.py | 11 ++++++----- testing.py | 18 ++++++++++-------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/comprehensiveconfig/json.py b/comprehensiveconfig/json.py index 7d12ccb..2a372e8 100644 --- a/comprehensiveconfig/json.py +++ b/comprehensiveconfig/json.py @@ -7,19 +7,20 @@ class JsonWriter(configio.ConfigurationWriter): @classmethod def dump_section(cls, node: spec.Section): return { - node._FIELD_VAR_MAP[name]: ( - cls.dump_value(node.get_field(name), value) - ) + node._FIELD_VAR_MAP[name]: (cls.dump_value(node.get_field(name), value)) for name, value in node._value.items() } - + @classmethod def dump_value(cls, node: spec.AnyConfigField, value): match node: case type() | spec.Section(): return cls.dump_section(value) case spec.Table(_, type() | spec.Section() | spec.ConfigUnion()): - return {cls.dump_value(key, key): cls.dump_value(val, val) for key, val in value.items()} + return { + cls.dump_value(key, key): cls.dump_value(val, val) + for key, val in value.items() + } case spec.ConfigEnum(_, True): return value.name case spec.ConfigEnum(_, False): diff --git a/testing.py b/testing.py index ada70ce..2a5cba6 100644 --- a/testing.py +++ b/testing.py @@ -10,7 +10,7 @@ Float, Text, List, - ConfigEnum + ConfigEnum, ) from comprehensiveconfig.toml import TomlWriter @@ -20,15 +20,15 @@ class Example(TableSpec): class testEnum(Enum): - '''An example doc comment to explain what our enumeration structure does''' + """An example doc comment to explain what our enumeration structure does""" + foo = "burger" bar = "chicken" -class MyConfigSpec(ConfigSpec, - default_file="test.toml", - writer=TomlWriter, - create_file=True): +class MyConfigSpec( + ConfigSpec, default_file="test.toml", writer=TomlWriter, create_file=True +): class MySection(Section, name="Funny_Section"): """Example comment under section""" @@ -42,7 +42,7 @@ class Credentials(Section, name="Credentials"): email = Text( "example@email.com", regex=r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", - doc="email of the account" + doc="email of the account", ) password = Text("MyPassword") @@ -57,7 +57,9 @@ class Credentials(Section, name="Credentials"): key_type=Text(), value_type=Credentials | Integer(), ) - test_enum = ConfigEnum(testEnum, testEnum.foo, by_name=False, doc="testing additional doc") + test_enum = ConfigEnum( + testEnum, testEnum.foo, by_name=False, doc="testing additional doc" + ) print(MyConfigSpec.some_field) From 2967a4ab1d2c1661e86e796fd36eec8194ed13ac Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Mon, 23 Mar 2026 21:32:12 -0400 Subject: [PATCH 02/40] fix bug with trailing spaces on toml --- comprehensiveconfig/toml.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/comprehensiveconfig/toml.py b/comprehensiveconfig/toml.py index c506970..dc62bb4 100644 --- a/comprehensiveconfig/toml.py +++ b/comprehensiveconfig/toml.py @@ -133,7 +133,9 @@ def dump_field( if isinstance(value, spec.Section): return "\n".join(cls.dump_section(value)) real_field = node._ALL_FIELDS[original_name] - doc_comment = " " if real_field._inline_doc else "\n" + doc_comment = ( + " " if real_field._inline_doc and real_field.doc else "\n" + ) if real_field.doc: doc_comment += f"# {"\n# ".join(real_field.doc.split("\n"))}" From 00b59e02ec27ef15c303dbe55cfd8993e6916ac3 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 24 Mar 2026 20:05:01 -0400 Subject: [PATCH 03/40] fix global instance setting issue + add some basic tests pt1 --- comprehensiveconfig/__init__.py | 4 +- pyproject.toml | 6 + requirements-dev.txt | 2 - requirements.txt | 5 + testing.py | 1 + tests/__init__.py | 0 tests/conftest.py | 14 +++ tests/test_fields.py | 63 ++++++++++ uv.lock | 203 ++++++++++++++++++++++++++++++++ 9 files changed, 294 insertions(+), 4 deletions(-) delete mode 100644 requirements-dev.txt create mode 100644 requirements.txt create mode 100644 tests/__init__.py create mode 100644 tests/conftest.py create mode 100644 tests/test_fields.py create mode 100644 uv.lock diff --git a/comprehensiveconfig/__init__.py b/comprehensiveconfig/__init__.py index 3a61fcf..832cde7 100644 --- a/comprehensiveconfig/__init__.py +++ b/comprehensiveconfig/__init__.py @@ -48,11 +48,11 @@ def __getattribute__(self, name): return super().__getattribute__(name) - def __setattribute__(self, name, value): + def __setattr__(self, name, value): """set attributes from active instance if available""" inst = object.__getattribute__(self, "_INST") if inst is not None and name in inst._ALL_FIELDS.keys(): - return inst.__setattribute__(name, value) + return inst.__setattr__(name, value) return super().__setattr__(name, value) diff --git a/pyproject.toml b/pyproject.toml index 3eb7dc1..d533c05 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,3 +51,9 @@ Homepage = "https://summersweet.software" # Documentation = "https://readthedocs.org" # Not yet created Repository = "https://github.com/summersweet-software/comprehensiveconfig" Issues = "https://github.com/summersweet-software/comprehensiveconfig/issues" + +[dependency-groups] +dev = [ + "mypy>=1.19.1", + "pytest>=9.0.2", +] diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 03ca99f..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,2 +0,0 @@ -# dev -mypy \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ed7dbfe --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +# NOT THE REQUIREMENTS TO USE THIS PROJECT +# In place for github dependencies +# dev +mypy +pytest \ No newline at end of file diff --git a/testing.py b/testing.py index 2a5cba6..a2d3838 100644 --- a/testing.py +++ b/testing.py @@ -65,6 +65,7 @@ class Credentials(Section, name="Credentials"): print(MyConfigSpec.some_field) print(MyConfigSpec.MySection.other_field) MyConfigSpec.some_field = 12.2 +print("clump") print(MyConfigSpec.some_field) print(MyConfigSpec.MySection.other_field) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..d1e83af --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,14 @@ +import os + +import pytest + +OUTPUT_DIR = "tests/output_files/" + + +@pytest.fixture(autouse=True, scope="function") +def managed_context(): + """Manages our files per test""" + os.makedirs(OUTPUT_DIR, exist_ok=True) + yield + for file in os.listdir(OUTPUT_DIR): + os.remove(OUTPUT_DIR + "/" + file) diff --git a/tests/test_fields.py b/tests/test_fields.py new file mode 100644 index 0000000..646cabb --- /dev/null +++ b/tests/test_fields.py @@ -0,0 +1,63 @@ +import comprehensiveconfig +from tests.conftest import OUTPUT_DIR + + +def test_blank_spec_create(): + class Foo(comprehensiveconfig.ConfigSpec, auto_load=False): + """Basic config""" + + pass + + assert True + + +def test_blank_spec_create_auto_load_failure(): + try: + + class Foo(comprehensiveconfig.ConfigSpec, auto_load=True): + """Basic config""" + + pass + + assert False + except ValueError: + type Foo = object + assert True + + +def test_blank_spec_create_auto_load_working(): + class Foo( + comprehensiveconfig.ConfigSpec, + auto_load=True, + writer=comprehensiveconfig.toml.TomlWriter, + default_file=OUTPUT_DIR + "/test.toml", + create_file=True, + ): + """Basic config""" + + pass + + assert True + + +def test_int_field_auto_create(): + class Foo( + comprehensiveconfig.ConfigSpec, + auto_load=True, + writer=comprehensiveconfig.toml.TomlWriter, + default_file=OUTPUT_DIR + "/test.toml", + create_file=True, + ): + """Basic config""" + + test = comprehensiveconfig.spec.Integer(10) + + assert Foo.test == 10 + Foo.test = 11 # manually set instance value + assert Foo.test == 11 + + try: + Foo.test = 12.12 + assert Foo.test == 11 + except ValueError: + assert Foo.test == 11 diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..3ffd42f --- /dev/null +++ b/uv.lock @@ -0,0 +1,203 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "comprehensiveconfig" +version = "1.0.1" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [ + { name = "mypy", specifier = ">=1.19.1" }, + { name = "pytest", specifier = ">=9.0.2" }, +] + +[[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 = "librt" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, + { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, + { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, + { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, +] + +[[package]] +name = "mypy" +version = "1.19.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" }, + { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" }, + { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" }, + { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +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 = "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 = "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 = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[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 = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] From 73ab44bb54314ad4e5832584bd62ee8c24ca5dc1 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 24 Mar 2026 20:15:38 -0400 Subject: [PATCH 04/40] fix validation of lists + more tests --- comprehensiveconfig/spec.py | 63 ++++++++++++++++++----------- tests/test_fields.py | 79 ++++++++++++++++++++++++++++++++++++- 2 files changed, 117 insertions(+), 25 deletions(-) diff --git a/comprehensiveconfig/spec.py b/comprehensiveconfig/spec.py index db366c3..9c8b16e 100644 --- a/comprehensiveconfig/spec.py +++ b/comprehensiveconfig/spec.py @@ -22,6 +22,7 @@ def __new__(cls, *args, **kwargs): class ConfigurationFieldMeta(type): """Provides custom union logic for configuration fields""" + def __or__[S, T](self: S, value: Type[T] | T) -> S | Type[T]: """broaden normal Union behavior so things don't break""" if not isinstance(value, type) and not isinstance( @@ -57,8 +58,8 @@ class BaseConfigurationField(ABC): """The python variable that this field is attached to""" _sorting_order: int - '''The sorting order for dumping the data. - This is important when sections are taken into account''' + """The sorting order for dumping the data. + This is important when sections are taken into account""" def __call__[T](self, value: T) -> T: self._validate_value(value) @@ -72,7 +73,14 @@ def _validate_value(self, value: Any, name: str | None = None, /): class ConfigurationField[T](BaseConfigurationField): """The base class for an inline configuration field""" - __slots__ = ("_name", "_default_value", "_has_default", "_nullable", "doc", "_inline_doc") + __slots__ = ( + "_name", + "_default_value", + "_has_default", + "_nullable", + "doc", + "_inline_doc", + ) _name: None | str """The actual name used inside the configuration @@ -82,10 +90,10 @@ class ConfigurationField[T](BaseConfigurationField): _nullable: bool """is this value nullable""" doc: str | None - '''Doc comment''' + """Doc comment""" inline_doc: bool - '''if a doc comment is present- should we try to put the doc - comment on the same line as the value?''' + """if a doc comment is present- should we try to put the doc + comment on the same line as the value?""" _holds: T """describes what type this field holds""" @@ -98,7 +106,7 @@ def __init__( name: str | None = None, nullable: bool = False, doc: None | str = None, - inline_doc: bool = True + inline_doc: bool = True, ): self._name = name self._nullable = nullable @@ -350,10 +358,11 @@ def __init__( self.inner_type = fix_unions(inner_type) return super().__init__(default_value, *args, **kwargs) - + def __call__(self, value: list[T]) -> list[T]: + self._validate_value(value, self._name) return [self.inner_type(val) for val in value] - + def __get__(self, instance, owner) -> list[T]: return super().__get__(instance, owner) @@ -361,7 +370,7 @@ def __set__(self, instance, value: list[T]): super().__set__(instance, value) def _validate_value(self, value: Any, name: str | None = None, /): - super()._validate_value(value) + super()._validate_value(value, name) if not isinstance(value, list): raise ValueError( f"Field: {name or self._name}\nValue was not a valid list: {value}" @@ -427,7 +436,9 @@ def __init_subclass__(cls, name: str | None = None, **kwargs): } cls._FIELD_VAR_MAP = {value: key for key, value in cls._FIELD_NAME_MAP.items()} - cls._sorting_order = max(field._sorting_order for field in cls._ALL_FIELDS.values()) + cls._sorting_order = max( + field._sorting_order for field in cls._ALL_FIELDS.values() + ) # generate default value cls._cls_has_default = all( @@ -481,14 +492,15 @@ def __init__( ): self.key_type = fix_unions(key_type) self.value_type = fix_unions(value_type) - self._sorting_order = max(self.key_type._sorting_order, self.value_type._sorting_order) - + self._sorting_order = max( + self.key_type._sorting_order, self.value_type._sorting_order + ) return super().__init__(default_value, *args, **kwargs) def __call__(self, value: dict[K, V]) -> dict[K, V]: return {self.key_type(key): self.value_type(val) for key, val in value.items()} - + def __get__(self, instance, owner) -> dict[K, V]: return super().__get__(instance, owner) @@ -596,14 +608,16 @@ def __init__( super().__init__(NoDefaultValue, *args, **kwargs) self._left_type = fix_unions(left_type) self._right_type = fix_unions(right_type) - self._sorting_order = max(self._left_type._sorting_order, self._right_type._sorting_order) + self._sorting_order = max( + self._left_type._sorting_order, self._right_type._sorting_order + ) def __call__(self, *args, **kwargs): try: return self._left_type(*args, **kwargs) except ValueError: # if left side fails, try the right return self._right_type(*args, **kwargs) - + def __get__(self, instance, owner) -> L | R: return super().__get__(instance, owner) @@ -627,12 +641,12 @@ class ConfigEnum[T](ConfigurationField): _holds: T _enum: Type[T] - '''The enumeration type''' + """The enumeration type""" _enum_members_reversed: dict[Any, T] - '''A reversed mapping of values and enum variants in the enumeration type''' + """A reversed mapping of values and enum variants in the enumeration type""" _by_name: bool - '''whether or not the field value is using the - enum variants' name or value''' + """whether or not the field value is using the + enum variants' name or value""" def __init__( self, @@ -646,7 +660,9 @@ def __init__( self._enum = enum_type if not isinstance(enum_type, enum.EnumMeta): raise ValueError("Type must be an enumerator") - self._enum_members_reversed = {v.value: v for v in enum_type.__members__.values()} + self._enum_members_reversed = { + v.value: v for v in enum_type.__members__.values() + } self._by_name = by_name return super().__init__(default_value, *args, **kwargs) @@ -661,7 +677,7 @@ def get_value(self, value: Any): if value not in self._enum_members_reversed.keys(): raise ValueError(f"Invalid Enum Variant: {value}") return self._enum_members_reversed[value] - + def __call__(self, value: Any): return self.get_value(value) @@ -677,7 +693,6 @@ def _validate_value(self, value: Any, name: str | None = None, /): if isinstance(value, self._enum): super()._validate_value(value, name) super()._validate_value(self.get_value(value), name) - __all__ = [ @@ -692,5 +707,5 @@ def _validate_value(self, value: Any, name: str | None = None, /): "Table", "TableSpec", "List", - "ConfigEnum" + "ConfigEnum", ] diff --git a/tests/test_fields.py b/tests/test_fields.py index 646cabb..61a6169 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -53,11 +53,88 @@ class Foo( test = comprehensiveconfig.spec.Integer(10) assert Foo.test == 10 - Foo.test = 11 # manually set instance value + Foo.test = 11 assert Foo.test == 11 try: Foo.test = 12.12 + assert False + except ValueError: assert Foo.test == 11 + + +def test_float_field_auto_create(): + class Foo( + comprehensiveconfig.ConfigSpec, + auto_load=True, + writer=comprehensiveconfig.toml.TomlWriter, + default_file=OUTPUT_DIR + "/test.toml", + create_file=True, + ): + """Basic config""" + + test = comprehensiveconfig.spec.Float(10.12) + + assert Foo.test == 10.12 + Foo.test = 11 + assert Foo.test == 11 + + try: + Foo.test = "chumpus" + assert False except ValueError: assert Foo.test == 11 + + +def test_text_field_auto_create(): + class Foo( + comprehensiveconfig.ConfigSpec, + auto_load=True, + writer=comprehensiveconfig.toml.TomlWriter, + default_file=OUTPUT_DIR + "/test.toml", + create_file=True, + ): + """Basic config""" + + test = comprehensiveconfig.spec.Text("clean") + + assert Foo.test == "clean" + Foo.test = "bean" + assert Foo.test == "bean" + + try: + Foo.test = 12 + assert False + except ValueError: + assert Foo.test == "bean" + + +def test_list_field_auto_create(): + class Foo( + comprehensiveconfig.ConfigSpec, + auto_load=True, + writer=comprehensiveconfig.toml.TomlWriter, + default_file=OUTPUT_DIR + "/test.toml", + create_file=True, + ): + """Basic config""" + + test = comprehensiveconfig.spec.List( + ["12", "13", "14"], inner_type=comprehensiveconfig.spec.Text() + ) + + assert Foo.test == ["12", "13", "14"] + Foo.test = ["fee", "fi", "foh"] + assert Foo.test == ["fee", "fi", "foh"] + + try: + Foo.test = ["fee", 0, "foh"] + assert False + except ValueError: + assert Foo.test == ["fee", "fi", "foh"] + + try: + Foo.test = "hi" + assert False + except ValueError: + assert Foo.test == ["fee", "fi", "foh"] From 3103b5196fdc3c26d4038f6b82a1bd9aa2bc72d1 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 24 Mar 2026 20:26:12 -0400 Subject: [PATCH 05/40] All basic test + another bugfix --- comprehensiveconfig/spec.py | 8 ++++ tests/test_fields.py | 93 +++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/comprehensiveconfig/spec.py b/comprehensiveconfig/spec.py index 9c8b16e..1e4e69e 100644 --- a/comprehensiveconfig/spec.py +++ b/comprehensiveconfig/spec.py @@ -490,8 +490,15 @@ def __init__( *args, **kwargs, ): + if not isinstance(key_type, BaseConfigurationField): + assert TypeError("key_type must be an instance of `BaseConfigurationField`") + if not isinstance(value_type, BaseConfigurationField): + assert TypeError( + "value_type must be an instance of `BaseConfigurationField`" + ) self.key_type = fix_unions(key_type) self.value_type = fix_unions(value_type) + self._sorting_order = max( self.key_type._sorting_order, self.value_type._sorting_order ) @@ -499,6 +506,7 @@ def __init__( return super().__init__(default_value, *args, **kwargs) def __call__(self, value: dict[K, V]) -> dict[K, V]: + self._validate_value(value, self._name) return {self.key_type(key): self.value_type(val) for key, val in value.items()} def __get__(self, instance, owner) -> dict[K, V]: diff --git a/tests/test_fields.py b/tests/test_fields.py index 61a6169..4745741 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -138,3 +138,96 @@ class Foo( assert False except ValueError: assert Foo.test == ["fee", "fi", "foh"] + + +def test_table_field_auto_create(): + class Foo( + comprehensiveconfig.ConfigSpec, + auto_load=True, + writer=comprehensiveconfig.toml.TomlWriter, + default_file=OUTPUT_DIR + "/test.toml", + create_file=True, + ): + """Basic config""" + + test = comprehensiveconfig.spec.Table( + {"x": 12, "y": 23}, + key_type=comprehensiveconfig.spec.Text(), + value_type=comprehensiveconfig.spec.Float(), + ) + + assert Foo.test == {"x": 12, "y": 23} + Foo.test = {"x": 12, "y": 23, "we": 123} + assert Foo.test == {"x": 12, "y": 23, "we": 123} + + try: + Foo.test = {"x": 12, "y": [], "we": 123} + assert False + except ValueError: + assert Foo.test == {"x": 12, "y": 23, "we": 123} + + try: + Foo.test = "hi" + assert False + except ValueError: + assert Foo.test == {"x": 12, "y": 23, "we": 123} + + +def test_section_empty_field_auto_create(): + class Foo( + comprehensiveconfig.ConfigSpec, + auto_load=True, + writer=comprehensiveconfig.toml.TomlWriter, + default_file=OUTPUT_DIR + "/test.toml", + create_file=True, + ): + """Basic config""" + + class Bar(comprehensiveconfig.spec.Section): + pass + + assert isinstance(Foo.Bar, Foo.Bar.__class__) + + +def test_section_empty_w_name_field_auto_create(): + class Foo( + comprehensiveconfig.ConfigSpec, + auto_load=True, + writer=comprehensiveconfig.toml.TomlWriter, + default_file=OUTPUT_DIR + "/test.toml", + create_file=True, + ): + """Basic config""" + + class Bar(comprehensiveconfig.spec.Section, name="burger"): + pass + + assert Foo.Bar.__class__._name == "burger" + assert isinstance(Foo.Bar, Foo.Bar.__class__) + + +def test_section_w_field_w_name_field_auto_create(): + class Foo( + comprehensiveconfig.ConfigSpec, + auto_load=True, + writer=comprehensiveconfig.toml.TomlWriter, + default_file=OUTPUT_DIR + "/test.toml", + create_file=True, + ): + """Basic config""" + + class Bar(comprehensiveconfig.spec.Section, name="burger"): + test = comprehensiveconfig.spec.Text("clean") + + assert Foo.Bar.__class__._name == "burger" + assert isinstance(Foo.Bar, Foo.Bar.__class__) + + assert Foo.Bar.test == "clean" + Foo.Bar.test = "bean" + assert Foo.Bar.test == "bean" + + try: + Foo.Bar.test = 12 + assert False + except ValueError: + assert Foo.Bar.test == "bean" From 2af7ee537b34e9d598fde49f9c031ef87841ecc9 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 24 Mar 2026 20:27:15 -0400 Subject: [PATCH 06/40] reorder some of the class definitions to try to be more organized --- comprehensiveconfig/spec.py | 136 ++++++++++++++++++------------------ 1 file changed, 68 insertions(+), 68 deletions(-) diff --git a/comprehensiveconfig/spec.py b/comprehensiveconfig/spec.py index 1e4e69e..8b5a412 100644 --- a/comprehensiveconfig/spec.py +++ b/comprehensiveconfig/spec.py @@ -319,74 +319,6 @@ def nullable(self): return False -class Float(ConfigurationField): - """Floating point field""" - - __slots__ = () - - _holds: float - - def __get__(self, instance, owner) -> float: - return super().__get__(instance, owner) - - def __set__(self, instance, value: float): - super().__set__(instance, value) - - def _validate_value(self, value: Any, name: str | None = None, /): - super()._validate_value(value) - if not isinstance(value, (float, int)): - raise ValueError( - f"Field: {name or self._name}\nValue was not a valid number: {repr(value)}" - ) - - -class List[T](ConfigurationField): - """List field""" - - __slots__ = "inner_type" - - _holds: list[T] - - def __init__( - self, - default_value: list[T] = [], - /, - inner_type: AnyConfigField | None = None, - *args, - **kwargs, - ): - self.inner_type = fix_unions(inner_type) - - return super().__init__(default_value, *args, **kwargs) - - def __call__(self, value: list[T]) -> list[T]: - self._validate_value(value, self._name) - return [self.inner_type(val) for val in value] - - def __get__(self, instance, owner) -> list[T]: - return super().__get__(instance, owner) - - def __set__(self, instance, value: list[T]): - super().__set__(instance, value) - - def _validate_value(self, value: Any, name: str | None = None, /): - super()._validate_value(value, name) - if not isinstance(value, list): - raise ValueError( - f"Field: {name or self._name}\nValue was not a valid list: {value}" - ) - - match self.inner_type: - case None: - return - case type(): - raise ValueError(self.inner_type) - - case BaseConfigurationField(): - for c, item in enumerate(value): - self.inner_type._validate_value(item, f"{name or self._name}[{c}]") - - class TableSpec(ConfigurationField, metaclass=ConfigurationFieldABCMeta): """A model/Table""" @@ -535,6 +467,74 @@ def _validate_value(self, value: Any, name: str | None = None, /): ) +class List[T](ConfigurationField): + """List field""" + + __slots__ = "inner_type" + + _holds: list[T] + + def __init__( + self, + default_value: list[T] = [], + /, + inner_type: AnyConfigField | None = None, + *args, + **kwargs, + ): + self.inner_type = fix_unions(inner_type) + + return super().__init__(default_value, *args, **kwargs) + + def __call__(self, value: list[T]) -> list[T]: + self._validate_value(value, self._name) + return [self.inner_type(val) for val in value] + + def __get__(self, instance, owner) -> list[T]: + return super().__get__(instance, owner) + + def __set__(self, instance, value: list[T]): + super().__set__(instance, value) + + def _validate_value(self, value: Any, name: str | None = None, /): + super()._validate_value(value, name) + if not isinstance(value, list): + raise ValueError( + f"Field: {name or self._name}\nValue was not a valid list: {value}" + ) + + match self.inner_type: + case None: + return + case type(): + raise ValueError(self.inner_type) + + case BaseConfigurationField(): + for c, item in enumerate(value): + self.inner_type._validate_value(item, f"{name or self._name}[{c}]") + + +class Float(ConfigurationField): + """Floating point field""" + + __slots__ = () + + _holds: float + + def __get__(self, instance, owner) -> float: + return super().__get__(instance, owner) + + def __set__(self, instance, value: float): + super().__set__(instance, value) + + def _validate_value(self, value: Any, name: str | None = None, /): + super()._validate_value(value) + if not isinstance(value, (float, int)): + raise ValueError( + f"Field: {name or self._name}\nValue was not a valid number: {repr(value)}" + ) + + class Integer(ConfigurationField): """integer field""" From 9f437bc58915d1c466891d35d55f8718672732e7 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 24 Mar 2026 20:29:20 -0400 Subject: [PATCH 07/40] add new item for feature list --- readme.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/readme.md b/readme.md index bbf9023..f4e4bf5 100644 --- a/readme.md +++ b/readme.md @@ -1,12 +1,15 @@ -[![PyPI - Downloads](https://img.shields.io/pypi/dm/comprehensiveconfig?style=for-the-badge -)](https://pypi.org/project/comprehensiveconfig/) +[![PyPI - Downloads](https://img.shields.io/pypi/dm/comprehensiveconfig?style=for-the-badge)](https://pypi.org/project/comprehensiveconfig/) + # Comprehensive Configuration + A simple configuration library that lets you create a pydantic-like model for your configuration. # Installation -``pip install comprehensiveconfig`` + +`pip install comprehensiveconfig` # Features + - [x] Supports static type checking - [x] toml writer - [x] json writer @@ -20,13 +23,13 @@ A simple configuration library that lets you create a pydantic-like model for yo - [x] auto loading - [x] initialize default config (with auto loader) - [ ] yaml writer +- [ ] Tests targetting mypy and other static type checkers to ensure EVERYTHING looks good across IDE's - [x] section list (via a Table field) - [x] Field type unions (overwriting normal union syntax) - [x] per attribute doc comments (inline and noninline) - [x] enum support (Via `spec.ConfigEnum` and python's `enum.Enum`) - [x] fully supported string escapes - # Example (Python) ```python From a923e7b0422d287278fee7de9218140eb46d7a3d Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 24 Mar 2026 20:33:34 -0400 Subject: [PATCH 08/40] add sphinx to dev dependencies --- pyproject.toml | 5 + uv.lock | 371 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 376 insertions(+) create mode 100644 uv.lock diff --git a/pyproject.toml b/pyproject.toml index 3eb7dc1..1e76a00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,3 +51,8 @@ Homepage = "https://summersweet.software" # Documentation = "https://readthedocs.org" # Not yet created Repository = "https://github.com/summersweet-software/comprehensiveconfig" Issues = "https://github.com/summersweet-software/comprehensiveconfig/issues" + +[dependency-groups] +dev = [ + "sphinx>=9.1.0", +] diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..c703f54 --- /dev/null +++ b/uv.lock @@ -0,0 +1,371 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210, upload-time = "2024-07-26T18:15:03.762Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +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 = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, + { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, + { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, + { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, + { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, + { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, + { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, + { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, + { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, + { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, + { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, + { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, + { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, + { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, + { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, + { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, + { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, + { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, + { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, + { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, + { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, + { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, + { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, + { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, + { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, + { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, + { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, + { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, + { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, + { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, + { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, + { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, + { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, + { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, + { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, + { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, + { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, + { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, + { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "comprehensiveconfig" +version = "1.0.1" +source = { virtual = "." } + +[package.dev-dependencies] +dev = [ + { name = "sphinx" }, +] + +[package.metadata] + +[package.metadata.requires-dev] +dev = [{ name = "sphinx", specifier = ">=9.1.0" }] + +[[package]] +name = "docutils" +version = "0.22.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/b6/03bb70946330e88ffec97aefd3ea75ba575cb2e762061e0e62a213befee8/docutils-0.22.4.tar.gz", hash = "sha256:4db53b1fde9abecbb74d91230d32ab626d94f6badfc575d6db9194a49df29968", size = 2291750, upload-time = "2025-12-18T19:00:26.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "imagesize" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/e6/7bf14eeb8f8b7251141944835abd42eb20a658d89084b7e1f3e5fe394090/imagesize-2.0.0.tar.gz", hash = "sha256:8e8358c4a05c304f1fccf7ff96f036e7243a189e9e42e90851993c558cfe9ee3", size = 1773045, upload-time = "2026-03-03T14:18:29.941Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +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 = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, +] + +[[package]] +name = "roman-numerals" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/f9/41dc953bbeb056c17d5f7a519f50fdf010bd0553be2d630bc69d1e022703/roman_numerals-4.1.0.tar.gz", hash = "sha256:1af8b147eb1405d5839e78aeb93131690495fe9da5c91856cb33ad55a7f1e5b2", size = 9077, upload-time = "2025-12-17T18:25:34.381Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/54/6f679c435d28e0a568d8e8a7c0a93a09010818634c3c3907fc98d8983770/roman_numerals-4.1.0-py3-none-any.whl", hash = "sha256:647ba99caddc2cc1e55a51e4360689115551bf4476d90e8162cf8c345fe233c7", size = 7676, upload-time = "2025-12-17T18:25:33.098Z" }, +] + +[[package]] +name = "snowballstemmer" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, +] + +[[package]] +name = "sphinx" +version = "9.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } +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 = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +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" }, +] From 11df8e7448448d22fbe3352a555991b44bbb9af4 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 24 Mar 2026 20:37:06 -0400 Subject: [PATCH 09/40] docs quickstart --- .gitignore | 1 + docs/Makefile | 20 ++++++++++++++++++++ docs/make.bat | 35 +++++++++++++++++++++++++++++++++++ docs/source/conf.py | 28 ++++++++++++++++++++++++++++ docs/source/index.rst | 17 +++++++++++++++++ 5 files changed, 101 insertions(+) create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst diff --git a/.gitignore b/.gitignore index 6497e2b..0741486 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ env/ **/__pycache__/ dist/ comprehensiveconfig.egg-info/ +docs/build/ # Testing files test.json diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..747ffb7 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..ec8db79 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,28 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = 'Comprehensive Config' +copyright = '2026, Summersweet Software' +author = 'Summersweet Software' +release = '1.0.1' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [] + +templates_path = ['_templates'] +exclude_patterns = [] + + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'alabaster' +html_static_path = ['_static'] diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..a1ba680 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,17 @@ +.. Comprehensive Config documentation master file, created by + sphinx-quickstart on Tue Mar 24 20:35:07 2026. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Comprehensive Config documentation +================================== + +Add your content using ``reStructuredText`` syntax. See the +`reStructuredText `_ +documentation for details. + + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + From 9747e3b7e5bfe37bc8dc2557fa697ab4a5eaf78c Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 24 Mar 2026 21:42:05 -0400 Subject: [PATCH 10/40] basic config start --- docs/source/_static/summer-sweet-colorful.png | Bin 0 -> 92984 bytes docs/source/conf.py | 23 +- docs/source/fields/basic_fields.rst | 11 + docs/source/fields/index.rst | 53 ++++ docs/source/globaltoc.rst | 13 + docs/source/index.rst | 32 ++- pyproject.toml | 2 + uv.lock | 235 +++++++++++++++++- 8 files changed, 350 insertions(+), 19 deletions(-) create mode 100644 docs/source/_static/summer-sweet-colorful.png create mode 100644 docs/source/fields/basic_fields.rst create mode 100644 docs/source/fields/index.rst create mode 100644 docs/source/globaltoc.rst diff --git a/docs/source/_static/summer-sweet-colorful.png b/docs/source/_static/summer-sweet-colorful.png new file mode 100644 index 0000000000000000000000000000000000000000..c919cb5374e1f77f9e8ee8f62e70f4f4dae77b37 GIT binary patch literal 92984 zcmV*eKvBPmP)EX>4Tx04R}tk-JO7P!z_$riu?L4h9r)$WWauh>AE$6^me@v=v%)FuCaqnlvOS zE{=k0!NJF3)xpJCR|i)?5PX362Z)oRit63RCf`c<=^*b^{F|F0Rf44jv1y+yg@v( zX&apPiA7eFRpN8vag#1c{K$31<2TL)mj#|Fn(5>`u}CZx+E{60Rx~x@Y2vV|>69;I zJXSexan{OJ*1RWwVJN4sq_|FN1TicjjszqqsG@{2Y(!|+NwJWm^Qe!1*!3sKrI4!x zMvetkph0&1;D7LYwpM;>(n|_OfzB7l`4|Pdc7aCCalVfor*Q%VpMfjA<*(F%*-z4I zEiHTm^lt+f*DX!i11@)fp(jH&WmgK)67qTA{fxdT3k=)>y=&gwn&&uu0Maz8y{D4^000SaNLh0L z04^f{04^f|c%?sf00007bV*G`2kHYC0wpy}p+^`1000?uMObu0Z*6U5Zgc=ca%Ew3 zWn>_CX>@2HM@dakSAh-}001BWNkl`2;vYl(bnXp^H7YY z6Q3lK;bDlN2sQkEcQ|LS{$ua6&)R#heeV60zF&QA-FwfS&OP^>@A{-_{Kvo$h1D?x7`SUnZ)evm z?sL4{^vfx_2GHI1D?#Mej>v!O?m6A#-7@sgb$60)#qPguX|xUeZ3~74V41<)Z7?{z zSNB=$KijW6lJD%di`*ur`$_#ehwmu98>QbPe9!(mf4;ny+mrq4;$`wVhSJB9*Cy|e zu+5VVOHlv%t;U7N*#8C$%hqjwwg~_HvhMdHzXFC02nL1?jJPKGkHvuk3O_b6MLRuP z23x?k7R}V6nYFNu1v7ydFs4FP8&t-Ch`^T7HWuxyMKf#BOj}IXXV}<3#`w?_&9sGW zE!f5fmLY60r~Gh#NtQqcMkH~ zjiN{Wk-?;VGXk^NhS9d@-_alDVyE-BiQJ3)XLi3KQMZ#)0H*GJsecQB4PuRn-N8)c zc6RYS2;{()$-PmxEySu&x60z65!fW2u*e@T{49$0KmMWLQ-3@1cg5Zw4m7#%$b*g+ zUj_dov2{QXaT=(E-6?VrlqJ$j^)PWws`sWxAYzgN3pO2Ge+&^j=MDE}vlLH-b&-OqB zV(xbme{>UfERg&n_MqrMOFk#w9XM=r6t@|N--ZMG@Mm5~WMRl%H-Xz|M`Y#k{8eK2 zB_Kky0IuRXd}nvXB{>JKLzlTJvC-uZm|(hNpE-8-C@};llL9#V-U!8As~fN3gph< zA+$w*yFU|lpxK49emVQ)>;aQ04!M}p6MOPIun+$w&>?^wcRvyh6BEfm18ylH9tn0& z=s_FIU=Q|866|zX3ZU0*7YXm2VBe;piWdgO!#QYp0Q#b5Bw-fKI|!q2b}-fk7_gQ- zkhPd?%rG4{u#E+LNIRP}sOu_(w9TwVJ8NNQ7S=M@I?(LGQ2^Mp{Z{PUdp5ejfQ|Hof#32+8LoSt=oSs!&(d5y4+v&l_#Km=06LtbKha--m{{Eh=_bXutGI5FqMHZ z6i(h+sL<^n9qZ>G8supBY%`bqZ|?K!v`E28K8Ha$qf1sY%E2;4pd}fc1ip;nDTRxk zStei?@=Gq4$CA~FWyAvHbw;1B@nFav3@HIYRbGjgazyeyE=-7HhD8=Q4db{!en{;9GLH`Hm$yD2-7>%o zOMoHU=gOIVQG)TufBeUI!OuO_hs!MAI09~C2vu!Rk7|sTT(N?T36UIWp#me?E_68H zro875L%}z*4yFq`Eq7o=B7Bk;<#85|GsOi7?%&h;i@V(|M_69Ev`nyw@Ant?J}m>* zy6fl?EhBfoyZHBLy60I!WBc3g0yGwAETQqsy8CUbJ->flciURPcDEV++HV6((S_wU zvhlX#?!B1AcJsf7727_&9qt=fcM-pikybAA4RzTqXJhxd`y8{f z4BNWP(+{5P*d03%{+ECGm#9Ftd;6{GIfi_dwi+2!wJU%aB6sO3twiy45NnhRF?j+) zslv|tS!fY%PUUdA@DYZKK!<^46jxov!P>o7IC#UED+N9Ztgi#X==Q*+MGtlX(AWcx z0*Dgi!u<}I!ZPyU)&pGxel2oPYxDaZfTh6K25>_RF7mrs1YirG7JyFK&H)e+E$e^` z{vyE-;yG3VYPgPpE&6N;e9?ONv`2I>Q<2MMU`M66S=)hW6QDQ5E2)U4tfdT@qVTm} z1_68`%UqOIq+u-z1q>Ffx~Jf~ZaNFQ&%f1ga|e8X_ji96(f$V>cmPj7=EQ;A5Jl<$ zLLbqD8nfwAI1=(n{js=kYkuaH7F$MTl2{OO6alj4D5*^GwJcghXCzAKEfRpt3l%mC zZ%Gep2c;=F8#YRjBIK*k<)`{^jk;VO802$Urpw!*FHn%s-i7eAD?W&v9r=7&cs^4S z7W)v0`jD1LDM3fuv6rD(pVzaY=+Ni&nDnewT8*Ww5G0BYp&+5<@49u5ixlA=F|=e4 z>7pWa=XGK4JP4W>CCNHsX-mvM@Hj2LL~AB;H{b~LvUTS|3aA*?c4-Xm-# zDkyUQ{rBUjr@Q;RFS-@Gt~(2V{lH&@r|sH%cKp3%8iRZFg{#r_+cfM0qB^dN1{S$ifHrz!}q=B%c>j2<*2B z^`OTb>+zDqoYdrgdH|!4dFz2KuiBPM2At{NvcfT?XJ9WXmO$r)#odo9tK|9n=tlw% zX1?cFcq0aup3MVW;{^05%GE&@EMyg)2{aJa263-t*oM)}tk=`Eunqg%oy;-zw;y=m z0UX6g0RYz4)^Z?KcC%L<_d0yypFY@!;hbjpC@M*XCo!^WHOq%Y1($lD(07Im{00gb zl%%~$OC!t|51Dvxq#jA+DibAz4e|${o{!@s7ss5lJelJQVQ`fH@y|~$T9}I@)D_R` z!rm@K?C+z`3)*;DmqU}`unl=Qk&Clk{!qM+C6PZQ5vq#YQJyYN3-tLp89E{g9pn61 zAG(GRwmSgD*&CeZ*77{t@NJ>h%t{PY7AMHUPFLZk9=8?^XK7Q64-3@zyReS>aJ;w{ zX*jCq?sT0Q#ks9C)U_V$y5dB`XlEAF@eI>LGt4F}+NO2g7ybme^TxBV`_;FGH+}HI z2gAp%uC7L4x_kS+Y}M}ky}R+Td$0CY+O8^VA_xc^k7%JBDT{mT5AeQT!sXKqm40Me zh`kZfTv?1>ijQNc7$FayP_6D;-a;swwU-GHqy_Nnhta+=iq#RVp8y&#MgW$8 zVRXWQI2rXo=fYQCfS5V?$!yYKx<12nV}{wJK|8Y^@N}W--FM#|R%dBxDFVA)d(X~5 z`JQ|3@nC0QDkn=Z6@kd8Nrk@7oNgbEL-BB%qtwj&q})K>ZeSmv;_IkT)F9MEQQtun zx>C}kp!M1gc2p>`lhU%R-!?mbDV9$711GeEiIh^8tLp^6GBP5i`yy{8D=&kxT(*_D zenP3PGlb)o3jlEp>aEF zF&q2Kq(M7%?Cy$ld#1MYhO@%$TYu$?4%{xf6~`ZcJYM{$HZQKrE3rG;#Mvz~4v3#u6Oo>iNd(4`pf~`}un~Y(eYM$plm*8_ zZDwg>rKPT;!kOO&sgv zShVFaYefx3JjkLL5%P5Yc(DvLt{tokU7NA%eeK*%W@E?hthIxy`lKg42@gE*z%Xia z90vC6uEx^J2=!8ps-i+2EiX`{$7mkBvfy}P595Vux5LGUAqKdB;Kjr(R3fld&00 zAXk1?%<8gqL00xp4=)M{=1?P~1%ymA_#mIp+s zeAswas?e~k4<#@IM8 z!DM~v!dV~kYJa;gycH*$a6**71AyHZ@5}Rdf!&=GOaiLfpkAs_jVhSRcnmPUNt4P1 zS2+-4M%Tj~M;wnA#k7dt^CGE?Vg;Czd>_G(ELe1UGombR;Z$pvI5Rrweou{A!F5|DsbWQ3C%Yy{KUiOgsQ&5U13u;vtlb@8Aa{uLFl=}+sfi>gNGO!^7D$3t8p#w=fVzN!rifPhy!1+PJ^JqDx zY))#{;*ip$K$nJ_!pJj4H;S$~)*>RVszw*nnRWJHW|HR_)x@(0P9#7_nrZ5Sg$iEq zGLP}G8q$_dV2M^Vc_LejcIrIs*7uKb=m%pQd}srQe&8+}2PT+$HrGsA{Nqgm#*3b&07-g&L}%inDh&K7;I>ulCyy6!yf4(%Uf{oyeVJ>tXH z{S!M>lb|E8{dc?J9zM5&h}n&3wC*S7u?2MCv2W>%rM!Q zVm6tfomm%x+x|OtY^}tI<714Y?45jl6zUR&!{s6HqfN3yrQ%^yu#1@i@>C?}MTJ3d zV-zJUgT=5#wm=~S$-s{XiXRIqh4Msh`ks`n52ugu0-2@ge8a+tMcA;X-~(Zg4o&|S zhqe(=QVNv0#l2)}MzA|$y0EpoEkoI!3F`ShDMf*5k6%hX#`37mq`wv6andCboy<6o zbvsYL7y$DFfjh0{j9t!d+-d6?OxGJs$KJ4WW__+MK2YR(?6_f@IO$OYdh+ivk9o}C z?zQKVTd@0*o4qA`i`mqz$8_8T8+q$}^lXdZtH+jMIR>=!*N9spSqHu>+>JxklvHVQ zt;Isyav!%T3Y|ekAkr%}1#nvgk>{Y$JaZu~0A0ood4ScWRmep*A#2n=*?z^|;Czk* zASwC-r7c$k&|@^y&Lz1G>*dTu9SAlJqqS}wtvG;s@G3F9 zt_PgqCxa-Z46UHSI+I(-aD|FLJ!d(*WJhAvp;8a^ir~h>GbNWI>%mjy+ zU7xRGWhl}DMP;HvP|NKI^3YWSJ3S>KfsS*qLmu+VSu5S@c6R;G`Q>A=`>b2A`|Ml# z@YQMu)qM84ZQC}Sbka%T{&&CoT_vRbi2B;gx8DTzrlC#_pO>dAje&*(sBU5817Qto^l*hNq)o&9CC5Bjy*BZceJ7;go{=A`EmXt>%}nukzAe! zCWj&A(T^w68(wGtRTuqyijwrK2Ue-JO`D(=g{~Z!SIM!K$o&32;QpStJu26Lf(p7333gs}9)|#UU*t77R;(v!2~(z?Qyw`gmyJqP~k^ z=SQ|-$IWMBkMEwydg7I?ft}aKVJ!g6W;3*Hi&IWH1pu(Iu`#$GW|}^4CGQFtjh~+} z#=(a-aPZ-E9C~B}8wbW1Z%ok6T5#LydAVdGq@XAHo%MXLUy}FX=BmDdeEKE@NCrtb zwRw6{2D!t;BVkA-HD4)k%wH#-BgLS!5}JgR$d<8cb3}ziKvrQTL@ib+<1t8p2x*~! z<1vdxts9d3TPg}p*j!RnFcQd25~`Yn?WtAU5}V_w_#$(5G?&@Y0JEFFzs0Er$_h@% z*jf*EJ+tEwN^0z`&*hO9p?k2a{e?_q^R(lpv$5yAec}Gj8@FN41^bFS^3Lndi9l~W z9s_XZ0Z)A56LIL!A@#d|{p(-HY&Od~DEt?&En_xqFD-b6398?N`$Z~A) znZ7fY=#u>aK*od_D4duR?)dOFTzURJ3>~Rb4lY6OQ=j@2zWn7cV>X-N;fEi_{{8!- z*Ue@#eE<93$8pCUhq|sKz`N|4^MgcdbG(i)M=qbRIJ#_(jW zZ{8(#QIR7Yc>%E7Y%)R5L8rjQX>~oy*9nG~=IaufQP)e&wi~>|Xc`;q>KNGg{FcGJ zmokMCi6TB>*LCMc&;RB(zlpYO@rqZxB73j8uCctl9E05l&+}3=a$wvI^KPv#URd1z zTVF=o_%^qx7evomU%;?Qos|n|uP*!*1I^i@Wy$z@=4Y8hz^kkEIJ=jFTy9t;*{30q zJKIe62GQcX9t{AX3ScylpfA0Sl!8Q342ZBD?u;+qX@~_T>Jr~}B zJr{l?5uy=xedxUC`Q!08%i)d3V^me;Eu&(_(ap1P;~5Ru0&VNTxn(@~;DheFG_DP9 zGWG)LrVZmCb2UWLRl%ZC%YxZ?&}+Hv0k7kH9q<)pEXd%HGmzsBuRAUC-=ncF>{RKMMj` zH`5p`!%i(`hi2G#WP*)H#@KjxjPZd9X5$8SX2BLX;nK(BuJ?ai%@FOpVH@_m`W65r zfDYJm(MPiHx$D~Vu1OtKP)I;zIX3l zFf)uXgIjCY2hS62Z&G|b^W7G05F2+V0_&gW^m+~2)&Ubbn=2v2UX`>?ik zV8^Xzdp*3I5dAl9pziinm568RX=n1Kq- z@K_}DC@z~_Ja?u^+1h}c~R(z`FZNdZK{*XRoXfS>u9 zpTS$+@)iZ?&hMEWdf<01E*{KgT~DpWjlXg`&ilR7!OS@M$*1>&zO4;K3o8c>AwDr! z%fhuN1*VwoIdJpr4sDd@YZ0?S%M$eZY51yuMesemo zW4_*w#MxlcU0WFOW`0_%6cSXR1F{1!V}T0TfeL^s8r?J2xG8XptFx&!QhG8WDx7qZ z#(R>FC+{uII!j)4xj8@@x(*j&dY#>Xl!tUZG0&UMJR~qLwHqqoY}Y1clf0dlL+ZK< zC>gTd4c{i=qSLV?YZ+hs&A-AozVQw0xcQvEJ3;{GzyD9q!tMX@PjSZE|E|cKmyY^my<-a)wlczmU*f9N~Ru~2Z8+revCm(ta zK7Y?`psKSu>)OsN*tq5yrrsM+R-lO(Rt_PT+>beUhWFPD!zQ@s88ZH)k`Fm2vF!uX zPI8mih0HXobG@FKv`<==fr6{uAp+ysvr~n zf*~PgM@h9*y6-CaLnKwkT>6n;$8A|TlZpUKynian1#<0LS_Fv0M_C!=*mzR{VhVhU z1Vu_%$k;-^tjybSpP!Sr!TN04lF(qDJ25A*p3^wWqH6qcsff-&Zid-p#OQ({};r{)9Dlk4<5v+Z#~h$jXzL7`WtuR%-{O= zn0;jx)pHKHeXs)RT=|3CM+i zCILOfpeNol{{D&MG5U2Df?rZZz!c^~aB?Q+ZaynK;%7$o$XH;o&L235Q<8Y5f!7v8 zRu#68001BWNklPP7rRhRg&hlOn*hK>4M7AQpDe5{%fKRh@LJD>f z+lv8EGA~NTB1R#8CM79|n~_+vXEKwtA{#R}D)mJcn&g=k%~ip!i3K1^(2Hika-S2D zA+0UgCB>m^e=r(DFs4FX*BC9e;D*7rfn|%Gx10-Z-Fsbmejl#xxZxZSPz1XU=(=^H zLR-|@+8Tc9r+zAqeEeqq$J-yqKmOxCqG_6N>6}$Q_pbj3r~V%&M#7HoeEI->{Iq_X zk!P*8X)&ENsH_E56m&Dp;!}y-KpMKp<$>NMoLniV9FJ8p>v2q3kQA*eykZmz?bi%3VhC*%C>2Lj-_ncv~_U$cqs|I}wKO(0~xT z6w1z<(3mncbr~yW79wP20a_dZUJP~t=%h8009X>88sQ&#ZbAe$z-R>U>Jrc*Jl>GY zkF{(83+%Y@oUqBEF{nI~T zI-LgZyhlCiQE1yX0AABHVYA=wfLcLt0X24X&lNFBB7Tlk<H~BAk7c)hto|Mg%3OqG+46S<;DbWB) z2cLl8#4+eeRY7cpFXo}qz)ns1j!Y$1mjY*0A;MF?JLZ^U2HBou zd0i;Gwzh_2k3BXr1pTWU{|Rl|Vl*0s=iGVcodAHJ_=%qg&wJ_H+?2D=zT?mEivM&H zzJBB1png>b-k$L2Gflh?95d<_nd!o5!B8&;|$z(+igh*>(>4I-~T>- z@Pi-V$xnWA^qNyoJrz&g_5=Wgdq4g?JnnIi!@c+3i^n|XG5CW&_=D)VM;>`3R##Ue zU_Ji$<5AZ&PI}`rL&3&XL4{z4*f*ANRPBwr9{h0cdQ@SwI>Pc1D_GgOjL}LBQ&oOn zk3&t$CbS^S(-GpTcv`5SbXYocBZfL}_hRwZG0UTfY4x*AqsnQLdg+TeT%3&6SA6?f zxEiBNCuGLCJB}qFkYE=BT`U0BnA#l6^z6AQvlHR1NgMrj@o|=GBPGF4tO%k@>`xDC zb4w-(dbu5YEGyB)=$WYlxTx#YmSrq1)5;}Z?) z{={yqXC&ePX6yX!T$@>&>DEerx7~JIR;1|u9(dpZJpS>I$0t7V3B2-^uMEq-^rbK1 zsoS>U!Ou-_{PD-*o_p@W7ryX??Da<;c_fZH>L}cI-+j^Y`|rOW7rgU~aQt^$Veigc z7Y?(Z^rR=@1#f#M*!b|ZGN_ju*tQ1S+Iu2uz0e%xc3Igm0^4IGGjIK?SWb;*F^q}R zkd{5gF11YcMGQIAn!~_rmjfi3r%PrQNAsdtFHUUs8j2hxWouEJ6JoYU3DcWrqDfgk z4233t@qR`DLR09pNZ3{;PfEkNeAvE_fxwk4A0C~!A=7@!N|&@ z;k*Ky_rT@?Vw;4pSzji_e1oGXg2K2(vXtegLqBa}(M+tLSJ9xI_8PX5ZTq}=utbD! zf7^9vNPBl8!p@yLaquq=;%QGi4&VIdH*v=ucK`tHzWZ)G>simr!0u%)dl^3UsZS*# zwfizxy`k6pU9#t_Alh}rD)F=PLPhJv%71&q{dn?|pA7!D>%EAX;p4#J)-4uWnwSgT znwt`|DOK7aa1#kX?w{D_xhP_JU9E#e!jgr~E*;b^b8drR#&VuzPU%Q#Pm|pg5!eMW zFc+Keqn0kj4kUVkVv*ck+^=e+-&{$9r-YbApeqzEV&G9guoTV)IRTfevB-0IaYjoA zy@6D1qM2i*uT=n^WN#|yX+Q@7a6UY^$Jxx^*k|@86HFfBowr=l9HKJ`>E06HYh* zfAcqggO7Z~mhwyYlreC-{b|&Wl1>LjaR;mD$QY z--1J18e0ZgH?-&}7ymdw>&_0#bkY<}sXG8#EIA+hD3TYnoYMiZTVjxegpoisaRo*i z<|yrFn%C64xk8IDf>O6fIr^I!b?^dare>(7f=S`7aB;r+PBJq~x@eK$V#v5$pi z)>{1B&;1;}``z#2>Z`Agp1-oP0wThJ0|&6Swidwd)KgE5o+m5BSH0mTT)pGw0Pk?Mhma%-q5|*}%Fj^gTK<1&G8w$wh)D-31M9xlY6HCL{f60=*hbCMD3yeJu0n9g}f z1;>P(=>;7f4ODB37U>u?)ifn{VYT;EuhyolO}n30MP>VzlG{@|E@Dld(K=IPcS<%h zH_unlkc)N_x_01VS+p-%RtgW2-LYsJ>q1s!;cMqq5>;xER+0O;%>8#J7;*q?K!CNd zGoaaEwBr_b>Ovhugu1R#`vbgh)pDPC_St9Sxo>$|T=AW=<7-R|0M0z~%y9eYqmRb1 z#~zE-)m6Ox?Qf4jaO>8sIP%CNam+EtU}Iwg2M!zv@AtXSeQxgY_JJKA!RxL*7gRgp z@$wNPnB@wlroJV=>$PnxY|}ahOn@;4)u_VK$_QpuIWcOnTm@Bg+2=|7ydsG#BLwfG zj624hu*<8~;mMR!LN$~{AHD{u7>OT;m7>Ccw1hW7OJgB)37C4|>!c_tqs#Jm#JMkB z);3lEKgFmscPM7HPc<$W=z6?3=O%y-D7%di=OCEA_nd-7ESVAR)2ZGr^$INI>q`2% z6r4JC$JXb7Y?6b^L)n-q3a4VL7yv|}Y+PLnZIsk^3mEdxB^Sb?0qhu<9-3ix$gw%J zKt%>s?U~J}aAmjkbs@npN~BkemDwk7XdxV;GX~d=i^ncdR6q? zs;WY$IwW%$$|W9i%rS0Xt_

3Zs<~>eU)lJI3S2QF{MB&&I?0d;6(sRH3R|c&qD+ zlvXfCBv+xR9jymFgoX!#OoZh@a}Rzdvff5vhC@15fLQNDfFj+P7xpd^lE<*GT1Oc* z>8==Rsv#;0eshT3Nu&!frMGimxy)#@DF1+%Qz2L((%X68uhA;T&<6K#qC1qswzb@je4mNyxyYB`g> zu$X0;PE14Qnin^=8V%TTRXu0@rP{z1ZE*5t@hz{|&Q}!XgDdZ7!x_e=b8$ zIq)WcozJ7SA$MmzVEKHU1i>_q=fN#J$F|Y?w6>eMYNI?MM>)fO4r@0^yBn|A%q(W( z7BdfS(~TLLNsD%B@pF%y?2fY;dR#sN<5C8BF zcd1TErAjyEKf_1n1TQ4WrqxXx3Xa8!g(67VVUQ7PxEq z^Z4H$`ZxINqyNn1ARE^=YT1VV*_7!T(}4g1e)Qq454{?@FTP=r?a6M6wY4=IcieHI zVylIxKmF4`joEAlW)58!0PunrydZ?V4?Xly1gsA|^bmgOcU}yt2qv_N)eiIw`2s{D z%s&+!^b9d?Rn%pT^spQJ4YG~@$o;#qu!_aJ3RwshREpWOH4x}*V zL}G$M`&KVa;RGx>@3x5A%8+zS^aFPYIin-e3lnN6ONmx4=+(Hl915Xq40atOi)@D2nIuPz&q~h~3pq-7-yGt~@Rwx!lh3cS0)f`eEs}~Kb4U=dnx0 zi^zExc{^BIs7PDPOwmo4l`)CRaG!#&TxrIuDO6((agsJ*#VIB2us|m6jlir}s7MSq zMcWsVERJwA$u+x8tQIITfK%IGa`A9fpx{1dC-Zj!?1Cw$1i1p(X&Syn;};(U;+t3w zY(xl5olR8hoQ!7^XX-}z2&2%zZtI-qNs2FDz8j7#JjgA>mF zNypxZ9J3o4)XN5=EhCIpYt$A>u9=#sJWvWC2!Yb$PWmt;=%UCsqKd zS_dXpWTG?xWZaivGxVDedOV1JoHXzkLfHgtjBraaMc~UTwNxkqU_pkbS7!CW+DJw; zs^Vg`0Y9}?%5!TNFbRk|EE47?zjFj~oP@C)&f-xh8v$J$zQm!aWN@NTku0-nD4U10 zAwP&RtK%%!$Qc_5oFM=cfm=76$2K-pUfa)aMSaq`>pT#EvcA3^8-*&KY_9LTw^Y;T zWe|2>Omc^#sY(6=g~6tIh;V(VwyW>T0qI}kQ#gD`8h+m97W6JWwR-e51-M@G${*V zOHvhu(Iq*+%otO13r?0vfXm%g;zs_wnxazkC$c;1*r^rsW?PFJZ@du~zwpc~I{L+5J^^?B)?eVozwz8` z8@=H}=i$nWK8zh7IzQ?l6+($@3qNY*vG~FGdjMk3yj^=Q>JM!H+T&h%Odgz}z$gUP zlOGm7T5{FaYNJ(> zu_s*|#w(wNxH6ndWDMzU%cK=;lELMadD=*@;G`Pc14T6JAV^L`1y$S*@(@ui<)pHU zG(Tt4KH4(aX$NR>NXGLp#CMRPGo-9SPBsJZqTl!heC$_0@46N)x*1npbW^^+`+2r| zF1jJnB*aHh2X>$Soi9hlin+%T1G`@MYjclOB7!jnwHFDm9I=8eYpYmYTfx%mGU_Gg za$!B6v3_Hs}O}OIH4}}W@xZ=P6 zZm}Z0`}H3Z+Dh`B5l%S0v@*id@(A@(1ygrzVLUirsZ@+C>2A3wM#~3n^MI*bE?bF# zFCFb@G>~KMO!z~h`OsI5hrS@nVWnYgq_tCu5yRoDmTMaZFSYdyip4qN3amka%|*@n zLqg&Fb0Pp1Q`yRGY$?;zO-Cu2?V&n7ci*RoAe47d*zOVoU7G-1xS#0c0z33QsG+Bo z^I5WB4eWRUup4mi5Q+3|a5Ys1WK0|bOR)FL&b$2d;`?5C@rQ9`0&;t{-;kwzy6^Y* zAOBty&R+Q5S4Cgyic7D<6_)jX%yDN!`hge?JMbX;C2%rmN`|bxR z{*|B?0M5q3WliZI>&Z+d+CkZu6D6767t%OAnprZQ^|*%mi>^5nAG-8o!Uz(&9sPoT zd$wOM!sLEsrOqydv;OhjmtMCSHNU#hGNWy*>;Ij#ux$(Y%DDCE0vpMD(~U`o!+aK6 zSyp~kPE#lm(B=Ft>a<8BXbsHfvb_c8C=U(!h5~f41Z8osOF=Gg;Zo*KAg|V%Z0usv z5doBK(9`df3>_z=`a48Jb|PIMG=Lsvb~HE^l2wpP*S9G2h8$%@T2)-R>D_tHt5Y@ISXx>#n~`vpEFQnW_34GLIhSyb?HH^4eO4j9w_AP zLf8scEu4m#T{)yR#5s%6zyVMIM9KZ7tSFJ5L^?X8GjWx?U9#L{xxBuuOy=JV=72Id zmf3)np+%ezIL~NOA(T;=Ueu=5_AOj8rxzZVp{$LWT)4KJusZEDN;*^2Im1T&c(^kNK)0BfsULj zkanPCT|Qij=;XZ7{;PN`o%MrUxb*^v3aFM0z=ReAo>`zFTzc)<_`vpk;cZ`c?YSBI z(#I_V?|%2Y@$PrOy9Y(_y03oqt5G=H>D0O@XN$190Fr^-iQkieuYBbz;D7BVlFgD^w*n=G> zZ^K#jV>6=USd_})J}1Kp+T+YbHDXbu z6(;gNU63+RI}#P=V3*xbfH8=N3)WI|OvXj8bVq}@T=8P+lv(l9Q)c3bEly?Xo28N-vHr-7jc z7SiqH8P9kIZvNHV&@_z-b~Io<7=fMGpg;K16pwy^`;FQLn2x7txkWWL&fU0%>!)3< zP%l@Yk+0g$EzwfK&X5%%ii4S?hK^%w>1XJ0Hvdf0veJ2=jwxacb{w50`+>VdC1}YA zDUlsT`>E1&nRQg8HtZM*TNQ>^$gx3dhL@30QZSHg3@H$jUWlMH)R$(SP(n=%n1Dh9 zJ`J>T@RKQq*i%n3!a2+b$`6_T>MZAsQc_6}lcu914gjuv z&2`vu%|+Sr>2!+OYz86Cgc+x@x^1@q6YDl5PLckX-yKYr>_fn5_j;dj%vj>#Ez zZDz|AmUsjhpsrke&&0~n%ZpJyRy2=u*k3=DEQZ8r&?6%`z2%=xJO1Q)?)v$x|FQaO z@{SW?S;!kwJeH9u8_n1)kHX_ZnuE05Uz)!|*~C*XT8PKBfjXKkQxsOFOmB$w&6jQGTt4$=6V2TvrnB9}Cb~OT^;Ht! zx*5!3E;g7{bxb01O$mmwGj>ImnMJ$NVsdbX*})mwNegaWTUK4ySYBDe($WY973{Qek=P5>}2}LA~mrJgkHH>ljEtthp^D?wem!*Fa?CQG4qaO7r96EGp5bO>fJQ${w zX@CE7-`~R+gQxw{PsVybe?t!MssHLIG|zej)Q~GGu?4s)XR@G_%XvOoKVFGedT&c5NK%kg)&xdI^wayvnE%lO!9KZ{?w>Lnlos;b4T zYQQyshTIHVUzE| z&pSS^ZVEHb5A6If*4EZ=?6JqF++49)rT1d2JG$fxs=oJ z19BYx*laiENB~HA-i`{T6)_#2^GPpZo+_lEx%26hLx8v+OV~Dq>0|~l&{VCPPul=I zb8GvW4{XEU*WMC=TMB$y9&a|AVLF{+X=y21Zj8Z;U;JWx``h2fpa1!vtFM3NGoOjO z@4h>G>>1B^2LA64ejU%dr3D}fGi7%PC!g(M=8NQjI;N!qj!%@rW0BIC%&6LFiD27KAL zLFhA7i<^g08pO~uIppDFZiB9y3lv>bO8L8FmTX?AfbM};-b04-B(&fOmI|zV@&EuJ z07*naRMTQb8T@2s#rPkC$#j!cv>Js^o+IvM_1}+4VR=Kz8>U>|LVY-6ApoxRpw#IC zn|Neo2UwN?v=-n7Y=Mfb^Te|RV&hU<3(7Vm%OfJpW;6A9&wu{&anw;qsm!hW``XvO zratz9H#}RhZX#T{qi;of{hkY4t~Ie;H-1E8H8LTC(_*rTaCVW+M<~gt zhLwOdqtCzP`8&bzs4Gmw?2e@mo(O3ihovnZ?AXGEtTXEi4mQl5wJoEaxb`<`(Jrmy!7UgTh71fUx8E8A zV#aH(-iE!eyCuufr64E&9(dq^XrQ*#>797uiMZvKTQ)_+cU`$1*Z=BAvwR=`?6~s6 zP-w7zYrET`MT_8V-u>jUisDp4DYu(!e1cBuvU-036lvl8Zx|K{-tm|tg;wlc$K;C| z!swGjc#!8ux{#H$46-)usu0S?^JA4R2WYb>5*9@Bqk0yy!J*Sr=4t{i&Zkuwohdkz z$T5wD>Z=NR8LNx;7*>CFR@iJRE~Y`jQLI%XXs)%nIG|5)xWP4ufR9a|OY>vy0k)1! zfkXbNV|igOrGcC|7t{~|?40x+G*>B#-8;xaLZbo^^#gvMp{=_Ff5!<(KkHC(IaN>z4sxp^(;}>`DxE9;CZNnpv zJc5G<55iiDjg1YA$76hP_gxr|$Cys1m`Cy!jX6XQ}Uv-jG&tGL%%7(o0ji$`*4U@`glkw$l$2P(61S7bMC(r1`kQ zxMfO_jzDobf$n8zFf^B9po_z>Y7^B-Vj=*<0#X#@L%6lM7W(Ot2MRKn`xqD8CLrY< z5%?uw%bYk8i*bz1nX_@r-UO5_TH6L~n?=t$Sj>p!L}$ne*o70f80F^*5!M-bwieCQ z4dj^~nqs!zpqaMd#(F80XL^l=o%&*gC4BO-&*O|MPIa>^$iRSL$e=P6Mpccvu3=0C zSh(Dt1+IBrFG8M7CjL2Qt{x8n9{uP?HcMks0)(XIa?TN5A?hi`@ zr`qD-h-nt)_t^a|oXMEgkT?-OL3{cukRIYBu zb1a1GN@E?f8@HJG*|M|s#+S58$}%PjKOR?L@j2RX_`H;|}sm zu0A(_-tzLY8s3)v{_M~G45QIV{f-+O8<Zn0F_j7H#Xc`&sXJ zxt|}xxcdYDFP^>QsX~3v^9QRB3mudeFcjPIF$e6h39ysVrimeygWxc7c%-;xD%yMH zZQ|&FjD)!nfQebJSlkpK>zc@#;X9_p^M3=@oo^$9AgS@vv~(KdLuyUM{fu15_BaLn z(3=E+;w}&>%?4g5q04hj4oP%Oa_Ni+{$2xG*jcMh3<>z%a)7 z&UMR)Om3K1sJAocVXQPWXc~*@xWV+$)L(o^JF{Wnwr!lVi?g-LE1TU$e)!>s`%?@7 zrY2NN2KBO=(Mpvo@?G-#=SJ)+H52IOO{8J#6Q1ydhz&~M`{#fDXB=_F5o&mx{ldms zTP>~BSUzG2D_fVbvULedM=WEsIzqitdqV_gfA#DgPmAWzk|A}Z;9SFW)|aIwvc!vm zogT_+|GW2%OZasV4EyVJ5L!=0e2x^H_w`Bp^9v&NTK}lG(gQbsUXjWz;K9Fm0LL@bX!M*=&Yp)}U%yv`ypOzY!bV73Kr^6t>Duz))c@_ z>Ib6^k9p;M$-c5C{v7DI6#EFqr&elNgq)HEMyybjR-<*0OwaTe&*B`TtD@_k3Ce$h zK_+ZYx~@pJ*MXjGeEx3g3l)t8x4j1+z_{zT{#wXp`mJ#K?o)C5WuJ?}O7>oHwy|g$ zpA#c??#NZ=q2Tgl7hieKCghHc9amqR$fGD!T_6AW$1moV5-P5a<<-j->Xi{jt4mlu zVhPJzm$ACGg4H8evC`dMTfxfKWh`%5#?nfkyQ9#`MZ<>XYN)DY-<0P?%x|y`5QnUt zKVu2(f;i_@Sv)Q=g`;Su#2kH+4p=C<73aX!Ul^oXn)eN^B*r3A-%QUU zy!G$Zx1Blg>;Cn?Cqi2Byy$Sy-aid%GmkzeJF$%`M*QV(-y@#mQ0_Uu@pOE8*B6HO zCjw6Xug}NFFZ&Fv^=xU{U^h)J%6g_2mc{y@RF8x_Q0&~{1m`-Mxj3=0EOfVf!F&od& zG;X~>1gZ?G+8LaVmTQbgBUE)4&brf00GI_rSrTW5!yIv;c(ayg{!*d15wgaa?GbT4 zlVKhsNnL-lnGWf!hS+92b@bI_ClD^hn+3aA-k}#$K$=%xc|2nba8iE45!>rc4_L40 zMqzp061S-0a^aTP6bKi1UN+N}bJo7RDG@LEn*0x<@5Kv%t z01A1~(vGkwxKZ&rTmV8&!A~+c!~hp3qbUPDPO7#fc2yUyN`^-f!!sE)rb+Kyf)G`Z zkvJ;TaLx%sp&5WzT=8PucG({R0M5AlCHTY}KO1^(o7NA+nKr>5y<=HnV3FL@2484wx3(zC7>bElz7J;iL?9Q~*1mGqq8` zYmvTBt->gaABc(!9OeG9nX5r`nbE>)^XTNMaN@~+>}km&bXUe*Mpi#L-D>? zF=#E1d|Sn=l~vKj;;fE_DiWF0WFA<`5SC?u0{LVu`B;(RAps&KiyBK;Oa^>1q$Nm0 zQRLk{a=X}6LrBr+!O!gaLa_#Z+7&0`j>|reJ9d38XT0g=^^aR?@zFPb5+8lb?EA1mU zV~Xj<6qEG{#)l>t9~fhNaDwr{2`1|kG~)(sV?+2K+djqfPACFE8uq5+MZiw{(n+eG z26A*XM>IpP|I^>+kl*l$=FnNubyRe>JCo1wn3NxoqPvp2FS7X*QQ<%i$m`^`0JJW2EmdaaKpl`0y3)Mn|HVVuyrqg4A}y>7O>!v;=Mu$v z_k-X^7;W|gU^o%E$5W|-`X4_0<1Q3z{l!LI7_!+ty4`Jyu0PavA!mch!3N_;#u)D( zW46)2HjGd2h;wx>z2e_RdB*PF9hZF$r(f}+uutE1*&pGI%U=R8;kGw@2CsbYX#jvv zyyer;`@QO&FAu8e(&WT z&^@~(8;Okn%^OcdYb`$i&M)D?2Om^_t`EKCW?b~{ZQl=)3)4?AQGIUAJKSRp*9bOL4)0ggJ=scM;Xuc+`&Qd1l2_AnDmIfgKeYoCa+3 zJ&$V=es(Ouijr9avTcmh+X?6)dA%svbY~|@mZ@VLOY2UH0G$XCN$b@u`_f4!7aNHd z?t^<)m*zF0J0zFnG1}MOj8P&-A>-l`t#lA0y)$J%&X^+r#X?~@evFk~K8{09VoKr! z1T9$bUF2L0&0;ogH{jb^3+~)c48eGTux%JH?2969eO`9uDIo>$sa>Cs0PplGUWD5( z`y;&K@)zTb%U^=q-uPLZ@&1=CAP?z;w+;wC_O{!@V<-Lc^T3vI$M4(;qJAn_YV!^N ztv3TD-y%kdvG8?oNdWG>XQk0-wqgvz4R79ui>}z#Z*4CK4<{SZO4>UjaVQoE4<%t7 zpu1xinO%X^(Ok%*!BDmbJPD@}L(>XUvX53M-4oD@2D2z*HHKv+TmZEc>^P(JnQ*sg zOuz#JfH(s|6Ak6&D1X;ioO9=5C?=#SLeO0Lr6W?Y$i)MY$wMTL7KoP@V0JJMAPPe)xA|!A)PLCGXHe6$Muibi=?~#>=lf z6lt8~%L$KQ4b&irpL zSN8?Bj6ZqLpW?@U?8orE?|l!etE-WfyR5ugc2OA_w@q(+>%IiwwxKjnTBo3d8{T{? zF1mc1*X23;`f60eR8Ep2vvV4gi!9dVFddO%8LrUk{jud-9c@yfKpo(9jnR~|H?d<$ z_Ser{jNMNOsY8(1x|I7c<1Yz606Bp;#n1$ zQ;SO9lOLUis)Ibh33(16Eht(^--Br(fGg6y=nPLP_+`y~nU+sXJIgvaxYq!Nc8cC4 zz)l&naf|836iw%C*V-QVrVZN0?f<4CR7+LRoQ2uBjT^ye8#kZ!^ebM3Px|UC0NnP* z&$>$O`%lAdZ~F9MZRfSMHGKR(-+|+gKOW!z{`c|7BaalFPtN&ouL66`+=t)lyzVY| z*V+B?KqyV%g_`sO9J9N88%(FcTOMI)WeKC@8g=dan#qXCH*vFVW-wRA_Jd%;~!;Tzr^sEIqG*k^S{0vAOAn^hyd-VqmIH6M;w9s z?z>OHsD8)U_V!odPu}-sY;0`c760ia!K0335rc-0eFAX(n|oGx@#W{Z@P1TdX?f(@ z&9*FKX?29rQZ1M-qGE_FG-bsLEt*Vl#0P z3v1;8>Az{^4<2JsExQf_GUOPZHyVJQp=leiwYc50y7ce1H-8#uy!U0e?Jb|eX8u-J zSMk`#J{Eubw|_ebc57>ExaB{7EZo28Z6C$q{$2F`b6g(K=Q69h!f1Jf~)Yp{}37eMlILrV^sLqAZ$oY;ae!=)Q|6G%#Y%vB;Q0gt#1| zT&UiNSr-()c%ksBcl(uRB~E5?Ar0UD4AT^ZCC z8^$MJcPCEUeTvW!dUxM0Y$jL4Fe8Jis+>HlVvudnPOO_v3o7R50AyfS8o?<4wr<^u zZ++`qgJ74+Q?$Pe-nlJSL8Ql!2RIM~%O_J;sA_}JsKU~!XLidYRLc(ROi~S$VJr#T zdjgd9!a2YPB%>f8X5B3gY_y5ddv))4(pSCu!a`#4BWW?~>87p<1qphQFfMKO7b#ED zQr0D19in!yGz?DlcP6uN8ZK1C%nrffMH(nXT^dNiE@!W#=Icl=QzSsc0~$IWkdyG- zWt!g|W!1%nD!W-3kVV^>n17t*202gTs|E(h+)%O!JuFTlOu??b8=uHl3=B^}CA4N$ zZZ?HKin(^4aK;#)%SHxz8bB3`7b2WJ{`lkZ!WX_Udu@7u5leSDq>VVum-kias;&a7 zt40;-u6K5I38SSE>Luft9g#oADFEQ5FMTQA`ObG@ZEdaieiyuBo6s#L&3R;Cj6qe^SXy4f%IY#!*H*E* zwgp>`+=4Af9f8%gEm-!2imI-?=_bn&jEQ@3C)ZEsyNsb zTI5Rd6TOEvH&lpwhSMZ0$6@SY=2^JZ=gK;(;sWsH)v}~Lj3Iy{2Du1Oht?tM*(Jfm zDEf@taV1%=1dt^dhjuc}emHT3kycs&aN6ak;xljhE}7 zvx4X_eoMyt-uJ$2`Lmz>?9AjdWp-cv>Q}LA*DgHt&_m(C%@|<^bS&HLdcQ$K26bIy zX=Mp3TUW7V>lPfbwu&uVS6$w2WrWev2$d0L1g_Oy=py5j1!W*7fsO>oQ3iG~0CISY zeg3t(hP37v&+VU|T)KTYx!*lrNBZJuo;SynMP;%XB=U3djDarHRCS+Da#SS@FlN?Ya% zk2B^$u{?=Ob10I1j?>tmlspYZ>6aduIetEuOO^)@9>nAD=m0d`a2i+wx8j6l`cZ{# zRe_ooXI%a=-1f##WqCRcj4rw45?pi5HL%uZq3LO-ot8PAbno+&r#vO(_DVo^;qRUY zBH+5W-4vE>f7bDZU*qk@|54|R3p(3ZaN%h;NilB=YCG_ensRao&XUGEpIjGAc zMaLrM8MCU?DzqS#K(1$c@qw7^m|PO8y63E~uSas6PIuQ<7N$GCEP&RhP3j89w6Mmw zVcZM;?cKW%qFF(#7pz!raKm5d1sz^X12M$_{A^I{psb5+Vt!2#ejpyO>}}e zoW8h}_IM1RaJ6Da9UPl|FRk@ExmN8JlEfpn@R$i55+T+P% zoI2c=s>tg4Cv9NJhxnvgJLY5ul^s6TRp6`zY~w~Mw$m2v%!z}o?|^WHI(NWvQVfaf zZ-nRnU%!CQzWMV&%V-vvuSz*$1At*v3-zI}!6gpyX(<^5j&u1oRycfS@_zx5hi{nl$xkwIk) zs%{99F)*k=Yyz-P4oxvWIK|}94DGlDPaT*O_eBqqEq%6Jag}V*4Il5`s~gA@zhB6& za$kI+tjf!Mp+YHI#06RI3mvRq@U`gS(J;pJrPWnXG*&=$4Jqz_hj786DcS)?d15 zXLf1<+xsG)g7xwnhe0m|FGYld4mt=w``OQWHGDt#!Ig<9p8x!!Ehjtkx&;*cnWA`uACWHv{pCV+gtI!8P0fQ|v}z)r;%Ex^wL z8#|!uQUFY|Oaj-NJ>$;+UgHc78BQCUBxV!e-G%1m$6bZSO*r!h&=d(&pRNb zhy}X-0SWL6y|b8w#S#|Iw08a_PG#^5VL>q?GtWf$T67zKTl>iQ+i1b^B~!#V-ay;U#DO zA@<*Yf4@QDKyTy5jd<3xo`nMsJP?3cJ(H8#6#VYk`aUZ?A)IsbS-5@cdvW{L_xh;I zN(?Q)WXure8r@8T=M3%$iVYJ~(LdM2G#6K|DDL5{L8c8Vevi`5Ux zY^{&QWznS4gkt;O-OAAmjLDb>X!5Sd&P+gy7o$!bk{QW%Piab6Tmy?sV1+oRHp{IA zvRzjN6zu8y?dnqp6qQ;M^iq@ z?cKPbvJe`{{Z2g}nUjy>?=gc2?E|efI<&e)t4&b!+UgqiJ-Le2J*${a=jgf+7+sAj zR5o+98dYE*m0*moZlnHW59;ltQ`}P0d!>oTw)Nwky(T^c+kOQ zAaSZrUkkkhiRfLjDU%(ENju~6zq^ecaCu++-y^7h|99`S` zHAYnvDjP6ek19|_=-Lj`JyT5gPSH+V=uYEHTOYtNHy$PK3*&){9>(UKoBUt+z(o%i zpS9y}Jl6N_lroGdh-NwTqd@rFC11eG$_iFjS7Wfd?i9cM@V6iOAO3IKeBLH}@Zt|g zx9x1+g>+h31bXM+dT!7Z7G8|T?O8!p3eBX(%K8j<)xL%74HFe9z|X#q zKu!Xnw|DOYJOiNN7V+0&&_l9!HuSfPW03kONNt8qK@4o!x|!CN&e9;y7Tt7L+XZa7 zAI^mrdx{9DXc%&0>g(S))H{`_P!bdrz9(mQvL`4*!b83;iQkK0y@PxUCFg3(F4)tc zc$b%nwkVWn<(=6QL!*J)P#(#cY3mmM?(mwSG8$)M<~09;{qVsA;8@$I&d5W|MdLGX z`8-~Jt0^P9mW&%ptI>^(ubpNSmcxa;Tju_WZpyUWL3QtiyRhZPjqU-8HUHUrKBlr?>VbMQ_BlxXC-?~kyl=i$EcXT zD%+e34;y8#Ej#rcT%@Ifp(za#jam_j4jDye6>uOXUc8>kQJAw*sf2dyPl$23FxAY3 zya@;;R!iCU6{IAp!0w8gy9o5$&yrmR?mf9IU%oC73@-sB+y{n()=wpnNGi zE0x0DWv9Q?j(U0`%qQD9XC%|gzyc|_T;il#Uyje8YeI0g+;W2L`I%lD)h4T=YmN6E zbh1Y*{)o*xkHUi&J%poo9#xhX3edaz!uxRI&f{ZggwK5tv6G;px82L$cC6`?kqTuk z!rtmzZhA8aggdsruNT-#W$uOz8$!w5)M!S=00Dw!Ip*hswnsI_D`PaHk!TRhuhhkc-p?5>(yz|$BTP- z((qH7fR+psI79A8!#lGG9u=eB{)>fxzXC~lUcN%wZy$2`Ubi7H{vV94%tVFjV+K`!X|3wet z=$)IhGsXiKKZK)i*bD-2|HTjXYTf|gBS2 zRSNZ}!Dw8g9yMs18dYOj0Pe|gC2X9f+eLZ0taV<}Mod&Bm<^@NiSpKcE&5)$nUOF# zN`$@y%7nw2Md-EPx}6<(nckfRJ&nQsT?BX%^pKB&WiQI;?c$K^6rfZg@H!u2#QU&| z0WbMoS!$U8Ix(dVLkcRpEhnsq@NM9li{+#Qy_0U(k~2FlgB<{zdczxR`RXJi1dy^PRHuOe-XnnSoSVBUj719RfT3$V>B_IcBT)gp}W?!!jwWYt}$6R#$;uTdOSki zm?4ZG?rS%o2!QAEa}D@)9t@dv4tU^1c%Ba@`i2bO?^X;zrL@}A$nomwCb0W?Ic>2S1%0mkzsbcX!Bmq0b>`n@& zvxjpTqs#z?cTs(9p}|qUU-c=!F-!X7l3j*fG4$UvISm zzOHRCpS76on_{*)!)(oH@aD5Qx_N6r?|hR4cKo|v<~K3CwRplB9=JFLz5jOAUjfLC zHq-uDT28*=hL^+nm6)N0!d;idTN8h>^9`{+&bg70lxo=jDZI8GyyGDpbJo#OS$6Yz zw_xin=i}~M?!lJxxA+sXs!aIKXwsmrYZKtzw&*&;@KmKRnlxD1ZynZcSiyKQM%~m# zn=9!m|g*;rD)iG9D`Y@oGq8Q%YYt|uOUk^BmZ6@d@OZ_aRj7&XbVmxOO>>} zEQ!nguil9)_4n2?z@eW0U1`A4KiTaAzNZFyELOuphJ|N#g5_ywbhec0+79i!#e6!) zd}_+v+1?rE)45@K_F%mCg8QQTcCw9$#cVdi+S(e9IOFipp9CVsT6$oA_6EZqUv=@T z#6U6(E9%;Sm}a(*hdsGh3ggKL>o%^#eox!au)Mm4s>nCkqTZ=wc^>py4}dIKp4@Ma zm>n--c?G~*1bT$%af*PB`qRw-fUf^`%J5|WE@piZyNgQN{_+-mfL8#a)WH}4@pWIyigMQX@6dT^iiS32DeM8*X|!F7cG_aLHp6V+4Aa%A0lm3nB=TSc zc3reMQS5*N4#3l%_B3zFw8-2u1-+B6KN$qVe@(#d)SahB%J!S7STuF-1w9=HFoBDbmEvr|2Nwx}*q}iPfp&i9 zc6J~o`*vv$FCSV2U_-IIb)QRy7Rj=H>P8|nUn@#d#CqAC=;xu3Hz8yx()A=3J(JiG zrbwnjSkRC>eJ4G9@nS?if8&)Sj|-kX063Qc6RL!P2<@dYo340%NSt3Sh?u z;Gjh^nE^a`*7xOBXIMu&Z%uh?FZ1af?YuSnwq0lSae~>2)`Sn60pb3O@Avod2S4}$ zp7pF}6+uk`pVm5(j`Y2~apxPo{NzpncJA%$o6ZuzM)=B~e+6}2qpoYb_zf=xh;Z*M z_uz!{wqPi?%ds7zxSZR0_D)=K$0ewn3geYA)^Avk4VK|mwxo4#BoXLYz+-&+q6dd56qC zhXxpB_Loakvi@A@{ujzv&+d|P)q-L{{R3x&)o?Cxa2*T7@_f~!OHpx)@@ja^&8e>G8v<)%}%h??6FwjcYvoI z=yj%_s=L;gyDT;`Njdg8;=KZDvK<_O~<$ z%Jw|W-SbI3$jladlF8DrUSy#=Vzn$=@8YoepvrNJVfawWnm{4RiTYE}fJ|&mebFII z<&U{1c5uOzHMVG{e!cFZUR}@NVcC_m;VJC!_5SzbKwyr0ckG50X06OXo|axZ@#c;d zZILoNpjBO=(>2scqi#(}t2#E#dLmjnt2V?v8 z?U>DG7>!2RZB9S^bbRo_4;EP>C62e=z8=8+@4X*w+v2>N&j*39@3FmD|J?P(?SiOP zPXXuv;H~d@tLf{FYK$jij3*O}$EJ5@O5ZAJ)x_qTKB18SD6zasmDlE(Rb&l7IbSIc z5loRoRQ$rDL@q@I(jW|iqd}|$I!gO_;v|qVyqo{&HqY?n&>|{fML#cNa{hKX^Gg{^ z*nu0G&L-j5wtrU@`g=n{-fu z?QCZ3oOEaUe;^^eZA^1tw`>?-r}3qWo?4ddvTvi&2&2&md-m*E*atl4rt|RNdrfJ5 z%v+94h84-{Qj~8up0_jlnU~!DRxf0(>=0wLGQwot1nbtXGZ_|RBQ}=hY}!W-1B{gA zwU*_HvX%=23~*xB8V@OuvbmJu5l0$47KFu@?uq$nZ?>&ucXX|ni`zMtBLPJaAKyolK$N74NxfZQdZ`XCFKhD&ZJH#;$%aRMVxeA*c8Ok5#Oydj zmNWx`F_;U{$M%Y!rv*Tl163S=ow7epHYO+&iZjy!z$*Zq_*z&O({U*)LC*ml$wEil zyPFj^;pD1e@@Hoq+F6Hg+M%6y)+5jSl+o8~v@h;VFHM<19&iWN3e=&uPe=6gdsh2&JL-=Vj!-1#t$e#@nG6uPu(V$m9QT&F+%r5FSSicCSGK8=5;JCZ z9F>dtw3hq9m=nz<$8I0)W9?ly9M}oavz$)bGrODEX(40sZ4nBuL)K>Bwre%od1p*P z?M|)x-C*E{Kl~w{fnRjn^TyiAR~UIFY`!@VzwOzxCyGrhy&ZVqftb(d_|cDkRP6Dk zzxT1n9>XRa6}=K~-g=Y2e!;C5h22{cnmwl-ttuPGXqjD8qiGs6qY*~yMi`IBCU>^3 z%ti{R7tlG)p0>XOq`+is?wPb9M?Qq8kk%bZ(a7gk` zUoVuiB{^*ENvl<6AiN&Cm7s_wmOcH-P8- z)5ys;%xmYk8@Av+Ve8cAT$}>cPk;K;C}uG_xau1J_HX|-XziZ9-Lf@~ay4IFCuEvwt z641p{+?dgojRcTvNTlIF#iyIFz}ktC#Siv^9rx^_E(v@C*9U)20gM6=CB~xS@8Ocq zMG=U(JeZI=EWzx239Gf}-EjQI$;LOKWD0_YB-vPD+b>CwEgbKml;q^(vJ;{~99_*ZyhFtm?MXQk%0Ra+vgi_c| zD9IG(y*om_>nQ*^J$^XRnnpv566iSqQKICftale@+Pc3vTeMG%K4bTjbi={`AC*d3 zv1ka=>fXImtG&!%UBVjKHeZ;E%`Aot+DHuhNJ)&0iQ_!k~U4ErYPvqs63f1 z{GJr_;@({lm-@j3*_;BHp+?(j%-77?*nD+nR>mygRZWeq?NE;^Ff-a&i}}1oH=P@k z&(=(3kACOzc;IdK;^=oCAA`44Rw!jsteC*un#r$ap~qi$B8Un1Z2Lm?SiEA#$-ZxP z*V{jxef>S}c@H*k-i)?wJ-hpPh95VdeQUfQsr2=rWbMc%tZl9HB|u_$Qi%4ecQHiPu@fgPuE2O0prfobd#K$rIH zbkskS*W;9@EVZog_lDmkn)#VwXCf?SL~bb2t|W!B$A4_UztbAs+-yo15bN;6&;1LY z^{QV62xwbEMKE8Vwq}*gJ9vZ#ee-q4;la1xht1a=7nOG0GCH!6i{2zt9JP-9x;#~P z5~sK+p1P_4CZK2AwrvYRZtvc`0DyxJJ{SOS-+lM_QaJ0OvK3L!=3s5g_;xmjs%p

P&W-K*T*D^co}8N1Oaks5KlytSo{<&AA{rv=2jo+ zu3L~($ehkgJt-~>6Q#(RbHFPKW`xL=#qxZ3Gr*u7Y?RW?pCCdSU{$ao&epNgb*f07Qjn`;pjl#mefqg{GHY2KVz84vG}f?Xj%0cT z!Ev9pd@}f8PQto0l!y~Uz34HK9zu?rNareb{;rMsGGR_`#a+FS^(~O_p{RB1~ z_sn>zDt?iFd^Fi12*6$Iol8If9P_g}EQ;8Fe55}3&cN;tIN$(0@x&7VfUkf3>jST! z`0aVmdmdI-SFv&9Mm+iClK_BkeB&EA%VI#g03AJtwh|mgGdn~;CP)tbRcCx^%UR3h znx-+quG-qUtCEjg4155g1a+=gn3u1T(Z#tMZUEt=W*+cV0K&B7<^F6Qe_Ul@d&$tP z_IH3KTca>*B@!}@!0Hs(2nF2>+^z6X65Sf5eZ^9e)b@#n%2<@9u!X)|w9&CdsmeXA zyTVRv3S_b1IDle5!;?=BEz!R#foa)XAnVy70XsQ3&_O2RqYybM?+3zxIEl|gG8Bmn zfrRc#vO51bP)a6UFE0e+L=AA;&vKJMwm1IgfBt7YPd~HVfK8O!%!H1Oj?AzY?N_>B zL!Dh1PjtwOJ1g0p13Lit@P|K)(@r}L2OoSezWd$p4u)wz_Sj?Cuwg^=v!aZOn=Zlw zf4&QC8-k2pbIBirbqn1o%j`;97>JYv?m79Vul+xG<};s(gI@R?C{>vuhpJ+?5!s8+ zNKgUjNQN)Q2UbxH7co0KkWmTXDN!Ov>@Eho><}gP+g|b{DsLMzJOO&-8rmd#clLHF z=891n%kg?b9>*xa3|x?#AOktIB+!-H58QW3ShB&=Q=;~KzO*3`m9k5KUJNp22AR1A zi~gOTeCh<~{Dd?BpJYwoKKTok|IP^H8E4UrcGE~UBzTF8GKo7+_=#LCo|*quYWy0{_&4zx7&Qi%Y?`n_~Y5YG^-J%;=BrP@ zgO}YCJ>|z-b)r=kn18$8@p=EXEmxi#f_6ao{N>g(4f0%lNh_J~8?qUvTSGnNM?u?sgTdf)f3I;JIL zDY@(=OZpN(=RU7{0Z$h`Mm)9Eyb$nG_R1W;pvs;K;d$UpT9A|;*&1BGte#c!n@yb&yps0mKfpkq7Dru!m zM7|w<_~B`hYL62qGOkfo%Kz;}z)SmOb|a&1YK+HYjG7v%R+)F6>>p58&IW0E>7o$( z*m;hXIv2V1Pz!P@D`iOlQ$ei0?AoX1pYg|$1)bc*<)n>kzps`hA#>cb%Z0_EcLxsQ zVsfw4bO}G}-$_s_0bC4(=|Ca^(P&^(SpPxEl8FcPLgj-)oK^-wj=kxr6y>HmIxd+D z6gC=|xyY(gQL)@?(6XdWfG&2ih+Rv#FttF)Oa%Y{AOJ~3K~yaXjRyg~G>R*-xg;yq zKU*BRX6qCq`oDu1#+d;$ zjj?xXY8$OcnafXHO9W?;N-5NJWmC<@XzCiOstUkqPZkjtB;7n4sYq;YYm^(gDvpFJ z5mBLRm4N4O!x1CH#a8O5(A@8(2B&Vt&bHVMe2oYh5aiQ}_l6;zmOS*$$8s)<|B$1p`km9%Lm~V(#^ole?Nh!aJ`2`&Pj+6W#K@JqY_>NEEq$^+7+cP`q($_#yg^CnD{I>Tk zEMqgEd-o;Le(3MN;t%onU-~AV@r-BSIWIcIT0I$0yLIc=V>B6|u4*qlCK^^D_`Tao z+5VjYOht~ z5x8elA)!P=#+=1oo$S}8Y|VojGT<{q3fI4b|Cn_mQ9&8>STyE11#79<-E4J=)t{|mZTBkXYcuGs!votshog7A0;m+KTA6)XGX1a4TDof;o|K0#yBnLX zinAXczTAB7VZrc9tg8fgLvOF$@kS^jR8*l-HEQKz7!~fg=;nprfBw5KF?~7Zwk6_h zlga<5Fh7tm9*?kY{W`4Qe?LrCR#4Y9D&^c?`oJ#eYGr9l%Jc#wyQM_Y7J7MJ3uIiV zo#Syg_dPt_wP4+Xxr-K-{h-nI?Ud`^Reh6L!S29W50A2(>lk%76CG1wO3T5fC3xC# zh9pEWlZmFgrL#biBO%4c5sO1#pN@-9BxR*5G7V9jTu%L^>p6J5jbwg`KKH1MU9-G*ykNZcsNP)Xm5m z2-My+rFTc1Vn~Q`FNRoW;%Vt#A!hc#;kb<^w4BRP zZoZi5^8kQ8^22w<>!31mEQ?*F{$2+sEGz}Hr%7l1SS1@Z z$loJg1CY&RhA{{{LgawQJvbrB&=>xNn0v8 zH*DB|XFvPd`1ZHIy)dZfbvsT+RaK~~8l#czjgd03jaKJKQsyFr+NM7jdTUM77?5{+ zz1oZktGdEyWEkGM4eL#&tThh^AT}&AjoU!fQ?5#hmn}jKz@_pqDHI)x*;O4K9hdop@XU^&_B@6`=}T}Dv`1<0IOBXKdrD0V^StVh=;P|n$%++0A`gJT)M zlJv!xxhTlY#`xe~h8Nb?#4}aeWa-~0?{Tw8?vyeIxN2%tb>|HMHeK@yJap;j005h= zKFM!tFvm}z{I!nZEyL{O+seuc=JRF=s7x%kUuE+heKuWj z?%0>$!OyNnGaC7DY}{flb+KS)SsWr~4#ZiQ zPNve%B_%@}6lJ5T9_=0XPi2c?G}**gFsW>KQ0mpiHcVXHiOuCL61;R`vMIqYm#ZKS z!YK?)IEj~ARMv9op-k8v_no5LeN3bRo+S%;0o8Kg!|%AeI7pE0#tuN9c;X2hcGzKf@rz%)fL2iO>8mQ#qXwhV2vt>^ z!AfV+)zAgsIkOKNGB@CQtM>1#stS$GoG=fmwQR3zG_~==3kINSuy+?76^tarng>{3 z(l+AI28mVbLR`$8iH%dRqtb8!xjR&vESO zhc6A9xg)PUG3gg_^m5sB#ffaYGPzVVH3Ed0aViNiQKGb%RnEm8^< zDX3ccsi^KW=F=&-1DIR0H{6;1*sAWJ$Bar<7C0+EV5k&;Hy<8g@=l9*$po+4E0 z4BoTn{IG%7#sg`^r@61;P>l1i4B4Pbb5JxLl^i(S}(`D?t@P zV4Qq@;Af;i3rN6?lLNQsZ|0%yP-@5B=eCmR2VMUTR>5zbQrgZMe;Q$K)zsihfvlZ7 zcc#B})Q(r0$5}HTzWnpqvwh?hCngQEtU<^EyVYZ7BHK z4Cokb)u18^c4TBAN>#RJB@SsFCg@#frqtTTODa|Q!GtpjP@;@=PA*uuQYlM9X;v8@ zE!4~OLJu!qzK$E8;y%YlqD^1~Or3$Xu1uItUDtqS%%(HUrpDZqwZ<23{}f(%!Rs-ZOt5~#e%SCd zvj^KWMjWgVETCLpk%$(l43l$G?EhO5?}ua5J2UuIF=n$mX3PubUYery+_3H3AtXDeQc-?8V+yNTUN<&ixMH*UZbd|~6XzL1K>*d!P zruyy+ci_UCF0((-uF1)a=Z?Y34Gsz1nG*o_{4E^08`)c=1ap)$0 zHe?yfIVgKTpro~{#9%@R?>?2BFHtBGO3@6|qr;(rW*?OSk9*@Kdlym(Jxq=Bad(Q% zRA6(&v4s}pp*iRYoEZi>$^xhdfvo^anWQ8g3<%)UK5)3uUjq%VY4tgxa58%5R9-HNh||}))>j~EszM~67P}A*ARtpadA+A0*;_m z?$TfnkBba0D{85S;YHuT#U9^a+3ML`in!bZdW*5HpZQTw2|w_|3Tt32=bpW(>nMO8 zFVFI%2D0@1J;`ZYkb-1-L(Zi%>55!bo^s*&r~Y$~`x&mx?>a_1?=V}PVYY8-CzaN% zSTi~uN>kWUR=4Khhla5FuUwh#TU)Wp9o_OgS zqCfC6SAJw6n7-%Y>v8t=ml)8u;N@`R=8Jb=>y4L+AZ-PL9c;{6v}i2=TF_8t)1+=j zsB078tP+i5G(iT7UKdB+#FT+y0qj`HPIL%#rC5GadQC|7W+EIY%b(69DM<)dAJD~M z-p}mP&kMjSE$8xJ%2&qlTl0 zmWs)SLlN|b!OlNo8Nl@?iDe(llQVMj&g_C<-(D<n3QZ z2CE8j2v0Iv6t3m&IxQMLLRdNou!9f~kpwywOUArAk~0VJ@vHGA)8QqOl(OKMionMy zI2P0=ij;=A$Q%g`rL>Prr1avy$6S=srb(%^@U)ZPqkR(-^7HbKA#rrlA~j8{8{FG(|h5(dXeI1 zBpVh5znW}*Cx#+^!=uyawCQU-dEfu*#l;@@G~=iDJZ^hfrjMmtr$e->T*~zIs-{6b z8lf6DP@@K_Hf8UTSH3D@cBym8GS*GYF#YsrmVP!fT?oY6$ zk7>Ye-=0{A>^QJZUa_pWKUmd-h`A-hEhGo1&e~gGMoW!246g(uRLTMwpY~ zMItT+3)iDy2B55;ox4;Ds1zCPpar&yfJ!lH3YUuFkNdt>!F+5k5Z7gvwfQ&&&vd3% z$13Ks_f)}Rzdyj*v%4NOcd~Gf)G3)m|AEAHW*T(v?!9;1a~><~%oZE%nN)@Q8~Ul; zc`CsGlFZbU)p0Mw0~Wh2OKAa{OSK-T7 zd?qVtQ^p6tXUO(#SDdi@^|*V-U-XvCLvN4X``38Eaeol4pv$t8?ONS@YEs%<)x)g4 zfsrCY*H!=%+EI(H3+Am^#tb9V+o4x>lJwvy6&1UXCGClcHw3Wez+^ApLe(0CC}r84 z3YW@(WNFW~P}1fA5?U-AiOro$JLO7D`%aN%bbgGiBrB62a}RqfF*%(vIq94b5SPP) zOC=Frv=1-Ge5BF7+u$S8HqViQ;|1iC#ik=_8EW+>WJXDZ97qh>6tBu zW@U_KGDbaaP&GBEQaJL8lZ%tu^z!fj{_i;Ozyo_n9t^z&sxl9cBs>~xc2Pr_~eZrFt@SbXfkKZ{7pWdwBYb4 zPH36MB-UIaT%ZuXV?fN|{K0^<6mPvR6hwXEvJ!sZXb&=NjB< zu+jjViKb@bM69YB^~g%c#4xebEslK0$zW!D<*Lv0z80SO%x5n3DlZv*_JQ5=pZ|RP z?n(c}x-=X1@t(K66X(2hn`iZA{z4$lo|+MWoflo{f6iNPy2>Pu*^FH0&O)L^LDWB{ zqdrDA+>D4yS%wbeMFq!m*E=LOy*q`tXII&wMP==wsw9WH6wX!1*^VY6g>ByAC0GG_ z$7AU+S9qY7P<~LP&5{I-vqd-2I}LpVl2L~Jg9sL1V|f&2w_P;E==(Y+vW`+I%dsAg z)3vXXZ=GNTuD>=Yci{qaE3qr;1-rQan3#K(z^|Y&jKtkUJrADbBTDEpOZFA25m4Jcp>FM>K6fAnj=cP2 zfPpVxeOLDEFXMmr{qKJtD=RBYYx{oZ75@cVn@Z_@+l@WcS=YVI+ZIujoyEBE;;a4l zUbOQHzgDKKqe|0^Ouw>jd>_;8)B3}WXjv*y0J><*lw4StamJe3VDs+m=z+?;SMELg zxj-dm20>^X_w-Z1qe2PGRvM(mN!E;5%D@*-X;ENf$#K-Ty+=cM!iiAOd+Tv*%Ocgz zoI>5=;X(R~<33!$1+^#v;a-_awDnMAc4^akktC(eT!%D&$jVD9hG@qd87_&Q#HK^C zTE@O9(sN290elpKt2w+HIr0!R(^zBo)MCCi!))IS?R0LncFq(ac$OV;>B)HHn!9ks zrLXe$WB2aerfkzd=ki#mEm)gIxqBQ#7J|7{bq!s2P}Lk-5vZeZZUnIDvR6m1W&+{C ztNX)we)5x_;OS3)I)3Swe(5RAH~!)sxBKg7?6?TZCOH6JzsU*T(#lDb|EB!p{4UxS;@mfyHS-`E5yC7%GwgI0twE?80E7 z@<6Af4C>SzSORHAx}qFoqhVv=v#iNn*BqSM(?Fk^WJx1l1hmh5fw^V zDl)gy`$aR`67cc@5G=+w7SqZKsbHnB90&__43Vgp-!Cj{ zGtFL-iA5ryf|3<)XMM%iO}PsW#(JjFuC-{VbF?!nEY>X89Tle}446{)%DeE=cbsB2 z6{anS6uMfba+|uTs!T6WDP^`3G)%~JWD3E>Ue|-y3@4F2{pnA~|H|8GS8c_e+jkbf zcJ4d3M?pkhLQz$y>&E!xjYb%a4B(9>6O6`VRCNQTDll0`7?!JRf~^x z!uaxOt%AMS>mKB6FU^6Svi&xd%mRiJ2&F6~-coSG30G_@cQ484K9yv8_JOH$g*0c? z42A}F6i1PQ>;BlTO87G+B0Ca-aKY>nc4sAX2ITApMK?F) ztXcmuB_9>hT)AyXhx0k3v}k65UZcI#G}-;9I1Ou;$0Ua&^_3(XLhk=J1iqp_G90t zLo63b-^)v2b(}b~B)FE^L}gE(`KuH{JIQ*uxJ5Y608Rf>hzR|P2yr4VTyR0*y+CND zQoo=KUEIsW1Vr(|+P3(;ImpG7h6NRp9&(7{5Cfxg+B$j3!H#3`GAGQA6S+yU^Oa>s z^sd9DOooz6pU^tFJt)3zo6%quf8FQJE5BvL9iy9dn6J(;-8;qf$tl)$ubPdDwH7?r zMpLH=Pd@NZ$=SFyE-@T*UbCbzA0wA*D%{V zMY}q;%lQH1L*P%^bt=Cd`}Q~Z>wEX^#cN;t+Uz!)H*dyo{Kju!Hk)DN#*NsxaU%{s z_~7U^CtdhP@p{pb!Gw2e07fb>J;s~ z^^PV+k`j*UAOHBr*ucMmJijcz&xdb61z)@7(>Q$lDOS&?BNp)0t3HW8y8LwjX>b?p zlh{pyHRH>oKX>G%uZ@2G!`FPGUx*IiSwaOTc9Dbo=JnD%;&7Vd)u{8(;1){b=#tC8t6`Y z2V_-|wF@JE`jLkg35_cgkT-3^_5@j=a{x%_Jj?SvJu2c+T*c8B+Y5Q&9F}^zdYm`ZBPoTLFj9euFeefVrmRO5m2U1V z9mz7nK`Di>vIO+yQ^cwY7D_U|qStU!dn>3;T(q`}-lcLjD%JW;1LCY&2|ARqD=s}& z*<&j$%?UEn$2cL z)O_Y2n-Z6-#Kt)&SG6Bp)OCaL$_VS$?}v5!t;b|CMpf0ua*5J-1&;HeJv-C+6qbe7 z&Vn9MXA493-mo(B-m)oZ&+aPf{PobQbKm2DPDiC|6y=zZt~3TEB$**t)hE3@IX~v) zy*u{nGQjZ?5=BlX1Wx5#sy1c!pd>e;0k|y(YI9-qH2gWv1_qomykxCPN=!5-2A@e8 zs|&rmTUZb#-;&ZNR{>m;4EKUH?%JawS{%6aY_t+nJ$XYNbYOk`eLJH=bk$74~tx=Z=p@ z0Ql+)&hTq_o$gR|mDvPQK-0|7brW=2gDMThDw)`EsIcq{fhjl>%bpx3KnKV4teY`N z#4Y;4gcc>Oj>^@nUk9PJLju4BRY=L%DUX0CO>W78C?(~5(vJROyOo!Dg)HMBoMpBq z0$6zvQNm1^slOK`*&rilHzi4B!W^UNWGRVE+$Pi3MWw5@?2Zkya|}=R_Be*rh;5d1 zc!h+s`0psOJIYvH+=ELdv8mx_7bloO%jd07(js{JkWiMoSxDD)n6FK-`s6;W?ODZq zI>(nT{}2v2^x%Lw{eA{cI@A}%cpTgnWUW2b) z`>9@5_0mgU=kN7bM7f&Y?tlXhD6+eww!OjjE$v^fEu+xDuALt#GQB=6-mBk!metW& ziAk-XR0ZXpUM=mBl$Cf8K6~?rFdB`papOj8KJAs>{dl}G#=8ABVB`K9Fj==^1AolV z2NIX@gxQ(vI#B;aXBRO#DzG{@_NMKILw?=&=ioNDTrW=h5;oS{aW*VXhq5`lgEvE! zAIHMvSg-0ESYPbU=cf*K<*8a~QVGs?N4+gI%Z9i-fcwfbS<)iDeW`zymUa^Kkd(Ot zi}b-#Jv5s;ElOEm;->kpoB>%3a%J&x%GAPjCFCFR*Qf-5Oakl+F^!!Ij*-j#!RCz9 zlG^#MCAJstZ|%Ac^VJzvpWGV&?;Rh+(c4drdMLRZhwIe*164WMzV;scnFF5b?|YSYT7u zydxD8bC|+9kV}D1$ud@eTwN?@!|llet(Tu9Ae_y|`$}1$C(TU0rve1zDi8WXcV2H4 z4i|J$eTXRK8j|IC;7dWS@V*Ru1+bH>FX`O@1vZs{UVMMcpr^$ag=cm;W_6y~Nhg#d z*!3~H)W zv(HFT+79<${UL1Lep(6}mn8rIAOJ~3K~!M8T$;nksbSpHd*s@WXW;j>Yd`TH0(hmj zqqhBtZwWm3&X2?s+lx;1QnXz+eIx?TC!c(>m+ig!?dPDX6zaN0J!)*8ESbQct^;?* z9?Q?ZT`x~5pLbg~HAYQ?(PWHu`>n^i{nleV8KJICj;&I04n%MZ(SlWJ(Zh?G9U`D} z!v-DnbM|v}1b7`PWv(mR(^G=cQ6@jpVK^bm+5$lfFFEAwF7NBnV3unXH1+xRTuy&c ze^uQxDNPcw(k#?+NozGk_Pqo>0%fJG1Uu%O0Z~C?6%8oF@FFd}ql=CKPm3ff&Dyg& zSJrAbs1U-Ie0!q|dTFRnG;9Hi!;0txAtfUIb}Ab90BLEPf}VJ<<9rrQ%WKc@5HUQE zD{1Fg+q=(5QC6pD+Ya|%^#L5c?KII)&S%uQEkq7{5a5@Vrpp+7E(3U*ww>yk-9ri3 zdCst-_A47+xf4r=fy!<(N z`IirsWxJ7F7!1ULIp*(i0W(@#|2EdibtybfHW`Xr8|VrDPCe17XiOS`otGmRJ(*oA zx9@F~ACwicnutJ?q*a+DE#=7L^D6&y36(ivv4VoKC$9>ss!>%9RMnW^ z8FI~Zg?K43+~;r|{zOM!8z=RCgd?`Sz6e^2SlhDf&b>*n%N$X}bvLq zbe(4CwliyXGvkQT&RQegu?{kw7E^S)U`rPCnw2p|D-$%6F{)7mR)h|0jwySLnTxm% z6RgANtk$nfGm1CHgy+Y)zw3X21wKxu3Mrs*u4GHfCQd}f$i&Zm z8xmexqFpK5`AgSz=(KVB&<^y(_sB4xVEMiX?2xUPWel&NtqcBRDtbXE0d|TM6dc$E zpfi^mwW>|xma1*imIJ#Ais~}>vGVW16#(UdvuZ}Do5ld2s&MROZ^8pRKIrcmpaQT7 z!81kREkQ2^MF)Dz`p}(y_Swtte=+dTbph;FS62%lnQv+=b&j>`c3o%U9F6T$=U4FL zK*?P?ZI1`Cd9+OhRo8x4q`7p=b%9LC4 zm>)xiYXbZ2-xRBEa)s zQxOX!PB#`POk_3u&?5Bm;C!=hqzvV#1eh{veJ!{|rB1xaX$#<3H-{)XOhGRo56zMp#+D9{cUTAI2*yXj~7l zCRC(C{9zi76Kya!Akxuw7pE9j#B>B|vXu4ZtP1_RsGNm#KPjz_$&V9JacSltOV?r# zk8+4g*Ut*)qq@_r2)sy^#Zeyh0(sC&C~}eb!UZ>sFr_7*`yuSFEZAuaeto+hecpNL zl8vw`;;=YAG>vtjX87^u4or$1SF=ISGOR8uZ)5ULoR)o-+H)vd;Z7tZ08uWb zvj8dkp;BTkoWeO@`R$T0nBYRS?UQ(u@P|caen066xl}&SF z!-qAw%?*Zf!$YgXBa(||8t7qS@v3QXB6c?FN|d&~^i4|BK1E{`8YENFhQQ9VKkC)y z^%3yW{i$2-B6}H1hX9$gNx6d-0SD4u)G*W_t0S|SyZ&Xb{Kqzh2?Tb)$3x3*6s?wJ zfi?vpVrd#1l@S8$+(jqs&VfX}0VaHTB#TRyowqpl(zgJbvFqCR;eXd# z+OH!ruF1doo4>iV?&`=Rk3`cn5qNIhx)nFwbQ1uRU;8o(cK6+Ur`eR)`sQ%dQ2N}N zlT5r#=@iz6o+)KEFdz5y2^>iZYj=K!zjIurfJ1RJrBr$~n&_k3C z=*^;M0245*n4;Qz==?#Y0f5_GO}8hs3&)X+F72HNkQ3LlQZ}7nG8b*4^IVLfk+&4t zP}1gCQK!zg14s-KEYL-zEs331=U}7E62AdG!4Sv*9b1bXwheS8w@sqw+iJ!fs4D_B z6x+8uN8YwY2gb46-;&H}Sp3=H73KS)%=q%PAIzTjC!c&08#iu@h6c}n{`2wZqmM?n z`_6a1GYFCozw<+0llRCYkKmw#4q6DTU$Q`V-1%=x#x8{ae%C#i&1Qa!WB2aec-4jH zL0qY#_PJPbmm; z)LV;LV!7WIfmiQ@ma;JJ604&ncEX~VMs1|t-hDM3B#4;-m-Tp&!)-J%?Fn*@$~kRW z9fP5uM49V28_8Z?Bv}zFW1L3brC>)vgXRLEZJ#OwyG|EDFEBfEz(?4W28PR!&%}R? zv#Bf<8sKpmO36sV105>+-T_|~K#^4_XF2h&V={~&0zFs8T82lioDC~<%DyK?+`e#S zu02VTB5*_28;`9w9b?XbLKlus5^R_#HiBUd(hRLzXw5is+vySL^_8!WKKdxmIp-XF z|NGzfW}Sx}c37{-SSf{H_=R7x5pp)hBpLot6~4wt55V` z=APlZcHZj0=T#S;<5{4wfvQoD$7t+8!X4bs?y4D&E$ETY-*u~cer1g~TEdimclq!j zFt&)HrDbgdYH=A0NZ~MJFeZ=+n-dF|9gqWErqf#lbcIjU8rqw5a^z1BYp_R!GO`4E z1sxht$SzAKrp6_LSofr2XNhwez#LUWEoK3ve9dz5GotYk> zMp=qdDq#WOw%cw)+qT9P*s`qjt!WypTel9g*$j_9`Y0ZM{BgYSg)dyXHwXZaJ@yz* zIRDQgD|TgdbjFR?JgBd}_P*Lp{~Q`M5V}t6VuuNi_wq8g z@4K!=>%S*_uh!a@5n6=uBpt1Y>6ZFSi~2_~ut%KE_H+!?Rq)rW#?E}M@)8seh!pqx zf-WxxQRTFHO2}3!amhZmYftWb6BVFqRhW}W2iV6C?(eYfA9Z8ge4FtI&B_YK>(*hs z-n?%n6R4)KrJll(+x|3vkr#At?(^UN_O}PiQmwUTcpv@fNAbJA`@5J-CfWMgBK@9= zP&Dgf4Vuvy&3KH_WQ=A!@(F6Hsz6HNWSr80> z&OOuvNsR?y2;_P{M#AEPrDo)ElKhM)}4G85Qhqi6e&SvP^)|>#mD4C(V&VSx`-5K|0X9_@4NGAK>#61j;V>LaMOm|D5 zS7LWMV|o!2q@mK5dj-IQ=qAHBmZ5gZplIl=cKs$Z)hSCpV(ABp>s!&o+}pd8dOW#M z?sP$Jg3Ccy<_s#CimtqoYpw)6nPaNs;Y1L4hEh!@D)%5jP8Xpl(TAm}lm%%_^l0>l zggc-|0qm3)5eJY{wrA)0l`|L+LyE9}>tqrZ`~)DfkWO$c9x%NjHqQf*d3+oqlDN3A z7tdPkWsWi@kSj3+A?jYg=O3PhFpdu@+zx~edrPSMVQ zS6^}-cD?sQc-fh!ThP;Hrxt<*JFxf04Wi#!PP)TaeWaZqV(mmau%q-P~=bS5XUHR(D+m3CS$sIaw z(Q=2@zVFzSqpfbE%UrjZck`%xHJ3T&?K~=N(=yg5)701zIhn}z zfSp2GMyh<}MmiKb?H9!+3qczOuN#YQ>-uUGdQyrMD(BvzDv{4%_$IjoEz^X9ZaZ+> znFV>Raqrc)h0T^u`}8sw+c;mFVz#!1+3Ff*t21=dnPG;k0VaI;+B^Tl4&};oJ6ss8Ow1bCWP)Zg!f0iT(YlGrrI?uV-A!JP zJM(n=xN|hA_GC^`RL17VH;Z-iIFka|bvcMY#MEej@sZT!MSHmM?N~%>7Q6TvQ{$%q zdOy#t%D7Yq>z_X0EJcy!zllo|NtFL~hVQ;d+tX$#i=7<#-)A{)#mo zJIv5xikmsVcs*jSu#^p37Ge^PxYBZGtGtYL{W}%)=OR{@$W=i3IvOXg(3}WgqWkSk zDWl{}^EWHMt>VVH%6-zbBdmqHo_8j+XJ#^J*|9Vr^2K8Y4qYALf3BZ`v{`qewgr^3 zLyO=f^6<5PUSwOP-rb_NcfIRf_?y4^8+`JUpTs9W`N@ThuJ7k>x$sPrgIm|AZ6C58 zHK?1$_Up`JsgxZe36Us~ahL&_ue+gyv3UxwF;Y~h14X($aUswk7K;s)u2jsUDYz0B z#mZWM7sovr41nKL=6Le|^l0M6ASfQLIy|=VS$-)vP5wq6Xzp?8b1aj}Vo6D$ktIJo z%B@UO3wIrCkK(d5-dW7GdGM=nOZq;?8pb3mV>mpIa=jhp!s2k9Vopc#_G#Z8UX)29 zV?-#?iIwiTazD|6oyl3S8y9VdZZ^Ywbq({iHFS2h%A zOUm5DoWNf1xD6$+D}!~I!kU#a^LZ^VZfb4sgEKbZWY?1i%5@a2j{4!#2T4m23dxc- z@H!LHtdbRJB)z|Z25$kmvy^wB;6vuzK*V)tRKRRg%jTG`&Wt_ObOvo19vC?8?Puak z*WHm-9c%yTK0)2^u@iC7XLeqWW@c zUa@Ldl$69pBLH5y7nj~nI-LlRq#P`Hu&M^I+#iXH-vJRX**7C#pA=Yn@hC(B z$ps5>rGT@nSIPZ7ycF~@wkKRnr0j*IG)9O2?M|z7dFc{|2-^jWTwnrVCQdQFU#DX^ z4(FCtrKgm8JDwP1UnOC&oVteVB!DiqK8=>C`+#k5FGoz;NO07`;#`YxKYj)Gy&nN<0pB>ZC+h_jiZ*lIq=i-n<4#Bs+ z^{vP;r>d&xXO@2M@mv24L}p0Qj7AtwCK#=opc%P99VJXs!>~bBh7C5O8l#mlCi|^m z-G=p8w{bnzZCGzWZ!$*J7;rQK?!stOmY?KG+B}Jjc?ghnGQ>dA&qK6(TkQ8G>uAw$ z;TW>Afd$3>M@;>^iL)|2mEM%hW7GQ){t1RB`Uc5T(@<&NTdo(_T^kWca*z z!V00+PQ#a4OX=tzXCJW#%=`gi@q*FPy1nS}k+V21NCbFLd>OCMrNakB6qT^0?^|Y4 zMa#5$sm%WJ5MT zVs~5sLY5XnEQ7I=Co`GursXFW=(;PHk$B+6zIZyCcyacm@xWxL40Nd^x!`>gu{o6v zCMflb8v-;QW^+AdZK@@V(%>^DNzuKJUUjyz`pnfgVeQ~&2Kf+~vUZm%Ym?vooD=^G zzWdp)_KLP7=;dJdrvL~KU%Obg^807J5_|USS&9)Jd;S?X_WU=&Z@j1=Fs{dD>RQ!} z$*Hj6Jdp>RV@nFww%zQ=s@hC;tENKLn4Ak$Td=E0Oll3&BMW|k_2se>F7)7dpmgOW zdU$0yNy^mvIai6Bm6aRGcQHrpt{8Ii+vR)^DAAc(vby3S;VpV7n<%q*Ec{@vP~LKq zUdDYx4MrirI*n?kOcz(O3(wOM%1o_1%uaMpZr_*0>e%4c7=xB%T`9AZv)AD4+FQg% zFE+v;iY2m2PL_eAv1~q_x1Ucv5+u`;R#YTjSBe;#DPAn>d3X+fQEvaAgV!?n{CiXObg^OagOz%v1upY!aKCnuwF6vvedYH)zO9 zyoblQ>~m;2x}MyBf*Ea>it-5fC`ue*0598i7QS@tdvNTwGmCTNUq5A&1-&DSFW;!V zT~dEq+Cch)H@_w(FVZn7`Pt)t`4Q&7@+@;wQe;-}DrHMhWp`=CPzR9|GR|dCiN9MC zHQ1ZwApGf&4lpQYIFz2=Z8j}0F}&bHIoeyvP|g5T>LD{M0$1Q7(3KSsq0jGSQu5rK zpaC(86KtVRvMwB zIOmDcm@_ZlfqK5sjfl(nXr@X-!wLy?)aPxzSV2TRqjoIw|xr)-wQ*Y8=dHqV~5xQ%4Ms zqI5B0D*I9;td0l!ngQstwKT?X8B!EK=)9ICk4vxc}Na3MIOE zd+r;K#ovA4?(!}sEwP)Ko>OiN815UFU zx-b_+q~eL?GWUy~dUqhR06h{5*-SwjwaiW*7Y@V%*b#}29nqrhT+bP|(4Wh+>1ppS z{yvKOqm*TtK&;z2&@-&3GNoRpL|F@AQy&h_W4E7$T|3?r_uIC;+5fG-^QR}`uW$Pl z0N_REoYL!>@n8?TJ7)Wv#4dXBINA>l04e+WF@D0(=JCJx#`j{&g>UhOoGuW%s?BPh z10|&di*@f+3{tKXr2<2z0(+p+*JW9YGINig=>+8LPPA`sFxwVS(!;Z_@lyZ0{Gu*y z&G+`!@>gZt0%B<#p1eefdcL3IlX21FUG*el#lsnj@)yKHSbXp}my(DaaMBO#J0WmS zKaeBf;}rO~;K#?Qh&k2N-fMH5=eXRWCawXn*=rL^y8#SB?)1`Dxj76#rUFNtij zrRFEOi$vmV$S#whlw3{=@Tio-O390w26S`O^Mk4P_S^$}><&1lXh6~bd&rwRaL2~- zXx^foYZG5NpZk6%g`4fPA_8tP~!7ad>s8gjwLNZ zlN{~74gsFC7}uRO5$M>iY)S`Y36Lc2R~W0gG!o^ z6ztqzb>EISO7Zw@Z}IuKenpiDU%dW3c=;t~d)DokT~*bn>l*c_!FXkYm33x4Y&@Qz zYO0LoAq6}k6fTR{kqy>}I2jzt7(`_It|2N>+u&>yrRct<5Jx!5h{ZIXu^~pSG#b)q z6#J;gO5|H8x2-{e*udYfzHsfhY|kb2Vmb|E_VCohe5oK>h)UWja)`=*ZI+8uXh0JD zLQ6zyW&Q&aR}>x|2=)$Klb zeqx&l*q}EFU>B|?h?IhW{Z$cABh}DWgV^`RCN?ObMn$3s=+mnL!Nl(B3&xnA@*b=vPV`|Lf|Y{ndemEs92(~~fvf#jVFr~^8r z&{61-Edb-~@2sb%uxOGEM0=wH{hC`BA&hc+%pv;7aGhdk+j6zSlIM8rB;Vz;MQx8m zD%*FMudZQr?_SL3bMy;mSl)Lo+T$=%CSF@w7s5~y`|K%7KoRk1Dd0(2N_+Jv0FHwl z0DN}$yK&r>|EcU0uekV~_>;3=uF4FdIT0xq)&f3~HGcY{F(wLkQqg44lM5>EDAco2 z5uV^uG=P?&bFen~5j8Q+BB+$kLW@mFtIq6FrjqHC^7w=*D^`}^CGxN#YhKICs7^VB zlm-?P6`%kBAOJ~3K~xSu0eG2PmfR?uH4fC5E@93V-boVpkc_#7|L9nUH>Wr%Pg#|g zN{2FO0-g<0Y1#R7}P0*ke|OY?#Zes5E`gIuldndhE`Ph9vWJmcIm;$mfc z>rKJk^W#25z~^_qCyUhf{}i@r4d~SKOp?_9^hIySpKdulb;&77bdez(u;&gObkIQn zCPCT0_{BY@qI2tY0-Htyo@g@2G`6U!`LvP3k;9clZh(eVlL4jda3a*FUgNY9g-hsC z50a^qWrFp_PBut|k*??wCwoX$z01~BDM^--3K&CRm;)duY$9FDtM)*nN)5ulm{6&I z>VA@b3irJ2eBEI(i%Qe`I^^vW0;-G%^*1eWHSb||U0DOMedk0_T^CQt+h?aA(3*ID( zo8@UhpWgj;9KY=qK7I9`9lqmifBY{n|Fj45d;c2#{cbZft#cb|+7@l&>UTjL)p);; z81=>bns{!)bxI&SU|1N`3V(clkN(`2BQpXEenIP1p(ST+&p8cHCVo<%UMomCaFIsXNV?ZrBE zmFX&EB&WAbR8bCQL7XY4ps$0QoU?^cJ(jXjIW1%8KxZtifw&ayWC^k{4_c-JwSnmH zefzG%V%}k~x`wrVt61H)iut~Mn6J&zE#?j|yk=cU4hX=5{`iS_?5W4&ir2prA35*h zat$ZoPtQFQpT6)d(K6F=A8U6Zw7zQRyDPx<-u*B}0G_h#MGowCzDK3yL(0Wu3_i8% z?Re%nXL!&`=R3dmk68Qrf5P;E{}s(|-3M(L{-4e;n@ydZd+TMP8Uc2c>bw%v%CiQ& zAO)7oe+8-w2HHubxKpV_Hwc1Uzn;w5$whYIEN(NGpvQxsE!!hGCl#}%gyleo5fwhx zU@GxLo?Am%XO?u~Zix6tA)WPpr0mUuEpz65$v(|3zrK}UE_zq6ZsMwrtEL$jBxEjW z$Qrn$I6z2iwTf68tCLLt<9>dkQfz=Zw?B%5Tu_X*ao$o9+Yd68N~T>kfaE--9br&~ z$hWsxbY9PHZGpb);5>H-&7{R-Izv01dddk$0ZeKPsCNac|MBXVfEoDcd9MQiY(D?h zk$n+9#bn#XR+3t9EIoO5hE>}hQ({o;dPwsdtPB`~W zHQr~4@Uy@DJKXc|!$9x=*fvh&b2`IpWs2#B70kR2UfZ-$+1S80Wtc0q2+`Iesxt@R z(|j`WC`tR#67uM2UlvoW%-(vH3(i{vyI_;ah{tUdb|*s51mn$`0w@~j8eGoS%5!g~ zN_|ejPeQo7UWcqh%X#gN%E*p156@I+(=p;w;Zoi|jI|LZkr5SA9w<#1>LMM%u^Bn;%@i>hD9IfB@ zsiZ2JT&%?r*gB7i#p)cZ`}SdN?;QPN;ep3RHP*TV!ZZ^w{8YuHMZ4&|Nl!9_dd>xZ z24=>k=f57$I{#JG&-d9Kj&S_c?JpV7PfVcq`JL~>Q?{pu#Z^1sH<-%SzzzWJy6Y|+ zbkITS`JdeJR?oHbTGB+g^()_iF$VwjsNYW=+NMD}nR=t170f)?O=eRxZ5w%dM9{^k zLL)6`44OC_>h`g+S)h&%!Nk?{Fizqgq>ogp0v}RfL0KwD^=Q$0;Om{}3wjn}2N^X? zX(j;3kwTSq@OH7xu& zOjt~3Q_NP9q5BYCJ_^&p{BOP zB^*kH59Z;-ss@Y(rK}T?_b(OEp5Em7dX^8Slm|1f2O$|1g>(HoSWR9YDz$M=!m(cJ zsGiz}tzPFPrznuA+~6gVlgjVMib`A5M8>1VGCr3wK-OCH-NF%w7aeTh$l-$ib$G7V`*L2`aTH6O5C!YUGTz>u=M-R@ccKvzv{@ZT54fnj~ zJx8OuaB7+cv)K%L_wGgC_v*1b?zjW@y4Stp?Sl?FsE~#_^hu9b68VN|Vx#`>v>{vQ zLH?}_QB1dbSZ6+N6HSNx>ltIvP8zR}>=ltI+ck4X~vg22ANtF!f=f`KQw8f3N%c@BN~wJ9kjarT}HDWtOUj8rDz>OGLQorkn87pZ;{|-QeE$zBm5;-~SzZ z_wH35v-9~}fgJ$+!7WnDt}?@<@-W71#f&C>=tigF`dZ=K2-6FC6>^FRMH?sK2}EcIH?de*b>gCG0=H{X0Snx;{Y&1SRW zeINSJhmH`HwQnhVQGvee0Ib1!y3S&4j&|y(DU->>rS@>?f4^9`=Xvj4wk`}brom)3 zi4kej7@!r6ElPsBh7$MD5_{lt>7?Vkk#Huf#{jt$1ZOpRnM>7D+C)Qf(Oa(~J8#e~ zWF|-+*_6r0k&g0em7gk!x&nSB;W)3KQ??&cNNMX^su0J`-vp%?X!?87F|15?iFB05 zsfw49LSLug68LHhN0odlA~!3$yqwGC$j)w-j$X9-tN>P$2KU*+Sd?jkw3G1qg+<@FPHxz_oc*Qy93S5P`sE41M2W^Q z(2WIvU;N@1OTXs*?sq?Y>|-B8+qR?6JM_>)@w>nKySVA5o7DSm+O!Fudi^_#G&h`H zxNEzMxVnWKf%Bke)|{+V+ctj8&^Lg3>r9RMUJ$DR(=-4Wlj#K3_Hj`Tsw-tE9QR8Xg3`0DADC7yelTi^-)Q23Tu?xF43v)r{8(x`teBS$@L(5sRXFx0ZAvy=jw@ zFZv$Od+k~Hi?iQ^(=IyGr}#GRcpu!Fnh_`OcvZF^xR3g*M?3+?9CHjNlgWOA-ZP*1 zOk8)}b$Hs-o`%nU_OpZE@Zk@CI4-{U;-!G`zylA&>gp;U_`nCM`=9)>(_-flfD#EM z0Po$lGd}mluRRBB31oVhp3rp$i`ay4^*!s&j-~m`kH7p49D2+VZg$wRBGN+@|Akr+ zxfIs=h%bO%n60iWl#DbmMp9Z5`C*>Yroe_$dK*l`Ef=W9;HXzBBR&j{l?^MX6l*|1 zKcPbDwsgaF*G1EU*Xs;a(_UG!32!Pnag;M<8De_Z#153>@=?~-;3PfA5^O1}FlO@Z zIlx(Q7ExXyAsW$49*mT_%T%#`3$O3Hwc0n+N2(+i0a}L>C*oXI@fnLqnwF%uplAb? zT80g*m-uK2$ zH{CS)4G%l)Fud(;Z^Ppr_qcVp#<#!y?c%*ox$rdugXyXE`oi7Y;*t92XYYuA`;yn4 z3+gSbfmXTkJJdT~eE~LY+Js;K^2yr5BZ^gWDC6Pdb&ppxv0*-T~aU< zVOHkV^<^;Q-ife5fnKQLiP8iMW{u71HkP9Bt1CD9+B302I0bqcxH~K%)pVhp8A39W zqpTB4*?yjcF>3A6mImaYC|`WI9N=WZk(JF18QySKI?Zc(9O`%^C7;~i84(pyC9WxH zokBrb0$nA-#V6?5hIZ@@?3BEZ{kzkRS~C!157 zHf_S5J$qC%_P_@|5M9@eg4na&wq}M8#m%x-})AgJo3mvfF+{Xr9EZG zYbp_zrx*DMm_*x;0KE93?ZMF*@BXt3M?c%k-f&*>!DtMcN#oR3CM}wVbm>4HiJ5L* zpo5SDJ40^TAs}7hSVlZh&m-|$Q3@)kB9SKQv9iieEbBE630Z($BeL;uHbObZj4bPV6oHS>cy9#uol)y6 z1|+B&GD49&OVumM6jH2B=Ty(r9aboH6(r(uEPk@z*~89}&FBX83=oX5UR-5hU5g^3 zVFj%X%Vcc^9!_=FkMiLYfo0}iN;G~k!j-V_SVMc1^Z#k zkDRM{cLyJQ@Mt|RfZ=Vo-G-~Lz8X({@{<8zDJ#X_`mNu>cfb2xTz&P`IPSRPaOBl+7t=3xj_HFAQE8<#oPo464N(mdUi!xK zp_3^c=>=a4XYJTxGM!*$!wRN~u&mj25(n}fLV}&E`O#Ry>_C11ff&q$#&7~XCn&9?N}{Vw@RXM>+AHcgs|^qvS|%!rKeM~HKhj*8%Bj$0UN z(2%=~)64$7(uf<7Tcj5{cr|T2XaOqucPO`-md99vA<&L6wsd61dE~*Vd&@&(PLw?-k<~&w8!>e?Vr8Ki-=!!|n+z5# zYf0g-B6StfT`5MY?+3KvvF0*~I-ca@kx!e+f(^l13u9csXTp`RH&badB&jYTEG}xG zdB=(K`HS%gj|+NARyAaXwT?c~cS*RD*~U=AGzQJI#cbmY?Q{myv|#I^LJ+Eh`9plW zvD2PCdvM@^2Vy>-V>+FVKKGB$bzZYqUiiKulH9Xr52n*;K~Wi_!~DkKzpKo9!h!cQ z0vb3WfTl5So_5kku+j?E6wSCnK83uj?L%H!&jCSGJ<^phN@qXKAp`#F7*a- z9^OFjuh^cf=V)uSwNfq#qDKU1N-Y$qnINqCMv49uC7jJb8!G&&h-=(hOgy8P+691V{s0uTr)29 z>8l^3EC4TlM%I_)j*Es;O+E}B7~^l7Ajjq1+Cu&zraee`j^v7wlm#PM44XASnWcgZ zC;6elt~owjLWU($Tf}Q+f8OOOs7+rw$ag`?DR%H zHzl77Z{w&8x+T~VSDH=GUD)WP=Od&-0d!>eJS&jp;Kv#Bo(Dir1D;<_Nw}N^(DRWU z!$o@55E@^{GhS6jI=xBi32~O@a@mnBniX6m@S;40GNqNkr>0pm<87`sy{Rqe#8$pqb^gRwnM-u+5kvh|HcQ)2A&rZ>F_0C3;?-WSIofBb%f z9RYa8`7id;P7VI@ymzb%x)JOqEvB;>rWJT9-wJ5T165R}La)Gh8XVKu@j2!rtM_HZi6IMO3q;a%s`J?@;30B4nSDBI z1>kvrV|bu51Y>>XU`U&|5otAhaXZpdGSEvj6b*WLVQ83^&Qzf-qHeiZGrkzH7T0w* z5}ou4=&?>~b7m3cpMy*444{E6JiwWNA#=&ViK~r_rj-VskjNGUWeW>qO&B@Ml?X0g;{(EqyoS3Cn;yN*B-CD0^BK6^rN-eT!0jd+y%i5CwKFhw= z9;iw-jeQ0$Tc5sKk!8hUDst|0Y~40&yab!E3G@gAV}-S+A{m#r1Nm8REqR%fj&*UT z9SRL}8cN7Dbc>Z_Yyla}ykuh;xfXI#u3)HI-}15oq&YjpnUZqptR@}VC~|_tjzHS5 zn6d^&*f9X(B$dded6ASJUM?Ikl;nXTb3LZ>I*mvpH8olajZluubU1jEM4E*^@U0(< z>N~6C+Sz+hbc+S%t7{k~88YX1ED>P)XI;07d`Isomf@@w80{A(X zvLOCzvuTZ(#e`@sss+1xTI0EdhTUU+De+St0_ZYHDV^fh$;Ji9Rb=qZ;I=556(kTA z$WwV08FH!p5)HjGlkc)5snglaxwW8N)O>VFnOM`H8TWe;`Bt+`7QO+w<2kvGax*cQ%qFfNKbxRwT~u@G zYtO(3&wewk?a?hd%vV>j@2-8=d*@wPy=xy9YinLnC9BC0;lv%USZZlnTU%QS?849Y znG4>7&s^{x-BtB^eUsS)Q?FU)tP_n`ToO1MW1?hxJDtF^t#4dVwBR&G7;JkG4BTrH zo`bpVAhP62^ZmvBr02C0r3A%6Nj~+J_KeBCPzDaH(pY`FQfuOh?s9b`(zz(;}?CdbcmL*$&zL`5}k+{jBt5hQfXh*Hs+WJIOHUsUz)a~b)P9xQD3is_3I z{DN-=vsIuMfUozU*9(6Q?VrII@J4~Md8@))jo&4s0-tUf12bv7bbAYJ zUZiv46^S=EMdIlcP3t(4MWe?97Kt>|LYrMj0Lg6^Rh^C1go6SXf=(?1I0+{+4NgFA zI8ah+rN1j@kz&ccLordxm9~YrExLQ0iDjTBjVAXn%02xB$3P>$1g?oz;S-!jyG0c{sJLrpndULj~k0)+`Eh+#;>$2vW6Iuj?Md}tx0=TE61YJU7k`F73(K;~Dvy*ct(?@A@CO=~48yrDYGWeVr zwm9L!(-j@4sO6o%exu>j=fAt!g>{RXr|V)IA>r$(`$(Gj8N1l^AZ79wtI0HXhwq}_Kg8f4T3mf zAS1LQ4Hb)IY&Zx`f9@MxZDOdZ2g;d3Drz=4*N+Do?j}1Y579soAVYOKbkr1&b@*RI zdGbE#mn-UjG7YviiEUaA(OpVw9jNpwD%2W&H9tD8#-L>c=*P(*BFx1C^SLwcSuE!0 z`VM`NFM^iEVawR?Py1YRqwBC(EYN%9<7mPiI&n?t{f;Zre`TUHKTl1f|a_Lfz=RLmS_xsYzx zlOTOYBBD|9Q(#92F_an;Tu~KHdih$}ld>IxB1ccMv!yEu3$9}7RkScLf+{m03d*@~wZcUfsftmd#h!uvqJobi0>%x13W`8GrY8e~15e z$RqHPZLTSC^6oPrr3u1VozvEHvPK;`a1Za>kWQa}{Rc-0#x)M#Pk!=~^8Vh!E;=kQ zhiy6#1Gej31IBv_erVepaLTS%yTt}QoGyXm7&2Q>LtcK}fQ|9Oo00-XqOpKVw8v$? ztk_$*!hjfy`ZNfUFF_EeOCl6pQR3lfjz&M1j3x$67IjHoOi!r%-v<*>CX}*xHZ&tL zH!~8Wx>6r6lo`_8EsIfqDz|Tz`4|$@A+^#3p{(UmR?`dKvIH#?m1=lnsn_z|xat(g zK)22aS9l|f&w=UQ__W2}pK5$#p)sR$j~BZlS1crBL?xN>FmSW>Kuzj=ZMB^aq{k$?`Etrd*WgC>>F2P7M?-f3nu%e)qdyynVtM|I-El z03ZNKL_t&qFOOPoeP_|l=UA-Hu~_pnzI~S>UK*obJ0QIWfIx{gm0Ccs5hr;9lL@h6M zMPf2Ys!50yJopfhc4@clBTYYg3)6dI6S>|~ zkeKl5vd%_8sIf>21eaSq2rs}3CrITOyRea4H(y)BVr>E2*-~U7+bO>Hz3)|ju8(Yc zD^A+=3b%DEMQL-X*eHp*m!I>N{Q^5?#&^H_-GY`EYCdeOmrb&&W_igrM{GW2*DKYy z5W|1Zo?v?y(YkcD*0rmcmQsx-IHtKbJByMFA_^m$C1Two=Q@A0B_05#N(3t#6@-e#b4^5m zD}?2Bux&JdjestxBO)dYoi$1d6h=tLo5&RxsV>7$@WPWVg?zRYSPZgJl!ILkfKu~@ z^)z%8@(jExPfQMl3S&L65ny{oAY!#4i<#hl#GFAjp*}z$EUbaSGDKh~R z_uI?1r>OJfoo4}HT(bSmIC5Do|C5zIe;fsiTc)#j}S(liV@KQ#{$e3f0(WvIbLA7J2pxsp|?#sIIwInHW&#&}^--?cR{)gwic zv~D`vPfkE-{e>usD^vuAR!1%=Af&zE<46z#>zIbFcrve%J19yY05Zfh?$tUwY14)%}-jf0GBkGqb%<59;mNv&Suh zB+277I!CTqSBo$*U?Ysy=u-KqHXl%~^prKskVSl84PZ=~>cYsPy4<;nv-`7Yjo5fK zY4(~Z=>(RDRg+=nOt{QOg_)dFSOdH|yi{OOTA9`4q3Ve>Rh3W1E=N5lmWk@Pf_hwg z3Xi-viA9-X2iN#NvWu;v&9*4k*hlRm9SYKvtheS zM@w&R?SvHBN2OsVBkWsIlh)4w_?_HZEV>SBt7}-jYt^N#y%#F=K+-L2v|8M=XAka= z-_UUiF;3d`Qha1vNgcWN+G}y_vB%TV2al(p1a5koQpk%gVnbG$> z7K;TIo_bnrp5Zjp?;$EaOY>xEy!faYrHRJ{H~G zdh4xt(1RYN?$5z))22<>w{PDP0P23*4{muQPJ8XED>b(ixfg20pt{SmTq0;ZZ6!rY z9!_<2sia=mSBwF&o(*6XUsOGZ#~K1w)$nr43se@Xr)ng%s|@y7_c@Rh&rR%va|A-j zX%em@9cAj6DAO=Y$yC4!mZ zsA59Ki;;S=DKo`3KUeD2jFOTP$Sy`SDpcxCSsKI08V=Ui<7%KMlwJbZDG^in_*A9? z2E-OLL+ip{-bq(Y``L@pGL zN`ZYSiK zXb8qd6=fJ?fNlt0B{_gxQbG>%xhcxiS|@s&uqI99wL8wnVM0MR%;;YCi&vF>1{OaD z>-*DXDc^3zL8#>3IW?Jax9= z(k5B>-op0cx|n;_B^}=e;m{6A^6v&x4nAC$LI@_dPAIlhgh&DeWrK{T*MynQBq1Lq z<&E*u@{N~|H|qV_Pj~lSkF~vX?7e+2_T8};^L?&fcgd^YkLO(cJk1x#Qi6|ec~1dm z0Z{U0#hAw3WCUIpB?)%c0^EaKQ5W#?P>RrOAZCk`G7V+4E;Y*@fR0=w*M!Ru z*#-Icf?6ZU(JbrstQKdkHY=ojjs7Lt!3B&(t)mvzRlt{okmmZux;&8MvTVnd`U>+p zdGugGu%IzelxPYo7W*jBb$g=|UdsF`h0L^C+CXfK5t}ygN#j3G?31L)PoPpT#mCb4 z4TRg7^b8fG7!rbw#31jQ65>x7b}O3-qkI#28FtM`}oE96y4dJd>ZG$vJ*pL`Wii^_M~WE=GB(DOWaR zqs$f6X7O2oT>`eor>l*R=o$fZCW0Nb(5OH*W)_yMtXvbTfG4?T5b8Fy{E!sqgSGZK zSJGJK^?xr&OGGYaSQ)iwazLami~ePlS3(^RHg$A|iB#t)-dD7Ugccs8%)5v>ArvOk zqv?O4gkrtUn5{g|LO!SW|MxE14gXl2v)jRWj16EW2F-*(jRU=BzwR_#diDok$*S*= ztQu>@e^P0yT$J)OQa*Os_O}7V_}SOKkNJF#0}niK6d#{;{>x#E!QqD=j`@6!2S511 zDQY|UWB|bL{od~>up^;}5#JU+u1h&>Vt{bY<3!_n`pFTP5+GUeJb7sqz`5u3 z;<3H}Jb9md-6-Z6?(@J#u==rZe|%i}SU?euNu;@Y;vh9)Oh{G9ft)k&$$br&bD)c>*yg#yKVI<*2~ z%F}`iO5<3Jm+O_LA1_6B(QkmObNN1EfvB0JcB*9qBceOk{WU&Vwz09`#0-Lfr1dM3 zN!2pSR!mBmC$`Kf?X)cR$>4!wrM)v3bWC0Gv(ZjW^y{q|MKG#xwBHhdvZ{+;PXC z!pcdz&Zr9EQ(X8NKm6R^;^2c1#(kdnyU}~Xl?jlE29$db>oQe!4x920-!c>jR7SjF zq$sJxu(qp z5Aw0Hl90@!kzc)Yi@J)9zh?`WnL%q?^ss;d z`h|(Yq~H+!Gi;w^2R83|0SJUkx4n0y1ByY=3)GY8bh?zeHUM0C@m~M{Zn@2?yjw`OX0$a9h!NnI}T>YEF4m%70qM$+G_yvF3W6#*^wpB3GF;1F(GHIO?*@hXW zD-%p6Excx^q;Zd=J7tuI0ZIyGa!<$(bgBx3=Cij!g*f=q3V&J(b-a{-6ST%%QaG-3 zXknrB^+ME^rLQFP1eI=J06mh8iV)q|SicMVIMv42SVwhI=|-DzRm{d1h0&FK?RJa} zT#kf|nDkU?a#gUaMthljQa%LFxik$*{|O!;6BcF4tW_`kKII%P#5^u5tF{UShBM}M z{P8@P)nvQXL6T@H6lg7!Mq|S8xl&sptBb{PjL7(%&v%8{Q&7oAc^g_x4ZG_LC!^%R zj?s6Fe$99L!u`bf+PkjC{+u50fCu2d_q{K6?AU>$jyei|^;ds|>2!*p{`9BoASGft z8!m|mpZUyZ&~+Vl?%X*Dc=>$qNxNQA))$33a@#hTOj}H6Q%qN8-qT{@Wa`N%jWeof zobdn;Z=95>4{*v-NG&C0vnVe`a7?08%l>wl4JKABi2R!5+eJ$P$-z@f%4MxCuW9Gw zbs@rQM1w*$D-3C4?<(~~*{SwPkRWzPbL^C3p$3OltZoTzZyy!(Q!T|>yfa!FKJMOq+v@^ST=PRn$16WyW(KJm_BYewS-h$1W zH?QN2maHgEW6(|-F9kotY~u_o8)ulTOgt5ZRCGrLL3k|HtOJxRx_dp}cy@*Q4u(1y zTDUXv^ITmv@!X-IE?rWO2ED(pM?<}D4)`WPeBVGjvC*i9DqxrCcxK0eo)o7ir zHK9QP8*dxfiqZ>Wy*xAoD$O07QW$eBTeo2`zKHQi4Nr4{L~TyUM<27C%fm~Pa7i_hDJ^F)HxK);1qq+<9;z-One$L-w0nlP z|KVVdB2;U`U->B$GL4Y!2kgS7!QH$^-w@Ed_dDT)6Y%kme;mL3y($uu_Ec}1^qlu0t7O$ zlMBBxiH%Zfxv)S0zSN`$scliS%bEpTfL$%Z!@#(y(8&vgeA2d6P$aR@YAlbn?dtf) zTrb&GI<DPgBtRVV@{cv+CU_AJITF& zc6Yq-jc>#WC!8<}cDnYe`E=IWbxFo2?mT@7{^WS=@Ve`+Qz`YeZ@nD3_lWh{f_>ki zUv%gf3-sLromXqNeIG>*nCze-pscx3re;hes3#>8pKrs=( z7o9688&k^zpK2uM$0@D$nO2!!g(`-k6y`zjx;?KNk-w`%P!(cuIa$Hwq=hK*2LQ7o z{c{r(VWPr`VRqd6iHr0cEB-D{RY1?&$_b2o%A%L)vY7IKc)oHL^V1i$cT!3=xRHfn ze$fd{YRv}Pox`N*36P$|2$v>ISC~kVAd_x~$&2_J!ETVlJTS$fTLJ;r#2}M9Cv+KV zaxiey8jb5t8q>PyuVXCsby)r7D%S3tqg(57`K#YQI7u-SP|dmooM;?TjgI=+a8pc@hyGMG1HHSpE&!PcK+b!?t zI7uin#v4#ifY}V~gkVggs0UQ>HXY;GN#Ob64}XY%{KtR9Z~yjh<6r;vUzY;ATB~z^ zwU9%a8^8_#jywBQ5HUWo)dAF@jG#BXeg^g#1iQSBd~2 z&2gs)FEu6%1n?J37Rk_H&B2wC9hY=79ss(~tcVj$6&=2|<`+@VYPr^L(2a|d2UsSU z480nO=qoiUte#kqw3Li*SDJW0c(@Lu_&QR0$%;bM_Atn>(ZZWt)pagwL6pmf$DJ(Q z)@g4tF1PH3N_&Rw*r}iNh+Y-M1hyXV);e@+9r}fJ+H}UCowVSdlljs_M5c}RqA(UG zUi^Gq{+jnMomK+CWHQ0==RY5pZF@HkI_RMFk&1@-9nao%IxgMzwn4znNzW4O!Xm{0 zPTKu)fC!gtNfG0zuRGmQGpq#%rDIq$4PnwYu#KBt4vS8WLXlHS5;$4ESOzfb1=yv? z&QM>C@F=RaR7SljMfqGTG#(40JYuBAY}lJjiu}UppYrXHe$I;Oq4{$f=s3;BLUy?; z&IvnXm>7)mvIz~6+GQkacT$STQ(S^JpM~>q zSnbfQIyGarqfoN7v~Yr-^B&fxv_#~j%>9RSf&|8k6S#uDnY3s+M^)H-@e9BfxO~g| z*2S*PyIvH(`0{GTLo*S}f!-jeDmy-ifahL(dOQF>_-F1lPka68K<5sWrXh5*39Rh_ zZn82r*QX(hri3lel)9ram8Cq&6Msu%oV=2=r;6k>EbbnKvg4u{vKsQox+wIk#zih< z%t9C-P)%%_oS@7>O^oGJ)>Kq(epwx_944R&pSM(BPX<7&(%QBbijB|abCn1^%)is2 zYqDLF6pmEOQzkXa<}e%WVN@qC)I4p~zHmJ~<}CIfeNO2Nru3dUn{3I3z&VbiB`2z6 z)t!RLxBsV)ZhJeP zvss>-rCVdDSk;_rCP&6)z~Ecq%XkEfm>e( zMbVw50rGK9jUfTL^7y0y;3J{kYyen4RmAKKvm*mRPgLa6>}rGzX+$gmUv3vUK+=G$ z*J~}i&ZyK zQZ0n=<9q!CcU}8S9DMArp<58l+5(Ga4NcRaAy=qxCM{?s7+W{!%mN*`?&h;!_hP4D z98wNp|0q}Apk^`-b}wC;mh$XfFUO_Z-d>IJ^7&r%Iux2TzELr0{AYIhfMCWkXZ?xL zcS9`9JI()f%&aLF1_%v@UUBUt_!UF{v_9AkkDZl3ES0uHEsLcMV!(W+80lG`6)=$- zi$tN0u2hJj>T~QlcC7BJ0bQPAR(ZFanYHuSUQ)6_wBHV?fTJy5it9gbK0d}+r)$D_ zN-Vmju!)>2fKGv4HgTciq)doU*Y2{$NzV0Kr)IZ5!39^-vLq2&2fa*zmUMKdpQpbw zvG>}rF1oV|_SA~r8T9l$KL2Vr5pnG9CndmR>EurM{`bF+-#_|DP~W4QcUUwFG?O`+ zw)Ndx12bzurhze?AFA(QyWV%J4Q$tYg1)aca`Jt+eA|1L-hR2C)nye4`OUju4uG+F z_sekE)^{wK+Pb6qEARXa{^0aKLDL$vvni&tHu`J0@8cZG8Y1_cspI51(91!kl1fU| z6LRydsZQ5v;3HkXqnyH0E52}EpVQ!1i#!WIRk8ocrO8Sz0Z}x;&g^tD27;WX5Rsi2 zcPvtVpwx{PlN1adCKDqYV5;OxG*BQ3SRi}0M)O}P*(1x)lNIVJwJ6TqzeVFfdarT; zcGgo)(*06I6h&pOq|KlpRU%Vc$wi$8^e(GCZe+vK!x5i;$8rWkkw(2?O!XgCd}i4R zlXi;^i?z=AuCA_Ov2THH?!bsG+Ytc;IkUdeFMp&|7P-!0VAlL!GWn14a z!0%GlOwi66 zG%cm^!uVNrR2DdP8~hKKeTB1^z*7$RQ>K51oUE)m z!<=e`L4Jw+)J2eJG8T;yl1=K&x#AadcbxT{c4YH)xs`%AK_W_a|_#f@les-x_0Ze0Dxn5J}KrW=`#SI-~L%V z?V@L(olG!WnPKIC70gyPplv5GXkwp)dw4Mc7hU#?9`k)`SgZvfSB?&8AKNzG*a(e` zXYKl*@xG63d)HF%D8Vj7dbQUNr>Ac}#dV~o4JIp7tZZDtbi)iy>kOORXLo6>0mB%B z$;2ri&!!W!ZG*m@f<9`z8FV1(;wYC<(H#q(9V@lpTyDN3Izxf*uqXkl zBMFM#;C&!0SI#ie&5vb-M@gQ&@mxE}xs%fsHGf$x+LF4lNnM-;!C0(eyk2FAivfaK z&95Dtlvt-zn^RDdTss7@I42l$CAu4ESmfL~C7{4cAsHFVPD#)g(HBN#{02E@K|U(8 z6USihmW0)?sKbf$gn}(#tlMRi6|&)D*-cBVEwI?zpFQqYL$TcP zrR*FA1OjUP1X`wp&lyD}dunTK87oSlvjZJh4BRhMSbg1+=r#!e03ZNKL_t)>(Jre} zS3xqajESKJ^jyEcoT|%(I|rsT<*19C;rGIz$VCLGpsklw7EWDns_>fRnMJKcQBefL zDAe(6?@}?_Tb%irSL4msy#Y)N(-4}eYd8eDN^cxMqO%$3Wo`=WM$bC#!DjA^f&9*I zyg^ZKdZ(0Zj3+zWXy#1}T)y>v>!z_f8vOHkYwzB@*sx&(tWB=t#+YJH{=3WFtZdhH zIN}9Qs`n{^fs7$Xp7uw0-}mTOU4z2S82hOTFNtKxsi@d)0V3YV^S4|cLUTh!J#8%rxetO|0()uuqf52S%~SY&&WUh2G9lCN_B54qE4qA!;sCfqSB2O z<7C<^V1U8Aj?XOy#!q;EIbE?QA9!4Hm|6-8h$eVQdw8S?GIHk37eEJ#V*Z$|r4Hl*Y!01G?iG4atq=&36EMNmQ0bn-x)RvC`08V)AbHQX_ zjg2$AVa5>*eD0!8jBd^{js0^K)vc|qVX;_XGMNlcb6mFNo!ETQONRg(LFB#{_W$eO zL5JNV{w4wFthK+aAbFJT~%c>aXr`6R}bX^B)ZH(x~N7u@M`5#^RE&S^j z{~q*^yK7#T|Ga(IV=E?>pRlGyKy0IOiW#ZZ z)l{3;ueGr*Gu7xG)k0gP1W>~b=IrrwyX|^AK(ScY#y7J;$UC8x?kEd+B$dXF$rslQ zx$~yQFv#&zVF|foXbJ16a%WqaDZKi|1U8qc41t|NW@w&(|X2Pi*-xo_W!8J!v&D z@;|ZtgG;HXUJm_Q(5JjDr9;Rmq0#FRRq=e(S#!R4A#VD3Grn0N4MU|Y~EvGIwwue zKr?X-404Xg1>aB)8tEp`m0LeCSZpSNBUkVKq^i5^&nZN7!Ed748Lx5jw!8}uc)$a2 z_zRBBW;08f?=%|5xMD|&SpWE(XSwLexa~>Cz%)QJAv6=WP_XeO7sW~s=18Hto&Ya( zV^gCda^^@zW~fw>C4g}e360TA>aJ+xJ2_>Ao;b$HDdAwq5iz5?a+FP75QeJb1IM8D zoK*t@l>$9wBBXupy^HQp^n?Q;nr+l&7?qs5W_<8YV;p?`Vvp zc0U$h*?JwGu=8;)lUnn;p*Q0JxsEMctEN@{aK|zD;yKsgk1l*tj7GUgO$H$k1mF`} zKD1w3bmPX2xYxbzg&+U;$D=^E%<0iDJf)*|9m~#>N_u zVb|Db$TcY@29s%nW~BkO02`OK69_gGlA$$Y~C&9yj#lEu$u?UX*~3t>KC3yG&I%G#Cg#LymsZ8ra5yEaRwMkHJ^A1<>o=*0qejvoK`P zcNV>$o^U%D;Y;USs{rhn9Z$v8+y4qr-g(^UQo*(E+Y+Zk4mkvO-g##PyZt#GbIdVV zTU*0dzVelL-*+$hD!Q(V9os-n36}tH%&Y!Hbl2Su(s*tjH3ZYZr~Gh&2@`^u5SocW zJ25b=)4+3bgTc@`@g}wLb?U}O8VYGMZLZA2as~W2lMSPa)uJ?n(kX5ZbW{lcRMTEk z{!~ik#Bef<3Ql`k@`;PN-U`fCGOoOorEOSiqh!OSuJSeyMR+nLsf|f1cT53tL8LNt z7a3Z6t3HiUGJa_tvLFql>Jdu(G+=Gov@<@uLu^M*);wI zWr}T4dQaqIR;YGTlg|Xy^R+Am52owe z2E-7U^E!3Fl9Q2RM{7mXf(IfziQ^E8=1i%+#eRT1um54#L#&{%{i;c+8q!x_NlJ_} zUvacpN^NdJYvHA|)`$KqEgkhP6=6?xc}b{pWOnXx2tPokg&g8#auG>0Gho}O`9a$O zN9}wpzOwymc*4%dyRt%WV?8f;&VKovFBjYH3+G&oqjx-cDf$ir_|%q5_Cx2+MRyxF zZp0Ix_(bg4vj?}`b{lTK`R1iSt_F6`e%(v_R7nsi4U-_OelPO8x~BEdYn;|y6QVco z-bMz1v2dHp#*~+Xv^(1@s?%p8U%QiYu;k}9D>I)|0NfDgZs7WWU8H81P)QG^`f(#; zOe#2bnL##NZ%L3hC~A$D`cOYczc1I*)mkLC1V@9pOn}>>fiSqaIg9dq^kD$Dl&l2h zPe|iHw=Bd_UE@>mg;Vt`q?MFk7Y^D%IfM%-_Ux08j6C(hgwVX;z|*U(q#_D2BGOYa zAVu%j(p`-Obh^JaEumRy5_yju4Z^Yjy&aGDJAG1JvtM9$ z=%I(=;DZmw!yo=|Jmet{!4sbF1ia)WFTwTKUytjrzaDqH+ufGvmTRyx#=x`<+G*>? z#hS*0p75R!#N%e-WT9Z-Q(1O$?Ivgs+NkiG=03G#7bTV{kV&&c)a;0cqr0KzgZ`Z} zP-7m*x5gqT_|%&!vW_$oFtEyPiIXad^T9PKkRU1Zz8)ooM;eb(otaT1(Z>2>7EbI> z2U7A`TQx+#%sLZgqy10l0}(bE9(nRE4{T?p%T+0&t{2f!MS>}7Hx$=XT_e}Zs#_+e z2AzB*7O8EbslYJU%C0AsDQ&DNQ4xSU9lOj-MW)4p@@ZTrQLZLV|G#%ug*zXIuWbJs zj@t2f74_DBggj-vX8UJHxBId{7b3fdJmewRyLT_1{`9Bg9{0Ehm>F+>``huYZ++`m z1G~n7-DJfXXHQn9-h_xE=t%404E5^r0iCs{F zodwXTX^Hhn$@RvKOm89f-XY+N>9SzYD?eV=GVO+-Z769yFdE|3VR#$y`cd3c+%$1& z3y*;PV8y8C+OWjR!h5;C@DeHQkiHd3C}QnHnQ5I?x=F%)CmZO8h!2qgV+!mXW;k9?$?!R>q8e*5is^rIiWG|)M)dohf0 zoWjX$irI!4rW;ordu& z*ddhAQ-T~amVkvXEimx- z3m*qC;j8C$J-nZs)U| zs-GRBC!PAmO;5;pJb8LKR8Px6QKqU;?}IFoW0nzK?rFqXnm5*1p$JkGYLtqmk|rj~ zUqYm^VfXIg=VAR$*faXM#bRHFZnZ-{XRpzgJ|8s>@a(L`!~#5X)D*CGglC~=8@}n| zFL+!^iD~_`_CI<$=%9n}o5vpp0QlH`a`j5Edr_*zwGG-ygLdltbd3=XZ-MS3b6VC` zPcl8VvC|rr7Lf~8(m<4_?x#vrNEYKWN?95^8r`iQ+0~l^Ec;+tWJeX3FDkl!M7a`> zjPKla6D=HvHFC!r7V?;9<@oojGik@08tpOwM_H&?E&zVBS#~5`zriBtX_EFxTC0DQ z)IR2dNUr|uOr6&Bu|Fiwjuy=JPvg*4T6j6TprtfPzs z!0Wx0BHL`WHg49`3L8eAwlId!Tc>nlJxGQ__WE;g7^HmEqP!gV4msqI|1c09zxx#Y z<<|WNy=T4lg`zP;XhcWWb)b86i|~C~U;;F3lED~f=@q9zf98~iGTTBYyXIF>a}X=R zk4I8sZMH)r(Oc<`vi_WEjeiG4=8!dO8DMCFOCFUe&{O#xDdB)xQ-nvOyh^@EvUWcX zTCyo9(^Fm#{3wg?Y6p82phk>uWJgu`yRx3-ScYnnZGOjcuQe=+bQ%ian5m-VUf&sp zYl~%S%%nk&!SSF}G#K*At{tOW_37)v>Dh7bbnE)L^)`!)BmeymaKqic1OT{U`?qoA zu1A55+pu##qBpVdQc#X$>}q&!qT>2<|9Yv~Sh#PoSQPi)dh4yfQgnCB)@P`l{rKId zW{Z$4Z%}ctNaes#wl^H_nD)h1=z_ABAjDa08 z^J~?STp!@pZ(IybSzvUQb_h^7Tj*IGCxoi2m*R{L5V*4AJiNXM^c2NvUPkL=C~X~2 zXY1cLSwQkoI!h^42Y|MmXrPS9o2&a$0yfHQU1jE)8-j_sRI0pV8Ql#aD*M%iMMTIM zOfjo^UVPm=B2?>XNpq<;iL&O1PiL9xJD}{Q+OBs3o_jm=-X}IR8sc?kTz~MF;(H#k z;}Q7lZQsNZJ0Inhczu*-T`i9pPVT+;V0X@6?>A5dS*HK!>FBLbSBEUAU3KU_D0>&D z%9aY_VIk?g@<}Kkn6|-WF>&(j#=sZ=t=GS^h_kNDi72e2ym6qbs*OaMFfK-`YK@M9 zgGWG4Ce~a5uD}nE&I?nv*XjGRZLjKh5K|lV@{U9d8YQ-9L<;4k;f`qr{FJjSD!|7> z>3jWia|V3jb=%~1z$HCI_c!%pl9L;`WR%>o6d#4u-!RGNWS|5MN`XH|%&IyV0f)0s z$D9#Tbl(9NhNMna0y#LJFojTw+C;zx@^NFn= ztp5E87d=l$^RlVneKUd_6#+1v0bA<~dZsPRga9vGY9SgF=soDUi3s*ol0Z%2q3DaW z&jgAz7Mc0>P_x5UDJS`;8CL=ft{l>(&MXhq#Zm0nc@at;SIAfkHAvC4uu9Sz8yt1Q zzx=#TaRvodhiin}V)|mhznItWL$tCmm2jwX&}O@?45mV-Cq!MTo2t{7vA`{ZM)i{+ z2O~~k$NAC%{TPMk@W_vzgPzz4>Jhe(ZV&ZE67(3##5;q;docg$Ex2R+m%ly)KmGlm zRJeCe1$bjH(qTBzJI8^ZWdhp{rt?xOAv*Maw2!&q2;6YqjSA$HWdHZ_v%>j;2El zh7sbXPKz-B9n?U-PD{CJdsv$4)NYp@M=kitgY%|bt=q|h3s4o~uB3Ry5sIpHXpHLg zag{^sdM14m2b4IFW>jS*C(#rM-#nix7@9JufhOuAF+9?UHBzxW)m5GHE&OG?u4-e^ z|4Q95;eQsF9Bzl_gn!v{^M9fC7BA&>@kwH~;Xh+|OgVLxu$t6cDXum&v>}H;I*g75 zaF_G3@mEiCf>OKx$}6vo2XS7_*!ZUne*V$B(tQj(`hrK|#&fSM4|0{o&b9Q}=yx>xY*HyBzr3Ht_^lwKXi}3v^xQ%*p!>-C~YzF~?%Dh?o7M^V0OXrdKu?FkaKE zMsXR%h2%I|^wTQ@U29f`g`s@)xp|d~i^2Tz64lpJ9?qP*ht)6DXQ?QcC+k;Uzg8zm z&N8Y7>rW`G1*6$dX)rcu=73hGmRWF?uNRCsOP99>bbLnHBm)i7F2fIC!BIrode10Z zvWdzNowF5Kq@&W0o&6U$;iA(VZHHXkhB+ebpTTHt6Pp$eBo|=o$G?a{ts`wW z6M|_dn+F{SI2x(*B_gwpGr5x2$0J8VEy5e>!sansndS{mvC91&6*j~=?2gK6pEEUd zqD@K^BW~twtSl15{fqI4Fla1q4SmohYEksDubnuU`Lak!wL}zY!$~P~L==SuAXHW4 zeR(1wMMJHSFi&#k!iQFPW+>AiNDD_{6Dh?=m5Uh*swvXCEBrWET*^>c%2P)l+ZeqG zrE32~=Qd`sxU<9B9dp;T@C4%!Z8Fq21yM*PVC$|0lJ(wrz3uyWbr@``OQy2DtS)$j`j!wBnHc_?8dg1mE#x z(HNx0pqV-^*~x|}+Gzu7+^AjOcg~%x_eMGmqnQ{?R$5F~TC`K+H3?bSBqqsoXGU$n z(Wts5iwRk@)MG+DTI$jGv-6&{(fm3GN=ed{c7{2HGv2RMh>^9ys8U;_;(D6yp`zAT zU&KYgRVg=7Wl&L05G6km5)eU}))Ip^=Mokz8$&(YrsGQOLp=O_$A>%8gGQ0aiw&Am zKI09f4V+y`Wwg>vTa}cu&naw;XW_igqz=SYf-;=^aYQ^*U$ZCOp^oH z0l*FCeZ8o8Jnn+WEzxvvr(gTEUt1dRHg4QF2zD}+9&4CrusgL#xjw$-19;}{QJ~bRKb5Cgzd_<2M z|F}1qvhsB~`PQ&h$sj33CCfy-u8K2{q^>FB8zUYIZUyB+ZE>f~2I7_J1N0>-;-M&I z%fqT_)fXU?BRZ~#?d0cbI=3Z_S&YK#8+iF#YVW*zC+ zCZ*RSc03F>p7XzP_>PC6t_Q)m|H}@+_h0{y0D#|l)o%|o<;CfS^KQgrE;vFx^KlnG zPAwH*Irl37fG1w~2Y4cm!nx<33jp}}&wsvjRCfFAx9?Z2FOT>X*zG<=ZG%s2`Cz(V zMCrf$U;l!8+~XeT51aa3qlfJY-C_aLgz>uMOy(jkxbZt`(98{{D-D>9qo^pVlm@wa zjm=hotJJ-#H8#qbZ62MMT2IZGt5ZXwJtvqDJdplX*&-RUkCZ59t}YWIyChf}RfL#C zeJ&sHX8BO8eClHESkL5y&oU063#SuJCG*atb8$^RmWmwfYgH z;hE6~OGj`t>+a!Jj~>bTkwqGcY`2V_xKegr%Fkz)(RahAv3>S;oJ$t6Ec%Q0v?8^( zE}dPh_E@`pj`7ZpW zZvE^Jai6FBw&&6zQcz@yLge-ph`HJSM@ZRUf8{>**be{=o8jo^|ECV}Bfpbk-e9QYpD&$A}TnpjxBx<6qs(3g|*Kr{u|v z06C)+K$K@;26PGJLWIXr4H;R@&B_{{@Ura%0QM2^5+FvK!W_t0ti`G>$CZR7)$43l z$IAgP+eg$7({s}HVs_G5wzF95>#=(K9P>LC=;u8)eDS~F&ZB->M2%lR=YQky3m;yH zFtg&hNYHb?R}OYL_&oN)$7G4SQGoZc7hZ;Y-RoYzaviU#j64#_<Ot2K-nOMVPG}QZAo&-FL2BMBfocTRy|5ycjgbGk`71-)+ z!rI@bMph00EDk^|L&vpjc%TqexsOs)QwiPzZZboHUJU$FV=y&eIr<*6_>5eeAL9YF zzX1?zW4ekI*(G##Q5^}OD*(>}p>Af>qq+cwN%+JAgIED&(b&;Stlz-4ASr?^Y~udv z0NCf4va3r4cb!$rs%w5(dRtY@L#&}$76n)cKM&5Jl7S!n=ttOe)WOA%arlmh;p^vo zC*FSag^y5z0O9oe7d#?@-rt<}Z5(#kVffznzBdj^*%*;b|Nk?=ZkaBu2D|41VEpCQ z4|||{uISXd%kTfc#eI3a995m~_jjtg``(+}n}8vpCW8hEpzKRP0l`rOLw(FtS5HNOE)U?e3~`e(#TS>eM-> zPW9~@;!NF7f4XmX^;TW=t>5zfeZQC9iJd;Qm)*`+FS*pKJsy1Wp%{lICl|lbb<4C= z{kuQ=Ufqoqge1{MJvyo@gj)rsG8*PmFUlDXT?}UmCnUIruh;vRn!HQvD*m8iv`K*x zHCJsh9GX-R;c-0e_1hsFiTK!$<{ZEi#so=J1U9I``ExVPaO#P|IfG#9a1BU%?|Au* zCom9%p(sI)j66{Zcy_AWiO0)&pAT@psuM>_jRi22kyshA$x2#lOuo&vDxy6v%5&3N zKXQwuv?}Q8`cYok4R#_uv$JS}YHeDsl&Q}-KZ;~Pwk0~F8M+%YhUv+J!42q$wdCNYET;pf#<5f$2?? z&L&8b;{2^9jqn695-6M2rp|R@B0XUo%}UL@h;M6<&enBZQIo#LqdV#m$@xHMBPN%` z<~oRRFTjhjS)B%zo7S4(Nff|Wj0L*uMLNc|x#6a_VN_bnlciov8l>bZ>J}fy0G%LV zZVNBkbL(~N0A8q}Z~v$Ia$I_A7UL(%a3p=~s z)J50;Ql-38o@oKk0$53qb{R9V1kd*G@DNGzJ=I4(dz8NKBD1`xT+)NDm@B zYvjX$!v&WfCkV$bCy(yJ3G({6R(L$u*vz|^On9bXRiN`CI#1HPKJF~Jp26oPXLjBA zypZGQSKx*qZRuJV_xZEBK2z3J8ZQBvye?v$M_LT-7(lAQtcdP}1w3I?c8UCG&((Ny zd3i3l5&9Lf;mXXdF!W7f$4&x1IcXPo3xM{`l(oG95F1E~>+Ufk-JPlxkVo(|bDgXh zGE>OfIr6qd)|F;~`~8xeap2Me0U)_Iglc0YU6Edz*M7lOg%=y6tsn*+tU?< zfM-yr2oeiMsIRoPaJ4JNZz*Cv?Sh*Na8Aw6^|I-a$^w0Oe0ztsA&r}DD~O+f-yQYqrNS#N3ava*0G;yIZMv(a`HrV#?Zvw5&3dd@I83 zOYWL9;)+o~V!9=-bm?z$LND{?&BLDmynpnly7G*RO`3aB&_%5N7T28f1AE_l&N;>$ z)XJWG39KtF#-=ez1=3ck6{8zM*Yql?J$24tK8ebnrkJ4!P7M#G&s^dAVo`~EnD0=J z`>N{MwN#BTvg05f49L6{pzFi%-n%ENAXGih2ERC5-t(WfXTZ zi6dq~v3Zl!1vX*QSewcUVe@dee#g~M)Vs*)s(*T2M-F&y)dssTfW$&N(Uky_Z3NQ-qQgruP$OreNU0;T$Z zQ${S*M()NSI%x`|gE|TUpcRmjX<<=j+!g`50%Q(39NnLU#NU*?Lf3+lx!DbkeabzD)^*Vai{ZO2ow z2Y&F9CbnX_m98wuVYn*m;YOSks7x9ck-`aroRfnpgE>J?`by-78x)B8a!f?4Xc%w} zJ+Hh!6(X^nlp>84wti=*_4A+;A)ny#$MjoEdsu+<%Jz;k>4NslIV1@fjung~l_ zEbSWg8|`bNy#3DILm!9+^pciQN+AW=6(FG|74r|l?r-5+F?;rGtX;b{%B7oZx$=yQ z@&0p9!hPSq4Qtk{!JIjBaI6734kEp9IcDVvwscXPu>SDn-=f)UqS`t<1-9v;S) zhir|<#WQ%4pFxr)NYVtN;eeeW&oL~l)e$E5n$V`)Wq_n3J+gcPa%y>@0Zsfk@qw>? z9aOaOyrzURtRLgF**X|kQ($;Cy81-h(+uaJoP|;*jo1gHa#p_VYAc9pHU}qDF&oi? zH;Oc8JXPYb4jD{ZB*JKzc#R4d@cb^U%h`+l7?||`KvX?OBy?ZdG$VZE78P7%2keAr z--AGsCQ`}cs?d8pt&JocsvONxigDFdSK-aO?*frAq>!MLtmlG~nwMA9{{li^`QFxT ztGpcv75XOoy6fGvwowgFBjnv4*JG2N969w?WEtc}SI(GKNYoZv1l5=K9xB`8P zxRoJQfG=8fD77gpW;6R>Y9n72^4a1ush&+LQ;o84u+MWCjgnVCBRIaeT%x}PJq8pJ z1Su%d3-khja{?qb@4=C%FRoQy03IbJU#6(zIb9WNH3)dAY@H1pf^vU2;pah1I9bBg znwk6K7Z+i7Q)gq5Gq_)PLfCNzxA+m%?3jW{Nb=KPR5kX%L90LPiYZN+)=D1Qb5{FZ zK&QWEo|Xe)f?|MLNruQ7iVB^(l9RSz*&evvaP`6nE(Sth{_>acsZV`svqgAy4U$-z zd)8TJVdct|eZ6i0boYMyHUL1L=je930Dx|{i-S%+#5s345=1VCG;z$g8wo^%kTw%E z26U8H82$8;e~NWg>l99kl(z6;w~SFPDPHW1DH7GCsC5b_k>f2^a;r#>`L|svhYNZ! zb#q^9k7DG6Xbf6mdAh%iHJT42J%UgSB@hKYSA5v3ga+ANNrUq$0xyYHEG@n^2hLl;u^MvpeepFx7Mv-KJtFRW;qd zeaXGpclmC*v)fG27)&8r`b4C1g=}0RZ|B;#)?oM@OYZKwmbNMB=jL9HKKf|PnKK7p z`qGys2ftM(Rd!l%I2zAxEkqNG8v^M-0}a!l5DlS0uR%x}K$5^1BEj(FO#RP#%d+L* zi9#{O@t`FtuZ2D38h#!rqz*@~P*+LJxS4RwfWl$<*vM8a=ZQsprQ(Pa*(pEDt4t}> zsE$-LAsJDoU-)_JK7Tp26^xg8qi2SnCxpY-a|5oj=Fdz}mj7DqKo|Q7`Ezr20Kr6f zBAA0KxntyjU9Xx#uco@!9p$RK659_M4@irK2Z#JPlqxRvY-_?hQl!ms3Izw_Fwde) zWs3JzN&}c44J$y=L}paVjdN82yc{96Ml;Sfu=NuKl70y>Mx0~@AQ`GF(FKrXN_WB) z>n^P<&)k!BSYsVq0QlP1zJ`x|>|_5efcL{6YM$H|zxc&Tn;cgx(rNB-t4{PfreK6VQoV}t*G*^hS+wEn8 zCD{43`>L*Goz(&W2qFCLtZ>H5;32fL?}gsPb)Lm#m}I4$!YxF$OH?P|r##4KN)f> zB}F{Weu^p?DmSXcIpg5b!_3i9ZiEF(cf;*x->Z3jQh%PjZ9Y#shca$vZgF{^rF(dv zAr{qzk>BP3yW(}{%$XCF80v?&7rs{A^DbX>2`*oBN!0Yzm0=WQs3*--6O0F%Xbv>c zXll*7BuUJ?tP`aK(nU~aHdiTImxXzZq5@4^$zBx5bZp7cRWxT*q2IwWt|fF;lJJ0A z7X>@Fdy5FC)l2y}P(`j|t8gW;=PEfQ0?JO76anW@YKoPaTnZj9wI_p%yM3JR>23!N zcvjcWnW2RS`Z6!iStqR9MN`#Z>v(y8WX7ZV#g zm{{LMrx@$X6j*B08K?Huim8QL4Vm{{LMdqcUd%F7yBwIy6qN+>D1fBKFEXTK45 zEPK%YfQ8=#Gvm4Eo(pzBf5+6*8@kS?0a|&~0(Wsi4`ErtTsC)?ws_q-8^>M%I9ctl z4*V`Z{Za?z^yPv7xEpJK_6%P2ml>9CCla!T-6GZI<%!TFU`%tfNm7 z;*>;=x}hPmVPm!;VE9&wh2W=+CJS{E7-MUR&P0YhmwIryX%2o$AxNjeGSlm2;~BbL z?U?`~h^FZRCxkQ+Ktlc;u)9;)QwLf9)UYG&ewPg!Hk4f@itpF#kgD!D`(FFd-b;6{ zY}*A(_ly#eU648DlvD8auYY|?J{A3fUtJxsu721L2*+xUm!EzqX3w6DX0r(Z*#BSN z3L?VGw4tp12r~6KLLvl^D0_k}E8Xmr9Hare9IT|#OJoSaNK*i*IhCe`y+Mpb9FNY} zb>}nbR+XlLN?IGEs&Jr5=ZaRi2YaQBaXPLAjuq1eL0*#q6jC?^?zo^$S1Jn z;z$F8&D2FP2CB%E(H!otIf1Zqa)G0wLf$+uJKM_lDM^XylE}!m;;_?NRcfeXX;a06 z2|Y+F2uQ++c1_9IoW!IDGIB=VH7Q|7BFnW?mEZ&-RY;o*P7ICN2ydUC=kkZJ_u@A?XJo?o__*KRC;upMrJ(LS=U#71 z-MQqR3g`jAf~9+DnW!bJC#~mg%JQz2M?gT}+B1LBcgp%oBg?sS=VH#BIhMWx0BhH- z#Rty+khep6x@-36REBv{IGMq#+p4d}u1@kNDj8%t;HoN-^ZyNHjU>r)X1H5?;v1=H!|W^$qJWc`eA zv7eT6xB=i5R2VKPKY<951#s8wq4s8=4znH}%QbB_c;;74a%y;-6^7u>?-$tRzTzTOWOehvzdYIEV{uQCwlt=O6pgtUw0u&q?ii>uEl86`KA(U03s7s=+ zo*X;;$@6He9`pWlT}5WStqOsFQgpx-t+fOpcCX8NY?$ubMko7KNI|u=xT2os98^Jh zfohPFD)Rp1&)YD?h5NW1hz^xni6bq0Mx}lTMW!{c+qP@ht}R>jxmGlh8AD|ZY5rGc z-D|G7#;#ii3XbBcjSdXhwR3dGGjzr?-ptX}!YQk8;%@q=Fm{dCX) zfuQPIn{L-FDkyyY05a z+_`fxckWy~`Q($8OD}%kWedM=m29Yx=*lJXPKNGy2i@^DWV;KQe*PLXdlQYBEyNu+;Gzk zrDNw3H`)w^JzcNbavX(h-b52CRB`i?TV8Q= z_tP`40|5N^j30aVAG7Kh9CO|=_GEL_=~wjqn#KIA3y4!qfy*%_Q-_j3-Z4!ECBZDg zssWJ_z!EagkY^e4PFF{FosRxL%b{`!u)^q#Lzp#d7I^o^p_{b=tP)UB*RW+2F1dKF zBC-KK%e^yPKS9B4ZnRxcN8opgK^!E@ZYm2GxT@>NQrg;ctP>Qwu1j1VNK=@VMH1;Al%EghGyxE=zR;as)Vo zq$^M>b~?dq>TMkf#SD??PFgPY(7%$(4)VLFMYbuelb%YQD$88ULFxLRjxY+@CwCj{ zv1kX}b?(F1@6~R%(cq>h?XuTrcfsn<-rpC$?)D{jVZqY9Vni7L!2K6I_&4R% zUA5>cd;9(8y&vyC?^po9l~Vw|eml6eaPJhHSSl!JWf^_;*T5$PL8%-n*LAwA+kwn- zZSI{(Ys$U)8&6{Ie|in_WE2T$#U7*S;5%zM#A9KvwdiL;?rmjB$)V(XUA@l-3W|0; zUD+o@jIU$e{CLwMK0oV$oYNpF2lqqS4X0s2foHS_Mm2#F)u^?&<5v;sHXqy&J$Y32 z6$Gwjgh?~2Xm>XDoNB{@>mxn5r1_XdaBTjT0#j}`&Jli6%%qiZGX*J?Z9*uekQZR5 ze9nMPYgqwGK#`u3un}ISpfcU~P*UNUZ;xX33Dbf|g@VFr`PlU1Qat~yb?A1xnEUbB z0D#q>yMGhvYt?1{(F5^^(OWkgxJ{;acjf6<8sIw?$DVgAR)2Lh_WJnVxMJbu0D$Ap zKhFE4moNOmEBZViJpTjmoas&IKeFak7~TFQC$2CVlFB*eaPQ{E~ctTb1h*a|{mz=DUcc>sZz+gIkJw##CV2@^&ND^ zy2!f{GFMo=>JjX*aDKF)d(ZigV2?#RR4grt2zM=euy4?DBcY-@8*(FWS#sOo9PH|% zvt!Rc)@wrb3)nv};5a$h9ajPR8^8Dm%$_|PJAP<8D>pn#8dY%Jz!ltzFWZtXw zaPC<;@>Eb(>zfd%R{e zrA19EoWoq3C*kEqn(xu{xYF;9C9{5H=*l(KEy5glqA+Vo9BFzut$N3~?0~@!;CJKS zFGT^016*mzC)rgH&EkHAJlFHHSv!Yp8@olRBR?>($HMuz`}{T7{j~XEJdNEK?tr^j zJ%ZgA?V!(`=3u;Q`9o6%pfaspjeTuetC!6McAHKsnS4+xcJBuwK-Oyq|DFNJ;o)Jt zcgdk%nxGgURv=YIF3bv-oLV+;KDg=-t*uQ8Bm|NaNSi=&AVF(Tpf!-7(bB3bLKG7n z#-R3ubJ*pqNWZfu;a0LX z%9>_H0hq$huOEI<#em4=1eWW&xm|A;>eyaay%kSsIsD92pT73s*ia4Yj-!H0-19x^ znj>YiLVyHABm!x}=%6J8$^(NCX9GX}_~XD<)%Rfu$XtRl0nwEpB_NeVx1+(X)6URs z=Q^@;=qre^&8?vaW4DDn;GR{F004Ghv?K0W_3)&iHkIYT(SvX+ZvSrtJK&0iSK_$y z-(NW$K77f;$n)G=*Zse^z2M_}^^69;w@}vzPq^Uyk^G_%!*K@63Ap4lnnrWdh3_%H zj5J;r#R-y;GzrZnp)ru4H9%-I^d(6c+Ma}sKW$;z$hI+&I8mJy9*EFWsb$V*Cx;|A!O?{GbfEZLOrBJ)o+-xHS z_xHSBXxd9K5IxrI@4X25QAC*ux{NtU{?9#9(nG*xMtC9 zc<*@&prnB6O5`a4xkN4%x)V9N?W`m!X9|$qn^i)-h}muncfh^pKN1ByH*M`3^qiyQ zf(tI#EYV%@zRZm6w%e}KRqZ87b?o_?uJY&09`%51rxSOm{Q~YQ{Qir&v)t`=t^S=m zA#%b6$JDePtCjjoPSrs7;fs#a5f%X?*jhA-gps6-q`^p=gtP$-at-JdHi0qUcItLP zWasC|gIZNUwVbrz$+0AypPekq>;H_^C&=~jxO;Hu zVe-JL2qj1`YyTF|rAPqyV~;EMpEE z*pxt|*2pV7WsP25LO>$?#zf)*o$bi7=RsTFG2q&?wU_*7ZFI=x_jzKBE9IBRQgs$MVtYw7HN0L?-ju+OrwV|* zBl(`sVaIB+aH>k(NUyrVh7Nccf4zPXGv6^@DU_(9!66NWMQTA&mcUI*@3XIW`>H=; zpG7-4!^&l!>Pts{6Q8=~AMxxikKv`uCa~r4gB3aI`Sa&v-MV#HyLPQj-wJSZL9dth z?BuSXbdDsQr(~+QUK-NXnP@eqk%5y!djpUIa7*R==; zs87+4zzEV;7v$inNxQ&@Kys-B7)R)lL^X|cni&O8uU}lKkdrCsFHj9ek?+6Zo+MJ@ z{?gS-cu9W?u%k*xn8P+mZAfG0Duq@!NgIi2UI>Vs@Zs|h#(yll0Utc~0Pk}vrM%Da z?iCAg?OC_u@a21#%E*QArUHKVxqGXT&8k0Q??pR$=`s;;(@i(wopcm_!9T+K_3N=E z20fls_s4{1pMAEkw6P9|{&>-v|CVTOQMYAs~nM36TwlC!d`mnkL~6qO8WP2CTKKMG>1|&2Q>Jlsfoe~U>4?}COe&DL9M1ieA0=1A1nf=D+lh~k8Tz9TFOh|yC7xw@UAvQDp*mKxC ztm(Qb!f!r%Pqe`R0C%o>7<(<+u}o<}xP0Ar@V5L2P)Y_^wy|Gx_viM-#gAN!9d_73 zH%}&st`0r)P#kvHVfgvae~vkG=HRKPof)g)1vQiiGWW)umiBF?N zXtXr=r40cg*sAWbkrboiWC_?#Y$BAd;|0-PaiP${vByEe2>dLVYwL9&t{?66jJDYm z?L;J{jmo+QPG#kscK~<@_2nWFC`UT9(M#Am2x44fZ??X|J@y{2p}^EMSHqcEy5r78 zTo?6D<=Xe^tSfe~OZLTs$TfL$XAcNlbJ)7~9y!hV|pvxN!pQ zZiYP1amnd7;`np-H}9wa9=|%roTCybC`f3|-F1ua#9_aF0j`;% zHJD;xS`)3I2GW)RJZ3N?z&Tju0A~Q_Amk9pHQ13vA{0`g%37W%$H`oQfa0@JVDm^d z^|xwMnXAATL};FJ$)3DyrucK2Ba!%bTL7Di2TeVl79{Fx)Slq@GQ}ldM;H zzh2Mt)R@iuqk!J&8$wHbr^3`{sJbHnkWyB|MVq$!$}V*P1ORue{G;`@5GLf@xN&3U zAiw02OT6p4*R4|xUa(-nD+0SBwJdbzCUZUuA78$Fc~n-uuL?%+oxXqC)s=!jdGo8K zv5r+ADJUkc8i@fQX05IvbBR35kact9c@D+0><|jJv$9l-jTI?uF-ak~Qp*#F8FSZl zoVj-!g5vjcH1l*KIgX5zado83RV!h)F%gP}BCa0nDW5oDg)30NA(`KiXt(Oc-v7Zh zbFi)f@Zee*wQ#+DFkylk1!R=+{qFZkV?qFo-gJ~otC)J%3wY9+UR6bkplKAij6TP58jd{q5&^-7!u4`3K{8{Ral(wF3bC9@>IR1!)1Te^;FC zc0FxA?z>G#(Mx)V_8|Z%aV~#nd@AMo1 zPPp($Tz~#8n3$N@EDe;|vu9)0tXTooB`VsOJ$p8`KW@0xMPm*?lkHiX5a`+o4w@J6 zz^-RIwGQlJ=XGFGj-E#tiHLr(8{x5E!&BS?=$y{0eNAIvq_hExx4z4tgD1)9&shdSrQeWKS?U8rNbTMRbm!S}4dNL_QNpOhDh1IMj+3obwF4>)GUeqaFePZ-9J&Z^F#{$lZJymR?p zQ~oxBwCS)?sw|3g$-Ue8)cJVe!Zo#^5Cgc)X~Tg^EnFc&~pyy9aM7qabNxqK*pr3A|(>*PN ztkP@NP5mk!77nM*rIe^h-Oj$gmqIEdw(0ZpC|XM;ZvvxWQRvqq^Z9(!E#ZP2h1OiY zAOT=htMv^tIi)$2LX_E*1v*hifrTu;AW@b{>Hx2ww6^y5;>`(c{jXa%AF|SCHYpYI zOix8fqe840`2HEcx6gV1iZ|oRvu?rBEB5R8+P_@9dQ(Cq5Hy*Vgy{wf*PapB*-T>; z>1{u$=H{Z3JoeaQm@{V%=FFLcZnp~nmJ9f|UQW31NPW-<)+2ys0zwnTVGOd}`BQcb zVYpJvuIw{r-6R!gv;@+od5^;N$)>UfP?>#Rog2{wscg0O){Fnao3vXOz@iFhfx(>PwEjDmiks zmGstcO9+FG+Kv-QQ-MSj=G{(&S6Fr?&=OxM0ZEaq=*QE|xg*zc`l`Dr)rRjq8dbHA zlvNEQK`o6*OQuCSWv(ToFI{w__bOMOrRO%UKI>K-xnkd*b+dko@gBk19C&%2qhV@_ zP?})sMtK0^O`q8w4_@@AzTRhZ^5&j;>M88H>#k_G+W>&+)2CbT>vp@F4FErQ;gQ(* zJB5*h3NTW3B1Wr}5E=~u(GW-k1C%%*7dV9x*Q(i;{Ex!LOdx3{XbuQFLMFVGmDlhP zPV=G!K!;;T-1o?OKgQY~mIPEcTWFW1jbUN;=@s-1==r=l7-@899~TRh%IE1h?;)ig zpOm?>uMcL=$D(?g@m+A`cQJ~XZk%uZzUNEv>MHgNJp7R{%@n%YB zwgg%&fks2m*IUrL|{Dk z`rF-izHVT|GSvbAcRp3m-9G-+XEqDig}}TiR1$aUqwX_>lOgXp=U~T}#eho!G+GH- zLz+I)Xz3}4LXL^t)H4En)pA-H6B2}JTnQwNM4NFZrQIPLPXyu&gd7QS2+9l{MIs?t ziUW`vWx6SM_1fMh?$xtEh3KjD{#=O9x@J+fD=B?|k~u>vhmv9&5&DuVJr6E(v(IrM zW#&^+RAB&b%&b^N7y=xR@M6tOSF^7d*r7*iiy&PsIhJHRhW+M7@j6MORc;0b1crtZ z3=9Z;GBwXhtGo}YwXf)_Q_AsS;3~_K`{>|uh5TE?pR`;#Ws0tn%XT<3dtf)!20#cB zF|Z5Qi$zCwo%=`Zv1mubDyRBwO2C^wv%TF39{=hyuL$JMJMTQ~yYId@=%9l(86u3ri58pY z5t;g3=@e%qq9C=L6DyDQ;-#|j$QawudnL${k_Y^;s->&LNi!vxx$F1~Z>4Yg6tWZbiG zyg+uiY*(9_EQw$9^FgVgcYd|1rkO4atnBN3l*2>A(JKuv0bB>IR zQDKN}LJ$plEFq9XP;R0+6Ws~NlY+vANsxb%LpUyIE*yw%7`p&={U}sX^HR3%Rn+HX z4&<4xLuZ+eyp&C8_4;|n)-=imnKG@_k(+Dw6LxIfF|vZ=cEz^^m>JS^w*f|bq60Jl z)sgyIMfRP-DnXdr`QKpYY69p2GiT1kwB|I7jErD(bky#1N4@VQG*UuqKw#Rm1k33MD*L?ZTZRj!p5r=3rHg&V za690sk!>)jvSZ3+Bf-|(lshe<)wQ!+JK+`eFKMGV@M4W=lZeb0OpKrycR znm{HqjCRMMSQ=|Z26q+EB%sScgMh4$lVIH{5`bh4cnQHu6NM4n3^>3DV7j@#7%MLx z3nN13y@!W}F)}<-306i%MsUorFQL^UOrMructitUQO8TQ1);?{ugh52rq%VNs@s5j zcOR(77USrvnf*nTVY&f44R*R2A*F&;61f4qEZ0_yu3N?ls}97ai*D+fnz<=yFK*ma zcymP)AbkCM&wB*BF5Ce`gqg<=;_szfB!cJJWqSje2yy?-;NKSySMp?1c2O$%>W3;&sD_eDUtHC z!-i+=;A7YlWKEvkQ*8-&L0ap$ffcnw=CISL3AW0eM^(DLDrK3XT97Il1Ja!sfbEb< zX`;OX7)`CN_@CMBo|;-6GyXijJAU!SUpSE#fQHZ@)^tIv#-Ool{&&Hy5oA4?1BlHU z#gj}0TIz)4Br&xeMU_Q;d6`LryJcht1&Bmobi=5ZNqp$U4oCn=@}h27*=`0qB^d_Y zeA6kc&1A|HipWR6*aTxJIbD~=hN-NM__&e=tMIi{+59Msb`%_Pe*q-l!(ipBlz!^6Y)(5gMr926KDZeZHXCI*KRBuyjOSwwWiRiKMaXH@ED zCKlu(o%JUZbf-Y4t|;{IifeP^7115Rn6R@ucWN37((R&Z8_zsbel*9%8`(4(3iJ0O zl~l3>HRW_;i~GKQ;d*q&+fYpaZ38SV0Zgc1K(z15Yzcspfy6{;34m={NItr&uf1de z$>CKwbvbq2e}W$w1zyuc@}6x0<(#a?pRRrD+Mq`Q>RtuD$GJ12^D$@69DMMk2?!#j ziB`&Kq=Z&WU}#7;E?O;t#L!bxX^62zrP4&)2!s1;W<*IT)lN>8R^k+Qx#Slqr3<#X zl*o+Kx-3#yDUnHqEX$DPIdWEzMqWUfA_|GZ#;TmK=r>C1CxxACdH(t5E1>5_dWE0> z1Nggt-Uh>ecojz9^4I@T>83`b(N_)gl>%J2d|<_{AS1XbgiawLw5Flo?W|8}y;Cv; zSw>KvA;1xUHLokQhb=)B`JbjX8LplxCJ<~lQwzXq`jd&mOed5L@JU%b=8tYE19~d- zmgSOa^5qpTB3BZUog#my2q#HLIR(IVS*MPj;I}GL)IGNVo*M(r9R{W~^_!+9;+oa~ zx>DaSs7ymQbV0KQ!H;hMy>28_+|;!OrL7IXCW4j>zH>b=M8FGOpqT*ez9At12PZu) zYIZe=&YU@O@WB(uHQ1#k+>rh2Eh z=o1(@(&1&h1DI4*DrC7tr<0@G&Ctzq2lOQJTq2Vaxs;+W)G<_yp{^|d|MGxpFj3m`Q5+Y&8 zxEg-5fU2o1MOtCr=$A9&ub6biMkZzQ<2PrleJi8-JoL#{L$k>d>Uk$ICl5lsRL z{4#^rF~nX1*b9)ql0YyJbjs;lK+qUOLTIEwvqcza2@JF}4W-dECQO#*q&x4alF}T& zteNgGZ1-XeheDFc6hO_jB&w8|t5PbexR@!4ZkD5)No3s&?M{YHTQ?*$*s;D=61lmS ztiS@G1Q0AB8YOuVrgl?|luZ`p^;^&D=b(?(;@sE2-v|m6n`W!6w%TOUuUjjNHBq+S zdh1tAD1IgLydV8nJw_%B(4`I1nsp8B`3k6tN^U9hUXAQ5=()x=cGoP$UC9>km8pH1 z4x}<|FvVc0z%0w#N*k`{xiZ{0GmRjXonByP#LI%3D(w@$v1|jz+GEHPgwDMyjVR5>W)hY)cv@!ZRKKx=vg;89R1h{#Xx)s%i~1h0rbp zKu1ztDIL+Z+g-FLGITncX42{ACc4w3Vw#Gg&&X2g6y)*WrkL}#uFh1z5Fn^x$L44P z-FKunSrenKv61IFHf-2{k&%%K7**CcbcRg@q)-+<26i)N&cu7(^B#Qjo8R=Fd&nV& zpz-zz5D}jJK}6AH6PJJZ!U2YJS8B%zVO+tQgtVpI!bp_zPOJ!Qh@v2_kfcJ8v(Gd4 zxs)dT-8JD&nMkahCQ!D~pc@Bz?pik6^xF&Tqsm?%%#Xs;|ZMyznDt`pk}_u{SGq6?LGCE61i#>YBncQRy|?!4x?);hcB zv}^I{^A9)E6&mamORQdfFSa@8jX+i!F)J`+E_GUB&cMek-w#(UzO}FDZL$%wu+A5* zo2Du9JjaX~Gy0119{<{Nn+$fnngc~gYhq#omtTH4=FgvxHET-og7od<_6K{-2c~1r z@dtrGxc@6ppxfI^8J59wf3ZgIVZ( zu_3_bge#mhm0Xoy19C2(to)seMuvV&N(Gg%9XC`t8*tMs0G6x?zEW9vqyiLOB5v{8 zzy9tFG+RxqeReIfyo+_~*CWd_C@G;tO{cRn$;q}wpf_v@dhH0%NXla+p{mD%?_G$P zggW2!l#aH>C6bTrp+PG=hME22I96^U=m01MsFnDQi{~A(-L_4gK3BS8LmIdK)CLU; z@C1WK63~nk{Op(xI@dD9t`68;TLn9O{t0~ZG8Xudah}X#MhyK<{ zS?P_yM!rQAAWc zg+Ie5&V8qj^i0=w?b@|qxf570I5V9dQ*18g5sMh_=;iz3>czM9ow80wX9;h|1TtgK zKltE-al;KaV8)CYQ4xWDm+iLO4uGDAz3;yJqBXtY;bFY`)vv~Z2OfxTeB&DtAm^Zw zM%eA++nVnpAVB!}=WfO5=qQfA@SXnj0XcjElGdwL5NlvdfkaR#$CT)%fXo=IBxEL# zm>Eb`37VAX_f5zs4f!BPrfUxcZLVaZG#ibvve6eSP2A18+E2Kc8HI8PxvCt$sz{tO z@a50wPUFz@ArKLiddX0rGDw!CMr;xQ+fFlp4Mwc>X;aDHXBKGNnZSVVjMLWBfbk5p z^>lD1!CeW;6)>2VU?jq-k}Xd9^|ee(Jjsji(`h9+5mq8Ly|xz1WY4?XkM3GYBzgtZ zUpl}9gGK~+R6%U7btid3fkuXaY#j8u@`>^qS#t6KoV;+vm@;Xg({r*K@S06s!%GrE zny?k%r62-h7x35|oXTLB;9?^c8=D-5B1dYOO=Ez`6-=nJF^L>hzT%f{(JM_ik+~lH zUvqPU*X^5W;DWL`UpkE`r)&RVHk+>~b=pDnr>?^?A# z;=&9%VrCLJ{2Z+;bJ?lC1OR;SqIZ~`tBC9j*cJZ^HvA}IrYDMK0H7O@Y#9PcN>I4~ zD1}Jj3AdJxS<0!Ml_{#s87i07O4C&aMv=1SY|_>e+}ts%qMQ0}pLwE^prp+PGUV9M z-T*Ed2L-Ubb^sV@f#z?iBePZlnll6FN|TlXoJn9J2W>g1L8_Vf=)Tluh^Zhq= zflLCq1Z_QChcjHR!%wUSy>2Fut5;r+=%Icf|Meap>|oMa0GQDGqPlk!SU0ZI`Gx?m zBZwVl=z5);05vs(f|}C+Xy-urLkTz)o&R(Ch&JX*3C$K^a6srZwrQFZDd}3Bk+3G# zy12H)WapYqO@WQ$H^;`& z$T#FECMFm#9an~w676n=@r_+wM16XC%|dS5^5H~1eb z4n{F}#sDrp>n40?`TiCHFI)Tv9J_o!OK$$*;#+al@&z^${qf@4`{tk3ji2?)*>h)Y z$H&JpG^FbrQ>Bs=sp`_DOL4;uH(2MhjT<*2O;fbnZ9MwuqkVsu!&kipyRSdUI*zs5 zZFD*v{B_4anG;{3V@5VZ5~hnPz_|1?Hvj-W@`bn9+Fg;>60sEo;EE}XPP?A{#ouA> z5l`s?^z;ev#JX1{$g&(XcLr#i=_Rcr5s3OJPKF&XjDhcd4z$NMlY`!*si)`A1NDnX z!B1@fy=oA=b_{s2gTlBYIc1+XsKy8rQA@@meb(DZe)(Q~-wP6$lWIE6mMLg%3u3pK zP``RnpX?+9DFMR`h;7aHO>_S%G<3Qjg59SV4kJl`hN;yJ3~%9e9V9aKQbcECBe-DHZEu&6nJMwoXOZ2uNaz%9+u7w z$yk~$tHSW`a48tz)b0Lp#liU2SvTM#D-NvC5{mziTmEKTzW5d#vwS}QZ0mPNE?)ou zc=xh>aP8T5^aWgXom)53o9y|3p84Btx7{|m8(IM5k|j&9a^*_gbI(0^`Q?|fdi82- z+QPVZ#tn|#w%&tA?%5VumSKE+97-wt>}Nm2e*5hQDJ90n#*pVZ#>dBT(&rBVk-~RR z{)2tRfBe@yO*F4l*kYod%y#(t|Gp2iXV1pWnKN8GVpH7Y^nH6%O}sR$2J8SJ z&tc_jh6M0*1`H4|m>@ZBTd2F91v(1q`WH0;d5nP>4T#qVe9}BEL5~3BEy(Y_=&kZE z3#aQ&zNyy@3=jqf$*SojMPq{4b1n08NwWKyCl&D1t3J%Ock4uLFj1X26sKa_6a45#@0?BG&2V7$56kbhH@y*B#nI>gn=-y$47CpIuB^t1sU=>jwP8 zinroBXWfXCRvc)h+%H{xGmc;W7Axwm09?8FR=jWdzE*(Xn#H%{y~`HZbojcn@9f$6 zbeDq9Hat8WofDmG`Rr#ui;FJ0XpuxK5g4A(3mxdq1VjB@Qz!7hEhD;$}lEiCvy5L;43Oaa?ofwj6>ubi2(K;i=z00sie1db(m<>H6Z301`rS>Vh7G;)vpml4j_@ z+y;P-GdA(phc%X7EOnz{79e&KP}d7RziY|@V)rc_T1upzXh1InyH72gj#QALoao4I zaL^Enn`FAM!U}%+r=hszqqpDx^vVyH5J>S08D5?Xbj8S40eb96!?I&@Sm#4qfR0o; zvdg4IyPaWl!vsc0+j<7Jn`=U`ah#H1T>JkX#^ImX9twoM@z>gQ+jHAv!z^1f`>9&>%!Fn|8*B3p5p`jrh z@QG>u7#KySAp$W(LE1oDn0ZJzqheOpoU0%g4W|Go9aduc&Rb#n+$}M4{%ptrf$ngE zCzGsv(>jZ*sZp;xJpcd*i%CR5Q~+L&MP2tN@H3;9n?`d+Al~^(0M7xtpZ^(X=ivKa z)N{2tfDvKqe#vL{31}!zEv`U}qAu5UtUuR2IC+=$XA-mxfnP{LGaA5{1V$PV+mBRN zj)0!LC=!1a?)6`VApI46WNhdudDjk#(>XzC>Y%v&{{BvuDrlDMrvw zr?^<&dG5ZYJhsb6g>Rd8?diAT$n)O<7!{LZJXyGaX9n(dp+n(;@1>YEYZhK}*fS;2 zQRz90NaMoU&2)Xn6WL@Ve8Tv~DUX$oTmc#skz8Z$FyK$Dv9UPS zjb)k^QWzP>7&<&vE@=%geDiLglj{!d^ag0=0L04)d(voxfNe}GEi|m>v zjZMrBQWx3P|L+8r_E+_kTB+2%x3xW8-_za;aypmYAuDVnobz zXK%fF9rikM*nnNxgY8Gzg8>|I`p&p4i0X!<&90c5h@FK>8p>oXX!_|D zC)t#iowhs|ksfLyK2fH%+EIg?)V6F|ppw%1!k>y2a>dAUiSe-x)*I2ziFOxxCh?_3 zzr@E^zTMXC3ZT6IM`N{y)E`>DzmW=M5Ab5tl6NoL$AU0SN_*(iJ)ABw<2Ot0?JLb4 z85zOg;2_H!3_oz^hId-$s_+K%sn&%EAw+>P*b zcQzpyStZ7QoV*(I=FP*wpB^^S>hO$kgeb~GMla6P;;d1OGc`b9eE+Ox(C)SkmXtsj zZux0%or`4lG{|`~Kn;P3>5X!ynET+_8ksex+i|Vg3SN0v$0A7B-taLy9e)Oi=Q^pobu9C29UgF5UriJ28NqZ zGn(jbHHe8;T1IP`Ge95ty6VnnK-)~$Dk4J-zy-XXnASUmRlj@`{NlI#yKP%5?e@3x|x51fgLfyViga0BD4fUEOa&J^qjMO5(4f zL9U#lh=3l$cP4k<&k~D?{F78A%HvW3-lRJsB~VH>5-33>Cj4FxW|uAg1CC$*=05f< z{hW6#+snS{JD2W(TQ2xB7M!*n7^r|o9Jp_K;7U+z$^cWMMoydd(aLxJyUH$qo;N>>>h00^8V|ex zY%@)FV%_`K{1te@sO3x-;K$Z^$3`W3D-^QpfGq|_zmdSp2JE1tyPlu*IR?LEN)oT_ zz5l42P5L@Q;3pY)DO&k3pV|3LR~XUHSqab*NDkN<{B&7b`1om>e=3BYuqpKGnnpRN z5bUf6yZ%8g4tO=sV>qA}#QJ3<2+yL-rpAV_SZ#s_6BiB%vN$-3!1NO<-;RGj^Vj&; zinsN(3;LjU%cScRkr)x-@MU}1bp{6qar*_2RvgL>{M;_M>74s}rjl+=Ev8R570tTg z;bBZnOjM>NrqT>?&9+D5!o86gItgrj5B7|mHU(OP2^uX; zH7T5Iv{H$J>;j4y-#YEjka-S{l@aF-fabpnwBWU%!GCY8_% rCyj=V^3p=TPWV2x{hpR4TmSz9!m77g#l;x700000NkvXXu0mjf?N*^J literal 0 HcmV?d00001 diff --git a/docs/source/conf.py b/docs/source/conf.py index ec8db79..64aaa45 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -6,23 +6,28 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information -project = 'Comprehensive Config' -copyright = '2026, Summersweet Software' -author = 'Summersweet Software' -release = '1.0.1' +project = "Comprehensive Config" +copyright = "2026, Summersweet Software" +author = "Summersweet Software" +release = "1.0.1" # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = [] +extensions = ["sphinx.ext.intersphinx"] -templates_path = ['_templates'] +templates_path = ["_templates"] exclude_patterns = [] - # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'alabaster' -html_static_path = ['_static'] +html_logo = "_static/summer-sweet-colorful.png" +html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] +html_sidebars = { + "**": [ + "globaltoc.html", + ] +} diff --git a/docs/source/fields/basic_fields.rst b/docs/source/fields/basic_fields.rst new file mode 100644 index 0000000..edcad5f --- /dev/null +++ b/docs/source/fields/basic_fields.rst @@ -0,0 +1,11 @@ +Basic Fields +=============== + +.. py:class:: comprehensiveconfig.spec.Integer(default_value=NoDefaultValue, **kwargs) + + .. py:attribute:: _holds + :type: int + + The type of data this field holds (used purely for annotation and IDE static type checking) + + .. py: diff --git a/docs/source/fields/index.rst b/docs/source/fields/index.rst new file mode 100644 index 0000000..5767783 --- /dev/null +++ b/docs/source/fields/index.rst @@ -0,0 +1,53 @@ +Fields +======= + +Fields are the most basic unit in Comprehensive Config. They are used to define named values in your configuration file. + +For example- in toml: + +.. code-block:: TOML + + x = "foo" + +Code +***** + +.. py:module:: comprehensiveconfig.spec + + .. py:data:: NoDefaultValue + :type: _NoDefaultValueT + + A sentinel value representing that no default value has been provided to some field + + .. py:class:: _NoDefaultValueT + + Non instantiable type for :py:const:`NoDefaultValue` + + .. py:class:: ConfigurationFieldMeta + + .. py:method:: __or__[S, T](self: S, value: Type[T] | T) -> S | Type[T]: + + Overwrite default type union behavior + + .. py:class:: BaseConfigurationField + :abstract: + + Abstract base class for all configuration fields + + .. py:method:: def _validate_value(self, value: Any, name: str | None = None, /): + :abstractmethod: + + The in-built validator for a given field. + + + .. py:class:: ConfigurationField[T](default_value: T | _NoDefaultValueT = NoDefaultValue, /, name: str | None = None, nullable: bool = False, doc: None | str = None, inline_doc: bool = True) + :abstract: + + Abstract base class for all configuration fields + + .. py:method:: def _validate_value(self, value: Any, name: str | None = None, /): + :abstractmethod: + + The in-built validator for a given field. + + \ No newline at end of file diff --git a/docs/source/globaltoc.rst b/docs/source/globaltoc.rst new file mode 100644 index 0000000..aa8527c --- /dev/null +++ b/docs/source/globaltoc.rst @@ -0,0 +1,13 @@ +.. toctree:: + :glob: + + self + index.rst + +.. toctree:: + :glob: + :maxdepth: 2 + :caption: Fields: + + fields/index.rst + fields/basic_fields.rst \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index a1ba680..52a7cfa 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,17 +1,31 @@ -.. Comprehensive Config documentation master file, created by - sphinx-quickstart on Tue Mar 24 20:35:07 2026. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. +Comprehensive Config +===================== -Comprehensive Config documentation -================================== +Comprehensive config is a python-based configuration library that aim to be extraordinarily pythonic and easy to use. +It takes heavy inspiration from pydantic models. -Add your content using ``reStructuredText`` syntax. See the -`reStructuredText `_ -documentation for details. +Comprehensive config includes automatic config file creation and/or +loading as well as complex validators for incoming configuration values. .. toctree:: + :glob: :maxdepth: 2 :caption: Contents: + self + +.. toctree:: + :glob: + :maxdepth: 2 + :caption: Fields: + + fields/index.rst + fields/basic_fields.rst + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 1e76a00..1c625cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,4 +55,6 @@ Issues = "https://github.com/summersweet-software/comprehensiveconfig/issues" [dependency-groups] dev = [ "sphinx>=9.1.0", + "sphinx-autobuild>=2025.8.25", + "sphinx-rtd-theme>=3.1.0", ] diff --git a/uv.lock b/uv.lock index c703f54..2cf2bf8 100644 --- a/uv.lock +++ b/uv.lock @@ -11,6 +11,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929, upload-time = "2024-07-26T18:15:02.05Z" }, ] +[[package]] +name = "anyio" +version = "4.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, +] + [[package]] name = "babel" version = "2.18.0" @@ -102,6 +115,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, ] +[[package]] +name = "click" +version = "8.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -119,12 +144,18 @@ source = { virtual = "." } [package.dev-dependencies] dev = [ { name = "sphinx" }, + { name = "sphinx-autobuild" }, + { name = "sphinx-rtd-theme" }, ] [package.metadata] [package.metadata.requires-dev] -dev = [{ name = "sphinx", specifier = ">=9.1.0" }] +dev = [ + { name = "sphinx", specifier = ">=9.1.0" }, + { name = "sphinx-autobuild", specifier = ">=2025.8.25" }, + { name = "sphinx-rtd-theme", specifier = ">=3.1.0" }, +] [[package]] name = "docutils" @@ -135,6 +166,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/10/5da547df7a391dcde17f59520a231527b8571e6f46fc8efb02ccb370ab12/docutils-0.22.4-py3-none-any.whl", hash = "sha256:d0013f540772d1420576855455d050a2180186c91c15779301ac2ccb3eeb68de", size = 633196, upload-time = "2025-12-18T19:00:18.077Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -307,6 +347,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/73/f7/b1884cb3188ab181fc81fa00c266699dab600f927a964df02ec3d5d1916a/sphinx-9.1.0-py3-none-any.whl", hash = "sha256:c84fdd4e782504495fe4f2c0b3413d6c2bf388589bb352d439b2a3bb99991978", size = 3921742, upload-time = "2025-12-31T15:09:25.561Z" }, ] +[[package]] +name = "sphinx-autobuild" +version = "2025.8.25" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "sphinx" }, + { name = "starlette" }, + { name = "uvicorn" }, + { name = "watchfiles" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/3c/a59a3a453d4133777f7ed2e83c80b7dc817d43c74b74298ca0af869662ad/sphinx_autobuild-2025.8.25.tar.gz", hash = "sha256:9cf5aab32853c8c31af572e4fecdc09c997e2b8be5a07daf2a389e270e85b213", size = 15200, upload-time = "2025-08-25T18:44:55.436Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/20/56411b52f917696995f5ad27d2ea7e9492c84a043c5b49a3a3173573cd93/sphinx_autobuild-2025.8.25-py3-none-any.whl", hash = "sha256:b750ac7d5a18603e4665294323fd20f6dcc0a984117026d1986704fa68f0379a", size = 12535, upload-time = "2025-08-25T18:44:54.164Z" }, +] + +[[package]] +name = "sphinx-rtd-theme" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "sphinx" }, + { name = "sphinxcontrib-jquery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/84/68/a1bfbf38c0f7bccc9b10bbf76b94606f64acb1552ae394f0b8285bfaea25/sphinx_rtd_theme-3.1.0.tar.gz", hash = "sha256:b44276f2c276e909239a4f6c955aa667aaafeb78597923b1c60babc76db78e4c", size = 7620915, upload-time = "2026-01-12T16:03:31.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/c7/b5c8015d823bfda1a346adb2c634a2101d50bb75d421eb6dcb31acd25ebc/sphinx_rtd_theme-3.1.0-py2.py3-none-any.whl", hash = "sha256:1785824ae8e6632060490f67cf3a72d404a85d2d9fc26bce3619944de5682b89", size = 7655617, upload-time = "2026-01-12T16:03:28.101Z" }, +] + [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" @@ -334,6 +405,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, ] +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sphinx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/f3/aa67467e051df70a6330fe7770894b3e4f09436dea6881ae0b4f3d87cad8/sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a", size = 122331, upload-time = "2023-03-14T15:01:01.944Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/85/749bd22d1a68db7291c89e2ebca53f4306c3f205853cf31e9de279034c3c/sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae", size = 121104, upload-time = "2023-03-14T15:01:00.356Z" }, +] + [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" @@ -361,6 +444,28 @@ 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 = "starlette" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/69/17425771797c36cded50b7fe44e850315d039f28b15901ab44839e70b593/starlette-1.0.0.tar.gz", hash = "sha256:6a4beaf1f81bb472fd19ea9b918b50dc3a77a6f2e190a12954b25e6ed5eea149", size = 2655289, upload-time = "2026-03-22T18:29:46.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/c9/584bc9651441b4ba60cc4d557d8a547b5aff901af35bda3a4ee30c819b82/starlette-1.0.0-py3-none-any.whl", hash = "sha256:d3ec55e0bb321692d275455ddfd3df75fff145d009685eb40dc91fc66b03d38b", size = 72651, upload-time = "2026-03-22T18:29:45.111Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + [[package]] name = "urllib3" version = "2.6.3" @@ -369,3 +474,131 @@ sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6 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 = "uvicorn" +version = "0.42.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e3/ad/4a96c425be6fb67e0621e62d86c402b4a17ab2be7f7c055d9bd2f638b9e2/uvicorn-0.42.0.tar.gz", hash = "sha256:9b1f190ce15a2dd22e7758651d9b6d12df09a13d51ba5bf4fc33c383a48e1775", size = 85393, upload-time = "2026-03-16T06:19:50.077Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/89/f8827ccff89c1586027a105e5630ff6139a64da2515e24dafe860bd9ae4d/uvicorn-0.42.0-py3-none-any.whl", hash = "sha256:96c30f5c7abe6f74ae8900a70e92b85ad6613b745d4879eb9b16ccad15645359", size = 68830, upload-time = "2026-03-16T06:19:48.325Z" }, +] + +[[package]] +name = "watchfiles" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" }, + { url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" }, + { url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" }, + { url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" }, + { url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" }, + { url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" }, + { url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" }, + { url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, + { url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" }, + { url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" }, + { url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" }, + { url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" }, + { url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" }, + { url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" }, + { url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" }, + { url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" }, + { url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" }, + { url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" }, + { url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" }, + { url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" }, + { url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" }, + { url = "https://files.pythonhosted.org/packages/51/2e/c410993ba5025a9f9357c376f48976ef0e1b1aefb73b97a5ae01a5972755/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5", size = 460845, upload-time = "2025-10-14T15:05:30.064Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a4/2df3b404469122e8680f0fcd06079317e48db58a2da2950fb45020947734/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3", size = 489027, upload-time = "2025-10-14T15:05:31.064Z" }, + { url = "https://files.pythonhosted.org/packages/ea/84/4587ba5b1f267167ee715b7f66e6382cca6938e0a4b870adad93e44747e6/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33", size = 595615, upload-time = "2025-10-14T15:05:32.074Z" }, + { url = "https://files.pythonhosted.org/packages/6a/0f/c6988c91d06e93cd0bb3d4a808bcf32375ca1904609835c3031799e3ecae/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510", size = 474836, upload-time = "2025-10-14T15:05:33.209Z" }, + { url = "https://files.pythonhosted.org/packages/b4/36/ded8aebea91919485b7bbabbd14f5f359326cb5ec218cd67074d1e426d74/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05", size = 455099, upload-time = "2025-10-14T15:05:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/98/e0/8c9bdba88af756a2fce230dd365fab2baf927ba42cd47521ee7498fd5211/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6", size = 630626, upload-time = "2025-10-14T15:05:35.216Z" }, + { url = "https://files.pythonhosted.org/packages/2a/84/a95db05354bf2d19e438520d92a8ca475e578c647f78f53197f5a2f17aaf/watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81", size = 622519, upload-time = "2025-10-14T15:05:36.259Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ce/d8acdc8de545de995c339be67711e474c77d643555a9bb74a9334252bd55/watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b", size = 272078, upload-time = "2025-10-14T15:05:37.63Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c9/a74487f72d0451524be827e8edec251da0cc1fcf111646a511ae752e1a3d/watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a", size = 287664, upload-time = "2025-10-14T15:05:38.95Z" }, + { url = "https://files.pythonhosted.org/packages/df/b8/8ac000702cdd496cdce998c6f4ee0ca1f15977bba51bdf07d872ebdfc34c/watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02", size = 277154, upload-time = "2025-10-14T15:05:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/47/a8/e3af2184707c29f0f14b1963c0aace6529f9d1b8582d5b99f31bbf42f59e/watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21", size = 403820, upload-time = "2025-10-14T15:05:40.932Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/e47e307c2f4bd75f9f9e8afbe3876679b18e1bcec449beca132a1c5ffb2d/watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5", size = 390510, upload-time = "2025-10-14T15:05:41.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a0/ad235642118090f66e7b2f18fd5c42082418404a79205cdfca50b6309c13/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7", size = 448408, upload-time = "2025-10-14T15:05:43.385Z" }, + { url = "https://files.pythonhosted.org/packages/df/85/97fa10fd5ff3332ae17e7e40e20784e419e28521549780869f1413742e9d/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101", size = 458968, upload-time = "2025-10-14T15:05:44.404Z" }, + { url = "https://files.pythonhosted.org/packages/47/c2/9059c2e8966ea5ce678166617a7f75ecba6164375f3b288e50a40dc6d489/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44", size = 488096, upload-time = "2025-10-14T15:05:45.398Z" }, + { url = "https://files.pythonhosted.org/packages/94/44/d90a9ec8ac309bc26db808a13e7bfc0e4e78b6fc051078a554e132e80160/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c", size = 596040, upload-time = "2025-10-14T15:05:46.502Z" }, + { url = "https://files.pythonhosted.org/packages/95/68/4e3479b20ca305cfc561db3ed207a8a1c745ee32bf24f2026a129d0ddb6e/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc", size = 473847, upload-time = "2025-10-14T15:05:47.484Z" }, + { url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" }, +] + +[[package]] +name = "websockets" +version = "16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, + { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, + { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" }, + { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" }, + { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" }, + { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" }, + { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, + { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" }, + { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, + { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" }, + { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, + { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, + { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" }, + { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" }, + { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, + { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, + { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, + { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +] From f585477014a5cee40eae9acdff1220fcca53693e Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 24 Mar 2026 21:58:46 -0400 Subject: [PATCH 11/40] restructure --- docs/source/fields.rst | 79 +++++++++++++++++++++++++++++ docs/source/fields/basic_fields.rst | 11 ---- docs/source/fields/index.rst | 53 ------------------- docs/source/globaltoc.rst | 4 +- docs/source/index.rst | 4 +- 5 files changed, 83 insertions(+), 68 deletions(-) create mode 100644 docs/source/fields.rst delete mode 100644 docs/source/fields/basic_fields.rst delete mode 100644 docs/source/fields/index.rst diff --git a/docs/source/fields.rst b/docs/source/fields.rst new file mode 100644 index 0000000..44874de --- /dev/null +++ b/docs/source/fields.rst @@ -0,0 +1,79 @@ +Fields +======= + +.. py:currentmodule:: comprehensiveconfig.spec + + + +Fields are the most basic unit in Comprehensive Config. They are used to define named values in your configuration file. + +For example- in toml: + +.. code-block:: TOML + + x = "foo" + +Module +***** + +.. py:data:: NoDefaultValue + :type: _NoDefaultValueT + + A sentinel value representing that no default value has been provided to some field + +.. py:class:: _NoDefaultValueT + + Non instantiable type for :py:const:`NoDefaultValue` + +.. py:class:: ConfigurationFieldMeta + + .. py:method:: __or__[S, T](self: S, value: Type[T] | T) -> S | Type[T]: + + Overwrite default type union behavior + +.. py:class:: BaseConfigurationField + :abstract: + + Abstract base class for all configuration fields + + .. py:method:: def _validate_value(self, value: Any, name: str | None = None, /): + :abstractmethod: + + The in-built validator for a given field. + + +.. py:class:: ConfigurationField[T](default_value: T | _NoDefaultValueT = NoDefaultValue, /, name: str | None = None, nullable: bool = False, doc: None | str = None, inline_doc: bool = True) + :abstract: + + Abstract base class for all configuration fields + + .. py:method:: def _validate_value(self, value: Any, name: str | None = None, /): + :abstractmethod: + + The in-built validator for a given field. + + .. py:attribute:: _holds + :type: T + + The type of data this field holds (used purely for annotation and IDE static type checking) + + .. py:attribute:: _name + :type: str + + The actual name used inside of your configuration file + + .. py:attribute:: _default_value + :type: T | NoDefaultValue + + The actual name used inside of your configuration file + + .. py:attribute:: _has_default + :type: bool + + .. py:attribute:: _nullable + :type: bool + +.. py:class:: comprehensiveconfig.spec.Integer(default_value=NoDefaultValue, **kwargs) + + .. py:attribute:: _holds + :type: int diff --git a/docs/source/fields/basic_fields.rst b/docs/source/fields/basic_fields.rst deleted file mode 100644 index edcad5f..0000000 --- a/docs/source/fields/basic_fields.rst +++ /dev/null @@ -1,11 +0,0 @@ -Basic Fields -=============== - -.. py:class:: comprehensiveconfig.spec.Integer(default_value=NoDefaultValue, **kwargs) - - .. py:attribute:: _holds - :type: int - - The type of data this field holds (used purely for annotation and IDE static type checking) - - .. py: diff --git a/docs/source/fields/index.rst b/docs/source/fields/index.rst deleted file mode 100644 index 5767783..0000000 --- a/docs/source/fields/index.rst +++ /dev/null @@ -1,53 +0,0 @@ -Fields -======= - -Fields are the most basic unit in Comprehensive Config. They are used to define named values in your configuration file. - -For example- in toml: - -.. code-block:: TOML - - x = "foo" - -Code -***** - -.. py:module:: comprehensiveconfig.spec - - .. py:data:: NoDefaultValue - :type: _NoDefaultValueT - - A sentinel value representing that no default value has been provided to some field - - .. py:class:: _NoDefaultValueT - - Non instantiable type for :py:const:`NoDefaultValue` - - .. py:class:: ConfigurationFieldMeta - - .. py:method:: __or__[S, T](self: S, value: Type[T] | T) -> S | Type[T]: - - Overwrite default type union behavior - - .. py:class:: BaseConfigurationField - :abstract: - - Abstract base class for all configuration fields - - .. py:method:: def _validate_value(self, value: Any, name: str | None = None, /): - :abstractmethod: - - The in-built validator for a given field. - - - .. py:class:: ConfigurationField[T](default_value: T | _NoDefaultValueT = NoDefaultValue, /, name: str | None = None, nullable: bool = False, doc: None | str = None, inline_doc: bool = True) - :abstract: - - Abstract base class for all configuration fields - - .. py:method:: def _validate_value(self, value: Any, name: str | None = None, /): - :abstractmethod: - - The in-built validator for a given field. - - \ No newline at end of file diff --git a/docs/source/globaltoc.rst b/docs/source/globaltoc.rst index aa8527c..f4ad070 100644 --- a/docs/source/globaltoc.rst +++ b/docs/source/globaltoc.rst @@ -9,5 +9,5 @@ :maxdepth: 2 :caption: Fields: - fields/index.rst - fields/basic_fields.rst \ No newline at end of file + fields.rst + fields/* \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 52a7cfa..3007f59 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -20,8 +20,8 @@ loading as well as complex validators for incoming configuration values. :maxdepth: 2 :caption: Fields: - fields/index.rst - fields/basic_fields.rst + fields.rst + fields/* Indices and tables ================== From 3ed0460e4faf738800388cf3b20b751794fabc10 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:20:30 -0400 Subject: [PATCH 12/40] setup rtd --- .readthedocs.yaml | 22 ++++++++++++++++++++++ docs/source/fields.rst | 12 ++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 .readthedocs.yaml diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..2c0ef8f --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,22 @@ +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version, and other tools you might need +build: + os: ubuntu-24.04 + tools: + python: "3.13" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + +# Optionally, but recommended, +# declare the Python requirements required to build your documentation +# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +# python: +# install: +# - requirements: docs/requirements.txt diff --git a/docs/source/fields.rst b/docs/source/fields.rst index 44874de..6b05bdd 100644 --- a/docs/source/fields.rst +++ b/docs/source/fields.rst @@ -73,6 +73,18 @@ Module .. py:attribute:: _nullable :type: bool + .. py:attribute:: doc + :type: str | None + :value: None + + A doc comment added for configuration formats that support it + + .. py:attribute:: inline_doc + :type: bool + :value: True + + Whether a doc comment should be rendered on the same line as the field (on formats that support comments) + .. py:class:: comprehensiveconfig.spec.Integer(default_value=NoDefaultValue, **kwargs) .. py:attribute:: _holds From 8ceeb9c673032503a7cf4ba1cfbf309809ca77e3 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:47:00 -0400 Subject: [PATCH 13/40] document large swaths of the spec --- comprehensiveconfig/spec.py | 62 +++++++++++++++++++++++-------------- docs/source/fields.rst | 52 +++++++++++++++++++++++++++++-- 2 files changed, 88 insertions(+), 26 deletions(-) diff --git a/comprehensiveconfig/spec.py b/comprehensiveconfig/spec.py index db366c3..2216aca 100644 --- a/comprehensiveconfig/spec.py +++ b/comprehensiveconfig/spec.py @@ -22,6 +22,7 @@ def __new__(cls, *args, **kwargs): class ConfigurationFieldMeta(type): """Provides custom union logic for configuration fields""" + def __or__[S, T](self: S, value: Type[T] | T) -> S | Type[T]: """broaden normal Union behavior so things don't break""" if not isinstance(value, type) and not isinstance( @@ -57,8 +58,8 @@ class BaseConfigurationField(ABC): """The python variable that this field is attached to""" _sorting_order: int - '''The sorting order for dumping the data. - This is important when sections are taken into account''' + """The sorting order for dumping the data. + This is important when sections are taken into account""" def __call__[T](self, value: T) -> T: self._validate_value(value) @@ -72,7 +73,14 @@ def _validate_value(self, value: Any, name: str | None = None, /): class ConfigurationField[T](BaseConfigurationField): """The base class for an inline configuration field""" - __slots__ = ("_name", "_default_value", "_has_default", "_nullable", "doc", "_inline_doc") + __slots__ = ( + "_name", + "_default_value", + "_has_default", + "_nullable", + "doc", + "_inline_doc", + ) _name: None | str """The actual name used inside the configuration @@ -82,10 +90,10 @@ class ConfigurationField[T](BaseConfigurationField): _nullable: bool """is this value nullable""" doc: str | None - '''Doc comment''' + """Doc comment""" inline_doc: bool - '''if a doc comment is present- should we try to put the doc - comment on the same line as the value?''' + """if a doc comment is present- should we try to put the doc + comment on the same line as the value?""" _holds: T """describes what type this field holds""" @@ -98,7 +106,7 @@ def __init__( name: str | None = None, nullable: bool = False, doc: None | str = None, - inline_doc: bool = True + inline_doc: bool = True, ): self._name = name self._nullable = nullable @@ -350,10 +358,10 @@ def __init__( self.inner_type = fix_unions(inner_type) return super().__init__(default_value, *args, **kwargs) - + def __call__(self, value: list[T]) -> list[T]: return [self.inner_type(val) for val in value] - + def __get__(self, instance, owner) -> list[T]: return super().__get__(instance, owner) @@ -427,7 +435,9 @@ def __init_subclass__(cls, name: str | None = None, **kwargs): } cls._FIELD_VAR_MAP = {value: key for key, value in cls._FIELD_NAME_MAP.items()} - cls._sorting_order = max(field._sorting_order for field in cls._ALL_FIELDS.values()) + cls._sorting_order = max( + field._sorting_order for field in cls._ALL_FIELDS.values() + ) # generate default value cls._cls_has_default = all( @@ -472,7 +482,7 @@ class Table[K, V](ConfigurationField): def __init__( self, - default_value: Any = NoDefaultValue, + default_value: dict[K, V] = {}, /, key_type: AnyConfigField | None = None, value_type: AnyConfigField | None = None, @@ -481,14 +491,15 @@ def __init__( ): self.key_type = fix_unions(key_type) self.value_type = fix_unions(value_type) - self._sorting_order = max(self.key_type._sorting_order, self.value_type._sorting_order) - + self._sorting_order = max( + self.key_type._sorting_order, self.value_type._sorting_order + ) return super().__init__(default_value, *args, **kwargs) def __call__(self, value: dict[K, V]) -> dict[K, V]: return {self.key_type(key): self.value_type(val) for key, val in value.items()} - + def __get__(self, instance, owner) -> dict[K, V]: return super().__get__(instance, owner) @@ -596,14 +607,16 @@ def __init__( super().__init__(NoDefaultValue, *args, **kwargs) self._left_type = fix_unions(left_type) self._right_type = fix_unions(right_type) - self._sorting_order = max(self._left_type._sorting_order, self._right_type._sorting_order) + self._sorting_order = max( + self._left_type._sorting_order, self._right_type._sorting_order + ) def __call__(self, *args, **kwargs): try: return self._left_type(*args, **kwargs) except ValueError: # if left side fails, try the right return self._right_type(*args, **kwargs) - + def __get__(self, instance, owner) -> L | R: return super().__get__(instance, owner) @@ -627,12 +640,12 @@ class ConfigEnum[T](ConfigurationField): _holds: T _enum: Type[T] - '''The enumeration type''' + """The enumeration type""" _enum_members_reversed: dict[Any, T] - '''A reversed mapping of values and enum variants in the enumeration type''' + """A reversed mapping of values and enum variants in the enumeration type""" _by_name: bool - '''whether or not the field value is using the - enum variants' name or value''' + """whether or not the field value is using the + enum variants' name or value""" def __init__( self, @@ -646,7 +659,9 @@ def __init__( self._enum = enum_type if not isinstance(enum_type, enum.EnumMeta): raise ValueError("Type must be an enumerator") - self._enum_members_reversed = {v.value: v for v in enum_type.__members__.values()} + self._enum_members_reversed = { + v.value: v for v in enum_type.__members__.values() + } self._by_name = by_name return super().__init__(default_value, *args, **kwargs) @@ -661,7 +676,7 @@ def get_value(self, value: Any): if value not in self._enum_members_reversed.keys(): raise ValueError(f"Invalid Enum Variant: {value}") return self._enum_members_reversed[value] - + def __call__(self, value: Any): return self.get_value(value) @@ -677,7 +692,6 @@ def _validate_value(self, value: Any, name: str | None = None, /): if isinstance(value, self._enum): super()._validate_value(value, name) super()._validate_value(self.get_value(value), name) - __all__ = [ @@ -692,5 +706,5 @@ def _validate_value(self, value: Any, name: str | None = None, /): "Table", "TableSpec", "List", - "ConfigEnum" + "ConfigEnum", ] diff --git a/docs/source/fields.rst b/docs/source/fields.rst index 6b05bdd..87ac476 100644 --- a/docs/source/fields.rst +++ b/docs/source/fields.rst @@ -14,7 +14,7 @@ For example- in toml: x = "foo" Module -***** +******* .. py:data:: NoDefaultValue :type: _NoDefaultValueT @@ -45,11 +45,21 @@ Module .. py:class:: ConfigurationField[T](default_value: T | _NoDefaultValueT = NoDefaultValue, /, name: str | None = None, nullable: bool = False, doc: None | str = None, inline_doc: bool = True) :abstract: + :param T | _NoDefaultValueT default_value: The default value used for this configuration file + :param str name: The name of the field in our configuration file + :param bool nullable: Defines if the value of the field can be :py:const:`None` + :param str | None doc: The documentation/comment for this field (only exported in config formats that support comments) + :param bool inline_doc: Defines if the doc comment is on the same line as the field + + Abstract base class for all configuration fields .. py:method:: def _validate_value(self, value: Any, name: str | None = None, /): :abstractmethod: + :param value: The value we are validating + :param name: The transformed name of the field (used in error messages) + The in-built validator for a given field. .. py:attribute:: _holds @@ -85,7 +95,45 @@ Module Whether a doc comment should be rendered on the same line as the field (on formats that support comments) -.. py:class:: comprehensiveconfig.spec.Integer(default_value=NoDefaultValue, **kwargs) +.. py:class:: comprehensiveconfig.spec.Integer(*args, **kwargs) .. py:attribute:: _holds :type: int + +.. py:class:: comprehensiveconfig.spec.Float(*args, **kwargs) + + .. py:attribute:: _holds + :type: float | int + +.. py:class:: comprehensiveconfig.spec.Text(*args, regex: str = r".*", **kwargs) + + :param str regex: Defines a regex pattern to validate against. Useful for email fields, ips, and other structured data. + + .. py:attribute:: _holds + :type: float | int + +.. py:class:: comprehensiveconfig.spec.List[T](default_value: list[T] = [], /, inner_type: AnyConfigField | None = None, **kwargs) + + :param list[T] default_value: The default value of the field. This always default to an empty list (required for static type checking) + :param AnyConfigField | None inner_type: The inner type used in the list. This is any field in the spec. This allows you to further validate the inner data. + + .. py:attribute:: _holds + :type: list[T] + + .. note:: + + Might require manual annotation if your default value remains an empty list + +.. py:class:: comprehensiveconfig.spec.Table[K, V](default_value: dict[K, V] = {}, /, key_type: AnyConfigField | None = None, value_type: AnyConfigField | None = None, **kwargs) + + :param list[T] default_value: The default value of the field. This always default to an empty dict (required for static type checking) + :param AnyConfigField | None key_type: The type of the keys used in the dict. This is any field in the spec. This allows you to further validate the inner data. + :param AnyConfigField | None value_type: The type of the values used in the dict. This is any field in the spec. This allows you to further validate the inner data. + + .. py:attribute:: _holds + :type: list[T] + + .. note:: + + Might require manual annotation if your default value remains an empty dict + From b8767446d1f58eec3431261e1186beecc93ef05c Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:47:57 -0400 Subject: [PATCH 14/40] fix configuration conf.py --- .readthedocs.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 2c0ef8f..76369df 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -12,7 +12,7 @@ build: # Build documentation in the "docs/" directory with Sphinx sphinx: - configuration: docs/conf.py + configuration: docs/source/conf.py # Optionally, but recommended, # declare the Python requirements required to build your documentation From 631dac2caa744d6fe3862e7eb2373079ea7b89ea Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:49:31 -0400 Subject: [PATCH 15/40] add documentation requirements --- .readthedocs.yaml | 6 +++--- docs/requirements.txt | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 docs/requirements.txt diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 76369df..b405f06 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -17,6 +17,6 @@ sphinx: # Optionally, but recommended, # declare the Python requirements required to build your documentation # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html -# python: -# install: -# - requirements: docs/requirements.txt +python: + install: + - requirements: docs/requirements.txt diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..e28b5eb --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1 @@ +sphinx-rtd-theme>=3.1.0 \ No newline at end of file From 89b78be7322ff45398743037f15c4ec5ffb256eb Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 24 Mar 2026 22:54:00 -0400 Subject: [PATCH 16/40] increase toc depth --- docs/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 3007f59..9633ae3 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -17,7 +17,7 @@ loading as well as complex validators for incoming configuration values. .. toctree:: :glob: - :maxdepth: 2 + :maxdepth: 3 :caption: Fields: fields.rst From 15d7e22cc64e13af38b67676c6e02c2d24d48e79 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 24 Mar 2026 23:07:27 -0400 Subject: [PATCH 17/40] automatic versioning to make `uv bump` more useful --- docs/source/conf.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 64aaa45..d7cc19f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,3 +1,6 @@ +import os +import tomllib + # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: @@ -9,7 +12,11 @@ project = "Comprehensive Config" copyright = "2026, Summersweet Software" author = "Summersweet Software" -release = "1.0.1" + +# Automatic versioning +with open(f"{os.getcwd()}/../../pyproject.toml", "r") as f: + pyproj = tomllib.loads(f.read()) + release = pyproj["project"]["version"] # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration From d4ee7d1a7ec585337fca7df8bc002b91cc67a5d3 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:05:57 -0400 Subject: [PATCH 18/40] Finish docs for spec module --- docs/source/fields.rst | 53 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/docs/source/fields.rst b/docs/source/fields.rst index 87ac476..6ee2018 100644 --- a/docs/source/fields.rst +++ b/docs/source/fields.rst @@ -50,7 +50,7 @@ Module :param bool nullable: Defines if the value of the field can be :py:const:`None` :param str | None doc: The documentation/comment for this field (only exported in config formats that support comments) :param bool inline_doc: Defines if the doc comment is on the same line as the field - + Abstract base class for all configuration fields @@ -137,3 +137,54 @@ Module Might require manual annotation if your default value remains an empty dict +.. py:class:: comprehensiveconfig.spec.TableSpec(cls, name: str | None = None, **kwargs) + + .. important:: + This is meant to only be used as a baseclass. The arguments provided above are for subclassing + usage: + + + .. code-block:: python + + class MySpec(TableSpec, name="Something"): + '''Instantiate to create a field''' + pass + + .. py:method:: __init__(self, default_value: dict | NoDefaultValue = NoDefaultValue, /, *args, **kwargs) + + :param dict | NoDefaultValue default_value: Default Value for a field of this type. + + +.. py:class:: comprehensiveconfig.spec.ConfigUnion[L, R](left_type: AnyConfigField | Type, right_type: AnyConfigField | Type, *args, **kwargs,) + + .. important:: + This should not be instantiated directly! Instead do the following: + + + .. code-block:: python + + Integer() | Text(regex="...") + + Also important to note that validation is done Left-to-Right. That means it will always try to match against the left-most types first + + .. py:attribute:: _holds + :type: L | R + +.. py:class:: comprehensiveconfig.spec.ConfigEnum[T](enum_type: Type[T], default_value: T | _NoDefaultValueT = NoDefaultValue, /, *args, by_name=False, **kwargs) + + :param Type[T] enum_type: The class of the enum we want to represent in this field. + :param T | NoDefaultValue default_value: Default value of our field + :param bool by_name: Choose whether or not we should have variants use their value's or their name's when we validate configuration. + + This is a way to use an existing python enum (:py:class:`enum.Enum`) as a validated field. + + .. py:attribute:: _holds + :type: T + + .. py:attribute:: _enum_type + :type: Type[T] + + .. py:attribute:: _enum_members_reversed + :type: dict[Any, T] + + A reversed mapping of values and enum variants (instances) in the enumeration type From fdcb7ab0ba50c5dad2b748343fcd52f2c1d61c97 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:39:09 -0400 Subject: [PATCH 19/40] writer documentation --- comprehensiveconfig/configio.py | 4 +- comprehensiveconfig/toml.py | 2 +- docs/source/globaltoc.rst | 16 +++++-- docs/source/index.rst | 11 ++++- docs/source/json_writer.rst | 63 ++++++++++++++++++++++++++++ docs/source/toml_writer.rst | 74 +++++++++++++++++++++++++++++++++ docs/source/writers.rst | 60 ++++++++++++++++++++++++++ 7 files changed, 222 insertions(+), 8 deletions(-) create mode 100644 docs/source/json_writer.rst create mode 100644 docs/source/toml_writer.rst create mode 100644 docs/source/writers.rst diff --git a/comprehensiveconfig/configio.py b/comprehensiveconfig/configio.py index 266aac4..3c68ee0 100644 --- a/comprehensiveconfig/configio.py +++ b/comprehensiveconfig/configio.py @@ -27,8 +27,8 @@ def dump(cls, file, node: spec.AnyConfigField): @classmethod @abstractmethod - def loads(cls, file) -> dict[str, Any]: - """load a file by name""" + def loads(cls, data: str) -> dict[str, Any]: + """load configuration string""" pass @classmethod diff --git a/comprehensiveconfig/toml.py b/comprehensiveconfig/toml.py index dc62bb4..8cb1c0f 100644 --- a/comprehensiveconfig/toml.py +++ b/comprehensiveconfig/toml.py @@ -67,7 +67,7 @@ def dump_section(cls, node) -> list: ] @classmethod - def format_value(cls, value): + def format_value(cls, value) -> str: match value: case int() | float(): return str(value) diff --git a/docs/source/globaltoc.rst b/docs/source/globaltoc.rst index f4ad070..3f4a6d5 100644 --- a/docs/source/globaltoc.rst +++ b/docs/source/globaltoc.rst @@ -5,9 +5,17 @@ index.rst .. toctree:: - :glob: - :maxdepth: 2 - :caption: Fields: + :glob: + :maxdepth: 2 + :caption: Fields: fields.rst - fields/* \ No newline at end of file + +.. toctree:: + :glob: + :maxdepth: 2 + :caption: Writers: + + writers.rst + json_writer.rst + toml_writer.rst \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 9633ae3..14106e1 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -21,7 +21,16 @@ loading as well as complex validators for incoming configuration values. :caption: Fields: fields.rst - fields/* + + +.. toctree:: + :glob: + :maxdepth: 3 + :caption: Writers: + + writers.rst + json_writer.rst + toml_writer.rst Indices and tables ================== diff --git a/docs/source/json_writer.rst b/docs/source/json_writer.rst new file mode 100644 index 0000000..36cf39e --- /dev/null +++ b/docs/source/json_writer.rst @@ -0,0 +1,63 @@ +Json Writer +============= + +.. py:currentmodule:: comprehensiveconfig.json + + +This ConfigWriter adds Json export and import support. + +Module +******** + +.. py:class:: JsonWriter + + .. py:classmethod:: dumps(cls, node) -> str + + :param AnyConfigField node: The configuration object we are writing. + + Dumps a string output from a given config + + .. py:classmethod:: dump(cls, file, node) -> str + + :param file: The file name or object we are writing to. + :param AnyConfigField node: The configuration object we are writing. + + Dumps a to a configuration section to a given file + + .. py:classmethod:: loads(cls, data: str) + + :param str data: The configuration object we are writing. + + Load the given configuration string as + + .. :important:: + + Output is not validated. This is done by the configuration fields themselves. + This should not be used directly unless necessary. + + .. py:classmethod:: load(cls, file) + + :param file: The file name or object we are writing to. + + Load a given file and create a configuration object + + .. :important:: + + Output is not validated. This is done by the configuration fields themselves. + This should not be used directly unless necessary. + + Private Methods + ----------------- + + .. py:classmethod:: dump_section(cls, node: spec.Section) + + :param node: The section we are dumping + + dump a section into json format. Running through each k/v pair and ensuring properly json serializable. + + .. py:classmethod:: dump_value(cls, node: spec.AnyConfigField, value) + + :param node: The node of the value we are dumping + :param value: The value of the node we are dumping. + + Dump a field value as json serializable object. \ No newline at end of file diff --git a/docs/source/toml_writer.rst b/docs/source/toml_writer.rst new file mode 100644 index 0000000..d0ed933 --- /dev/null +++ b/docs/source/toml_writer.rst @@ -0,0 +1,74 @@ +Toml Writer +============= + +.. py:currentmodule:: comprehensiveconfig.toml + + +This ConfigWriter adds Toml export and import support. + +Module +******** + +.. py:class:: TomlWriter + + .. py:classmethod:: dumps(cls, node) -> str + + :param AnyConfigField node: The configuration object we are writing. + + Dumps a string output from a given config + + .. py:classmethod:: dump(cls, file, node) -> str + + :param file: The file name or object we are writing to. + :param AnyConfigField node: The configuration object we are writing. + + Dumps a to a configuration section to a given file + + .. py:classmethod:: loads(cls, data: str) + + :param str data: The configuration object we are writing. + + Load the given configuration string as + + .. :important:: + + Output is not validated. This is done by the configuration fields themselves. + This should not be used directly unless necessary. + + .. py:classmethod:: load(cls, file) + + :param file: The file name or object we are writing to. + + Load a given file and create a configuration object + + .. :important:: + + Output is not validated. This is done by the configuration fields themselves. + This should not be used directly unless necessary. + + Private Methods + ----------------- + + .. py:classmethod:: dump_section(cls, node) -> list + + :meta private: + :param node: The :py:class:`comprehensiveconfig.spec.Section` instance being dumped + + Dumps a section to a list of lines to write + + .. py:classmethod:: format_value(cls, value) -> str + + :meta private: + :param value: The object we are formatting into valid toml. + + Dump a value or node into the appropriate formatting for toml. + + .. py:classmethod:: dump_field(cls, node: spec.AnyConfigField, original_name: str, field_name: str, value) -> str + + :meta private: + :param AnyConfigField node: The parent node containing this field + :param str original_name: Name of the python class attribute attributed to the field + :param str field_name: The true field name being used (this could be mutated) + :param value: The value of this field + + Dump a field into valid toml diff --git a/docs/source/writers.rst b/docs/source/writers.rst new file mode 100644 index 0000000..306a2d8 --- /dev/null +++ b/docs/source/writers.rst @@ -0,0 +1,60 @@ +Config Writers +================ + +.. py:currentmodule:: comprehensiveconfig.configio + + + +Config writers are what ComprehensiveConfig uses to load and dump configuration objects. +These are a basic interface that can be extended and used for any format! + + + +Module +******** + +.. py:class:: ConfigurationWriter + :abstract: + + .. py:method:: dumps(cls, node: spec.AnyConfigField) -> str + :abstract: + :classmethod: + + :param AnyConfigField node: The configuration object we are writing. + + Dumps a string output from a given config + + .. py:method:: dump(cls, file, node: spec.AnyConfigField) + :abstract: + :classmethod: + + :param file: The file name or object we are writing to. + :param AnyConfigField node: The configuration object we are writing. + + Dumps a to a configuration section to a given file + + .. py:method:: loads(cls, data: str) -> dict[str, Any] + :abstract: + :classmethod: + + :param str data: The configuration object we are writing. + + Load the given configuration string as + + .. :important:: + + Output is not validated. This is done by the configuration fields themselves. + This should not be used directly unless necessary. + + .. py:method:: load(cls, file) -> dict[str, Any] + :abstract: + :classmethod: + + :param file: The file name or object we are writing to. + + Load a given file and create a configuration object + + .. :important:: + + Output is not validated. This is done by the configuration fields themselves. + This should not be used directly unless necessary. \ No newline at end of file From d822faaff1bdb2d7bc7349bf72846a4261e04788 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:49:43 -0400 Subject: [PATCH 20/40] Finish Documentation --- docs/source/index.rst | 52 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/docs/source/index.rst b/docs/source/index.rst index 14106e1..45868c0 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,6 +1,9 @@ Comprehensive Config ===================== +.. py:currentmodule:: comprehensiveconfig.json + + Comprehensive config is a python-based configuration library that aim to be extraordinarily pythonic and easy to use. It takes heavy inspiration from pydantic models. @@ -32,6 +35,55 @@ loading as well as complex validators for incoming configuration values. json_writer.rst toml_writer.rst +Module +******** + +.. py:class:: _ConfigSpecMeta(cls, name, bases, attrs, default_file: str | None = None, writer=None, create_file: bool = False, auto_load: bool = True, **kwargs,) + + Used to automatically load config and overwrite get/set attribute methods to allow direct access to data. + + .. py:attribute:: _WRITER + :type: Type[configio.ConfigurationWriter] | None + + .. py:attribute:: _DEFAULT_FILE + :type: str | None + + .. py:attribute:: _AUTO_LOAD + :type: bool + + .. py:attribute:: _CREATE_FILE + :type: bool + + .. py:attribute:: _INST + :type: Union["ConfigSpec", None] + +.. py:class:: _ConfigSpecABCMeta + + Combines :py:class:`ABCMeta` and :py:class:`_ConfigSpecMeta` + +.. py:class:: ConfigSpec(cls, **kwargs) + + .. important:: + + Not meant to be used directly: instead subclass and pass :py:class:`_ConfigSpecMeta` keyword arguments. + + .. code-block:: python + + class MyConfig(ConfigSpec, writer=JsonWriter, auto_load=False, ...): + pass # fields here + + .. py:classmethod:: load(cls, file=None, writer=None, /) -> Self + + Load a specified file (or load default file with default writer) + + .. py:method:: save(self, file=None, writer=None, /) + + Save a specified file (or save default file with default writer) + + .. py:method:: reset(self) + + Reset configuration using default values of all fields + Indices and tables ================== From f4c7590f652b02c22320a650146d9a512b7445e2 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:09:39 -0400 Subject: [PATCH 21/40] add Section docs --- docs/source/fields.rst | 49 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/docs/source/fields.rst b/docs/source/fields.rst index 6ee2018..b90db67 100644 --- a/docs/source/fields.rst +++ b/docs/source/fields.rst @@ -112,6 +112,7 @@ Module .. py:attribute:: _holds :type: float | int + .. py:class:: comprehensiveconfig.spec.List[T](default_value: list[T] = [], /, inner_type: AnyConfigField | None = None, **kwargs) :param list[T] default_value: The default value of the field. This always default to an empty list (required for static type checking) @@ -154,6 +155,54 @@ Module :param dict | NoDefaultValue default_value: Default Value for a field of this type. +.. py:class:: comprehensiveconfig.spec.Section(cls, name: str | None = None, **kwargs) + + .. important:: + This is meant to only be used as a baseclass. The arguments provided above are for subclassing + usage: + + + .. code-block:: python + + class SomeSection(Section, name="Something"): + pass + + .. py:attribute::_FIELDS + :type: dict[str, AnyConfigField] + + .. py:attribute::_SECTIONS + :type: dict[str, Type] + + .. py:attribute::_ALL_FIELDS + :type: dict[str, AnyConfigField | Type] + + .. py:attribute::_FIELD_NAME_MAP + :type: dict[str, str] + + .. py:attribute::_FIELD_VAR_MAP + :type: dict[str, str] + + .. py:attribute::_cls_name + :type: str + + .. py:attribute::_instance_name + :type: str + + .. py:attribute::_has_default + :type: bool + + .. py:attribute::_default_value + :type: dict[str, Any] | _NoDefaultValueT + + .. py:attribute::_parent + :type: SectionParent + + .. py:attribute::_instance_parent + :type: AnyConfigField | None + + .. py:attribute::_cls_parent + :type: AnyConfigField | None + .. py:class:: comprehensiveconfig.spec.ConfigUnion[L, R](left_type: AnyConfigField | Type, right_type: AnyConfigField | Type, *args, **kwargs,) From 20bc6d09e1939b0375d0b501936bd16da2211112 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:15:06 -0400 Subject: [PATCH 22/40] Fix formatting --- docs/source/fields.rst | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/source/fields.rst b/docs/source/fields.rst index b90db67..2ed832d 100644 --- a/docs/source/fields.rst +++ b/docs/source/fields.rst @@ -167,40 +167,40 @@ Module class SomeSection(Section, name="Something"): pass - .. py:attribute::_FIELDS + .. py:attribute:: _FIELDS :type: dict[str, AnyConfigField] - .. py:attribute::_SECTIONS + .. py:attribute:: _SECTIONS :type: dict[str, Type] - .. py:attribute::_ALL_FIELDS + .. py:attribute:: _ALL_FIELDS :type: dict[str, AnyConfigField | Type] - .. py:attribute::_FIELD_NAME_MAP + .. py:attribute:: _FIELD_NAME_MAP :type: dict[str, str] - .. py:attribute::_FIELD_VAR_MAP + .. py:attribute:: _FIELD_VAR_MAP :type: dict[str, str] - .. py:attribute::_cls_name + .. py:attribute:: _cls_name :type: str - .. py:attribute::_instance_name + .. py:attribute:: _instance_name :type: str - .. py:attribute::_has_default + .. py:attribute:: _has_default :type: bool - .. py:attribute::_default_value + .. py:attribute:: _default_value :type: dict[str, Any] | _NoDefaultValueT - .. py:attribute::_parent + .. py:attribute:: _parent :type: SectionParent - .. py:attribute::_instance_parent + .. py:attribute:: _instance_parent :type: AnyConfigField | None - .. py:attribute::_cls_parent + .. py:attribute:: _cls_parent :type: AnyConfigField | None From 5697390b24000e52bdd8fe9940221659791729d3 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:37:00 -0400 Subject: [PATCH 23/40] fix typos --- docs/source/fields.rst | 14 +++++++------- docs/source/index.rst | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/source/fields.rst b/docs/source/fields.rst index 2ed832d..2b1b18a 100644 --- a/docs/source/fields.rst +++ b/docs/source/fields.rst @@ -27,7 +27,7 @@ Module .. py:class:: ConfigurationFieldMeta - .. py:method:: __or__[S, T](self: S, value: Type[T] | T) -> S | Type[T]: + .. py:method:: __or__[S, T](value: Type[T] | T) -> S | Type[T] Overwrite default type union behavior @@ -36,7 +36,7 @@ Module Abstract base class for all configuration fields - .. py:method:: def _validate_value(self, value: Any, name: str | None = None, /): + .. py:method:: _validate_value(value: Any, name: str | None = None, /) :abstractmethod: The in-built validator for a given field. @@ -54,7 +54,7 @@ Module Abstract base class for all configuration fields - .. py:method:: def _validate_value(self, value: Any, name: str | None = None, /): + .. py:method:: _validate_value(value: Any, name: str | None = None, /) :abstractmethod: :param value: The value we are validating @@ -110,7 +110,7 @@ Module :param str regex: Defines a regex pattern to validate against. Useful for email fields, ips, and other structured data. .. py:attribute:: _holds - :type: float | int + :type: str .. py:class:: comprehensiveconfig.spec.List[T](default_value: list[T] = [], /, inner_type: AnyConfigField | None = None, **kwargs) @@ -127,12 +127,12 @@ Module .. py:class:: comprehensiveconfig.spec.Table[K, V](default_value: dict[K, V] = {}, /, key_type: AnyConfigField | None = None, value_type: AnyConfigField | None = None, **kwargs) - :param list[T] default_value: The default value of the field. This always default to an empty dict (required for static type checking) + :param dict[K, V] default_value: The default value of the field. This always default to an empty dict (required for static type checking) :param AnyConfigField | None key_type: The type of the keys used in the dict. This is any field in the spec. This allows you to further validate the inner data. :param AnyConfigField | None value_type: The type of the values used in the dict. This is any field in the spec. This allows you to further validate the inner data. .. py:attribute:: _holds - :type: list[T] + :type: dict[K, V] .. note:: @@ -151,7 +151,7 @@ Module '''Instantiate to create a field''' pass - .. py:method:: __init__(self, default_value: dict | NoDefaultValue = NoDefaultValue, /, *args, **kwargs) + .. py:method:: __init__(default_value: dict | NoDefaultValue = NoDefaultValue, /, *args, **kwargs) :param dict | NoDefaultValue default_value: Default Value for a field of this type. diff --git a/docs/source/index.rst b/docs/source/index.rst index 45868c0..eea4aee 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -4,7 +4,7 @@ Comprehensive Config .. py:currentmodule:: comprehensiveconfig.json -Comprehensive config is a python-based configuration library that aim to be extraordinarily pythonic and easy to use. +Comprehensive config is a python configuration library that aims to be extraordinarily pythonic and easy to use. It takes heavy inspiration from pydantic models. Comprehensive config includes automatic config file creation and/or From 0f96fcf5194e4bdfe22b5c7f73afc0a7b941aca4 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:45:29 -0400 Subject: [PATCH 24/40] fix typo --- docs/source/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index eea4aee..17bc1c8 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -76,11 +76,11 @@ Module Load a specified file (or load default file with default writer) - .. py:method:: save(self, file=None, writer=None, /) + .. py:method:: save(file=None, writer=None, /) Save a specified file (or save default file with default writer) - .. py:method:: reset(self) + .. py:method:: reset() Reset configuration using default values of all fields From d13df6cd865e85c8e30213ab4537a8e6f57a1af2 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Wed, 25 Mar 2026 20:03:57 -0400 Subject: [PATCH 25/40] Add documentation url to toml --- pyproject.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1c625cb..6ac3569 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ keywords = [ "json", "json-writer", "type-annotated", - "type-annotations" + "type-annotations", ] classifiers = [ # How mature is this project? Common values are @@ -43,7 +43,7 @@ classifiers = [ "License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)", "Operating System :: OS Independent", "Natural Language :: English", - "Typing :: Typed" + "Typing :: Typed", ] [project.urls] @@ -51,6 +51,7 @@ Homepage = "https://summersweet.software" # Documentation = "https://readthedocs.org" # Not yet created Repository = "https://github.com/summersweet-software/comprehensiveconfig" Issues = "https://github.com/summersweet-software/comprehensiveconfig/issues" +Documentation = "https://comprehensiveconfig.readthedocs.io/en/" [dependency-groups] dev = [ From f6c4394f9f3bfac1e7f24559ecff0b14daab9257 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Wed, 25 Mar 2026 20:06:35 -0400 Subject: [PATCH 26/40] be a little smarter next time, Abby --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6ac3569..6d345bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,6 @@ classifiers = [ [project.urls] Homepage = "https://summersweet.software" -# Documentation = "https://readthedocs.org" # Not yet created Repository = "https://github.com/summersweet-software/comprehensiveconfig" Issues = "https://github.com/summersweet-software/comprehensiveconfig/issues" Documentation = "https://comprehensiveconfig.readthedocs.io/en/" From 62b8b686bb41e6082d1a5d9a4688d117052e252e Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:49:41 -0400 Subject: [PATCH 27/40] make tests use all available config writers + add more tests --- comprehensiveconfig/spec.py | 2 + tests/conftest.py | 10 +- tests/test_fields.py | 190 ++++++++++++++++++++++++++++++------ uv.lock | 159 ++++++++++++++++++++++++++++++ 4 files changed, 330 insertions(+), 31 deletions(-) diff --git a/comprehensiveconfig/spec.py b/comprehensiveconfig/spec.py index 6e08573..5575751 100644 --- a/comprehensiveconfig/spec.py +++ b/comprehensiveconfig/spec.py @@ -137,6 +137,8 @@ def __get__(self, instance, owner) -> T: return instance._value[self._field_variable] def __set__(self, instance, value: T): + if self._field_variable not in instance._value.keys(): + raise KeyError(self._field_variable) instance._value[self._field_variable] = value diff --git a/tests/conftest.py b/tests/conftest.py index d1e83af..591e04b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,13 +2,21 @@ import pytest +from comprehensiveconfig.json import JsonWriter +from comprehensiveconfig.toml import TomlWriter + OUTPUT_DIR = "tests/output_files/" @pytest.fixture(autouse=True, scope="function") -def managed_context(): +def managed_context(filename, writer): """Manages our files per test""" os.makedirs(OUTPUT_DIR, exist_ok=True) yield for file in os.listdir(OUTPUT_DIR): os.remove(OUTPUT_DIR + "/" + file) + + +toml_config = OUTPUT_DIR + "/test.toml", TomlWriter +json_config = OUTPUT_DIR + "/test.json", JsonWriter +parameterize_values = [toml_config, json_config] diff --git a/tests/test_fields.py b/tests/test_fields.py index 4745741..b35e9bb 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,8 +1,13 @@ +import enum + +import pytest + import comprehensiveconfig -from tests.conftest import OUTPUT_DIR +from tests.conftest import OUTPUT_DIR, parameterize_values, toml_config -def test_blank_spec_create(): +@pytest.mark.parametrize(("filename", "writer"), parameterize_values) +def test_blank_spec_create(filename, writer): class Foo(comprehensiveconfig.ConfigSpec, auto_load=False): """Basic config""" @@ -11,7 +16,8 @@ class Foo(comprehensiveconfig.ConfigSpec, auto_load=False): assert True -def test_blank_spec_create_auto_load_failure(): +@pytest.mark.parametrize(("filename", "writer"), parameterize_values) +def test_blank_spec_create_auto_load_failure(filename, writer): try: class Foo(comprehensiveconfig.ConfigSpec, auto_load=True): @@ -25,12 +31,13 @@ class Foo(comprehensiveconfig.ConfigSpec, auto_load=True): assert True -def test_blank_spec_create_auto_load_working(): +@pytest.mark.parametrize(("filename", "writer"), parameterize_values) +def test_blank_spec_create_auto_load_working(filename, writer): class Foo( comprehensiveconfig.ConfigSpec, auto_load=True, - writer=comprehensiveconfig.toml.TomlWriter, - default_file=OUTPUT_DIR + "/test.toml", + writer=writer, + default_file=filename, create_file=True, ): """Basic config""" @@ -40,12 +47,13 @@ class Foo( assert True -def test_int_field_auto_create(): +@pytest.mark.parametrize(("filename", "writer"), parameterize_values) +def test_int_field_auto_create(filename, writer): class Foo( comprehensiveconfig.ConfigSpec, auto_load=True, - writer=comprehensiveconfig.toml.TomlWriter, - default_file=OUTPUT_DIR + "/test.toml", + writer=writer, + default_file=filename, create_file=True, ): """Basic config""" @@ -63,12 +71,13 @@ class Foo( assert Foo.test == 11 -def test_float_field_auto_create(): +@pytest.mark.parametrize(("filename", "writer"), parameterize_values) +def test_float_field_auto_create(filename, writer): class Foo( comprehensiveconfig.ConfigSpec, auto_load=True, - writer=comprehensiveconfig.toml.TomlWriter, - default_file=OUTPUT_DIR + "/test.toml", + writer=writer, + default_file=filename, create_file=True, ): """Basic config""" @@ -86,12 +95,13 @@ class Foo( assert Foo.test == 11 -def test_text_field_auto_create(): +@pytest.mark.parametrize(("filename", "writer"), parameterize_values) +def test_text_field_auto_create(filename, writer): class Foo( comprehensiveconfig.ConfigSpec, auto_load=True, - writer=comprehensiveconfig.toml.TomlWriter, - default_file=OUTPUT_DIR + "/test.toml", + writer=writer, + default_file=filename, create_file=True, ): """Basic config""" @@ -109,12 +119,13 @@ class Foo( assert Foo.test == "bean" -def test_list_field_auto_create(): +@pytest.mark.parametrize(("filename", "writer"), parameterize_values) +def test_list_field_auto_create(filename, writer): class Foo( comprehensiveconfig.ConfigSpec, auto_load=True, - writer=comprehensiveconfig.toml.TomlWriter, - default_file=OUTPUT_DIR + "/test.toml", + writer=writer, + default_file=filename, create_file=True, ): """Basic config""" @@ -140,12 +151,13 @@ class Foo( assert Foo.test == ["fee", "fi", "foh"] -def test_table_field_auto_create(): +@pytest.mark.parametrize(("filename", "writer"), parameterize_values) +def test_table_field_auto_create(filename, writer): class Foo( comprehensiveconfig.ConfigSpec, auto_load=True, - writer=comprehensiveconfig.toml.TomlWriter, - default_file=OUTPUT_DIR + "/test.toml", + writer=writer, + default_file=filename, create_file=True, ): """Basic config""" @@ -173,12 +185,13 @@ class Foo( assert Foo.test == {"x": 12, "y": 23, "we": 123} -def test_section_empty_field_auto_create(): +@pytest.mark.parametrize(("filename", "writer"), parameterize_values) +def test_section_empty_field_auto_create(filename, writer): class Foo( comprehensiveconfig.ConfigSpec, auto_load=True, - writer=comprehensiveconfig.toml.TomlWriter, - default_file=OUTPUT_DIR + "/test.toml", + writer=writer, + default_file=filename, create_file=True, ): """Basic config""" @@ -189,12 +202,13 @@ class Bar(comprehensiveconfig.spec.Section): assert isinstance(Foo.Bar, Foo.Bar.__class__) -def test_section_empty_w_name_field_auto_create(): +@pytest.mark.parametrize(("filename", "writer"), parameterize_values) +def test_section_empty_w_name_field_auto_create(filename, writer): class Foo( comprehensiveconfig.ConfigSpec, auto_load=True, - writer=comprehensiveconfig.toml.TomlWriter, - default_file=OUTPUT_DIR + "/test.toml", + writer=writer, + default_file=filename, create_file=True, ): """Basic config""" @@ -206,12 +220,13 @@ class Bar(comprehensiveconfig.spec.Section, name="burger"): assert isinstance(Foo.Bar, Foo.Bar.__class__) -def test_section_w_field_w_name_field_auto_create(): +@pytest.mark.parametrize(("filename", "writer"), parameterize_values) +def test_section_w_field_w_name_field_auto_create(filename, writer): class Foo( comprehensiveconfig.ConfigSpec, auto_load=True, - writer=comprehensiveconfig.toml.TomlWriter, - default_file=OUTPUT_DIR + "/test.toml", + writer=writer, + default_file=filename, create_file=True, ): """Basic config""" @@ -231,3 +246,118 @@ class Bar(comprehensiveconfig.spec.Section, name="burger"): assert False except ValueError: assert Foo.Bar.test == "bean" + + +@pytest.mark.parametrize(("filename", "writer"), parameterize_values) +def test_enum(filename, writer): + class ExampleEnum(enum.Enum): + example = 10 + something_cool = 20 + + class Foo( + comprehensiveconfig.ConfigSpec, + auto_load=True, + writer=writer, + default_file=filename, + create_file=True, + ): + """Basic config""" + + bar = comprehensiveconfig.spec.ConfigEnum(ExampleEnum, ExampleEnum.example) + baz = comprehensiveconfig.spec.ConfigEnum( + ExampleEnum, ExampleEnum.example, by_name=True + ) + + assert Foo.bar == ExampleEnum.example + Foo.bar = ExampleEnum.something_cool + assert Foo.bar == ExampleEnum.something_cool + + assert Foo.baz == ExampleEnum.example + Foo.baz = ExampleEnum.something_cool + assert Foo.baz == ExampleEnum.something_cool + + try: + Foo.bar = 12.20 + assert False + except ValueError: + assert Foo.bar == ExampleEnum.something_cool + + try: + Foo.baz = 12.20 + assert False + except ValueError: + assert Foo.baz == ExampleEnum.something_cool + + +@pytest.mark.parametrize(("filename", "writer"), [toml_config]) +def test_comments_appear_in_output(filename: str, writer): + class Foo( + comprehensiveconfig.ConfigSpec, + auto_load=True, + writer=writer, + default_file=filename, + create_file=True, + ): + """Basic config""" + + class Bar(comprehensiveconfig.spec.Section, name="burger"): + """Section comment example""" + + test = comprehensiveconfig.spec.Text("clean", doc="Example doc 1") + + test2 = comprehensiveconfig.spec.Text( + "clean", doc="Example doc 2", inline_doc=False + ) + + toml_output: str = writer.dumps(Foo._INST) + assert toml_output + + assert toml_output.startswith("# ") + assert "[burger]\n# Section comment example\n" in toml_output + assert "\n# Example doc 2\n" in toml_output # ensure this exists and is not inlined + assert '"clean" # Example doc 1' in toml_output # ensure exists and *is* inlined + + +@pytest.mark.parametrize(("filename", "writer"), [toml_config]) +def test_trailing_spaces(filename: str, writer): + """ensure final outputs have NO trailing spaces""" + + class ExampleEnum(enum.Enum): + example = 10 + something_cool = 20 + + class Foo( + comprehensiveconfig.ConfigSpec, + auto_load=True, + writer=writer, + default_file=filename, + create_file=True, + ): + """Basic config""" + + class Bar(comprehensiveconfig.spec.Section, name="burger"): + """Section comment example""" + + test_section_item = comprehensiveconfig.spec.Text( + "clean", doc="Example doc 1" + ) + + test_text = comprehensiveconfig.spec.Text( + "clean", doc="Example doc 2", inline_doc=False + ) + test_int = comprehensiveconfig.spec.Integer(20) + test_float = comprehensiveconfig.spec.Float(20.20) + test_dict = comprehensiveconfig.spec.Table( + {10: "burgers"}, + key_type=comprehensiveconfig.spec.Integer(), + value_type=comprehensiveconfig.spec.Text(), + ) + test_list = comprehensiveconfig.spec.List( + [10, 20, 30], inner_type=comprehensiveconfig.spec.Integer() + ) + + output: str = writer.dumps(Foo._INST) + assert output + + for line in output.split("\n"): + assert not line.endswith(" ") diff --git a/uv.lock b/uv.lock index 2cf2bf8..d754b7d 100644 --- a/uv.lock +++ b/uv.lock @@ -143,6 +143,8 @@ source = { virtual = "." } [package.dev-dependencies] dev = [ + { name = "mypy" }, + { name = "pytest" }, { name = "sphinx" }, { name = "sphinx-autobuild" }, { name = "sphinx-rtd-theme" }, @@ -152,6 +154,8 @@ dev = [ [package.metadata.requires-dev] dev = [ + { name = "mypy", specifier = ">=1.19.1" }, + { name = "pytest", specifier = ">=9.0.2" }, { name = "sphinx", specifier = ">=9.1.0" }, { name = "sphinx-autobuild", specifier = ">=2025.8.25" }, { name = "sphinx-rtd-theme", specifier = ">=3.1.0" }, @@ -193,6 +197,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5f/53/fb7122b71361a0d121b669dcf3d31244ef75badbbb724af388948de543e2/imagesize-2.0.0-py2.py3-none-any.whl", hash = "sha256:5667c5bbb57ab3f1fa4bc366f4fbc971db3d5ed011fd2715fd8001f782718d96", size = 9441, upload-time = "2026-03-03T14:18:27.892Z" }, ] +[[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" @@ -205,6 +218,66 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "librt" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/9c/b4b0c54d84da4a94b37bd44151e46d5e583c9534c7e02250b961b1b6d8a8/librt-0.8.1.tar.gz", hash = "sha256:be46a14693955b3bd96014ccbdb8339ee8c9346fbe11c1b78901b55125f14c73", size = 177471, upload-time = "2026-02-17T16:13:06.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/21/d39b0a87ac52fc98f621fb6f8060efb017a767ebbbac2f99fbcbc9ddc0d7/librt-0.8.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a28f2612ab566b17f3698b0da021ff9960610301607c9a5e8eaca62f5e1c350a", size = 66516, upload-time = "2026-02-17T16:11:41.604Z" }, + { url = "https://files.pythonhosted.org/packages/69/f1/46375e71441c43e8ae335905e069f1c54febee63a146278bcee8782c84fd/librt-0.8.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:60a78b694c9aee2a0f1aaeaa7d101cf713e92e8423a941d2897f4fa37908dab9", size = 68634, upload-time = "2026-02-17T16:11:43.268Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/c510de7f93bf1fa19e13423a606d8189a02624a800710f6e6a0a0f0784b3/librt-0.8.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:758509ea3f1eba2a57558e7e98f4659d0ea7670bff49673b0dde18a3c7e6c0eb", size = 198941, upload-time = "2026-02-17T16:11:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/dd/36/e725903416409a533d92398e88ce665476f275081d0d7d42f9c4951999e5/librt-0.8.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:039b9f2c506bd0ab0f8725aa5ba339c6f0cd19d3b514b50d134789809c24285d", size = 209991, upload-time = "2026-02-17T16:11:45.462Z" }, + { url = "https://files.pythonhosted.org/packages/30/7a/8d908a152e1875c9f8eac96c97a480df425e657cdb47854b9efaa4998889/librt-0.8.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bb54f1205a3a6ab41a6fd71dfcdcbd278670d3a90ca502a30d9da583105b6f7", size = 224476, upload-time = "2026-02-17T16:11:46.542Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/a22c34f2c485b8903a06f3fe3315341fe6876ef3599792344669db98fcff/librt-0.8.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:05bd41cdee35b0c59c259f870f6da532a2c5ca57db95b5f23689fcb5c9e42440", size = 217518, upload-time = "2026-02-17T16:11:47.746Z" }, + { url = "https://files.pythonhosted.org/packages/79/6f/5c6fea00357e4f82ba44f81dbfb027921f1ab10e320d4a64e1c408d035d9/librt-0.8.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adfab487facf03f0d0857b8710cf82d0704a309d8ffc33b03d9302b4c64e91a9", size = 225116, upload-time = "2026-02-17T16:11:49.298Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a0/95ced4e7b1267fe1e2720a111685bcddf0e781f7e9e0ce59d751c44dcfe5/librt-0.8.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:153188fe98a72f206042be10a2c6026139852805215ed9539186312d50a8e972", size = 217751, upload-time = "2026-02-17T16:11:50.49Z" }, + { url = "https://files.pythonhosted.org/packages/93/c2/0517281cb4d4101c27ab59472924e67f55e375bc46bedae94ac6dc6e1902/librt-0.8.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dd3c41254ee98604b08bd5b3af5bf0a89740d4ee0711de95b65166bf44091921", size = 218378, upload-time = "2026-02-17T16:11:51.783Z" }, + { url = "https://files.pythonhosted.org/packages/43/e8/37b3ac108e8976888e559a7b227d0ceac03c384cfd3e7a1c2ee248dbae79/librt-0.8.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e0d138c7ae532908cbb342162b2611dbd4d90c941cd25ab82084aaf71d2c0bd0", size = 241199, upload-time = "2026-02-17T16:11:53.561Z" }, + { url = "https://files.pythonhosted.org/packages/4b/5b/35812d041c53967fedf551a39399271bbe4257e681236a2cf1a69c8e7fa1/librt-0.8.1-cp312-cp312-win32.whl", hash = "sha256:43353b943613c5d9c49a25aaffdba46f888ec354e71e3529a00cca3f04d66a7a", size = 54917, upload-time = "2026-02-17T16:11:54.758Z" }, + { url = "https://files.pythonhosted.org/packages/de/d1/fa5d5331b862b9775aaf2a100f5ef86854e5d4407f71bddf102f4421e034/librt-0.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:ff8baf1f8d3f4b6b7257fcb75a501f2a5499d0dda57645baa09d4d0d34b19444", size = 62017, upload-time = "2026-02-17T16:11:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7c/c614252f9acda59b01a66e2ddfd243ed1c7e1deab0293332dfbccf862808/librt-0.8.1-cp312-cp312-win_arm64.whl", hash = "sha256:0f2ae3725904f7377e11cc37722d5d401e8b3d5851fb9273d7f4fe04f6b3d37d", size = 52441, upload-time = "2026-02-17T16:11:56.801Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3c/f614c8e4eaac7cbf2bbdf9528790b21d89e277ee20d57dc6e559c626105f/librt-0.8.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7e6bad1cd94f6764e1e21950542f818a09316645337fd5ab9a7acc45d99a8f35", size = 66529, upload-time = "2026-02-17T16:11:57.809Z" }, + { url = "https://files.pythonhosted.org/packages/ab/96/5836544a45100ae411eda07d29e3d99448e5258b6e9c8059deb92945f5c2/librt-0.8.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cf450f498c30af55551ba4f66b9123b7185362ec8b625a773b3d39aa1a717583", size = 68669, upload-time = "2026-02-17T16:11:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/06/53/f0b992b57af6d5531bf4677d75c44f095f2366a1741fb695ee462ae04b05/librt-0.8.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:eca45e982fa074090057132e30585a7e8674e9e885d402eae85633e9f449ce6c", size = 199279, upload-time = "2026-02-17T16:11:59.862Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/4848cc16e268d14280d8168aee4f31cea92bbd2b79ce33d3e166f2b4e4fc/librt-0.8.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c3811485fccfda840861905b8c70bba5ec094e02825598bb9d4ca3936857a04", size = 210288, upload-time = "2026-02-17T16:12:00.954Z" }, + { url = "https://files.pythonhosted.org/packages/52/05/27fdc2e95de26273d83b96742d8d3b7345f2ea2bdbd2405cc504644f2096/librt-0.8.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e4af413908f77294605e28cfd98063f54b2c790561383971d2f52d113d9c363", size = 224809, upload-time = "2026-02-17T16:12:02.108Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d0/78200a45ba3240cb042bc597d6f2accba9193a2c57d0356268cbbe2d0925/librt-0.8.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5212a5bd7fae98dae95710032902edcd2ec4dc994e883294f75c857b83f9aba0", size = 218075, upload-time = "2026-02-17T16:12:03.631Z" }, + { url = "https://files.pythonhosted.org/packages/af/72/a210839fa74c90474897124c064ffca07f8d4b347b6574d309686aae7ca6/librt-0.8.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e692aa2d1d604e6ca12d35e51fdc36f4cda6345e28e36374579f7ef3611b3012", size = 225486, upload-time = "2026-02-17T16:12:04.725Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c1/a03cc63722339ddbf087485f253493e2b013039f5b707e8e6016141130fa/librt-0.8.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4be2a5c926b9770c9e08e717f05737a269b9d0ebc5d2f0060f0fe3fe9ce47acb", size = 218219, upload-time = "2026-02-17T16:12:05.828Z" }, + { url = "https://files.pythonhosted.org/packages/58/f5/fff6108af0acf941c6f274a946aea0e484bd10cd2dc37610287ce49388c5/librt-0.8.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:fd1a720332ea335ceb544cf0a03f81df92abd4bb887679fd1e460976b0e6214b", size = 218750, upload-time = "2026-02-17T16:12:07.09Z" }, + { url = "https://files.pythonhosted.org/packages/71/67/5a387bfef30ec1e4b4f30562c8586566faf87e47d696768c19feb49e3646/librt-0.8.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2af9e01e0ef80d95ae3c720be101227edae5f2fe7e3dc63d8857fadfc5a1d", size = 241624, upload-time = "2026-02-17T16:12:08.43Z" }, + { url = "https://files.pythonhosted.org/packages/d4/be/24f8502db11d405232ac1162eb98069ca49c3306c1d75c6ccc61d9af8789/librt-0.8.1-cp313-cp313-win32.whl", hash = "sha256:086a32dbb71336627e78cc1d6ee305a68d038ef7d4c39aaff41ae8c9aa46e91a", size = 54969, upload-time = "2026-02-17T16:12:09.633Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/c9fdf6cb2a529c1a092ce769a12d88c8cca991194dfe641b6af12fa964d2/librt-0.8.1-cp313-cp313-win_amd64.whl", hash = "sha256:e11769a1dbda4da7b00a76cfffa67aa47cfa66921d2724539eee4b9ede780b79", size = 62000, upload-time = "2026-02-17T16:12:10.632Z" }, + { url = "https://files.pythonhosted.org/packages/d3/97/68f80ca3ac4924f250cdfa6e20142a803e5e50fca96ef5148c52ee8c10ea/librt-0.8.1-cp313-cp313-win_arm64.whl", hash = "sha256:924817ab3141aca17893386ee13261f1d100d1ef410d70afe4389f2359fea4f0", size = 52495, upload-time = "2026-02-17T16:12:11.633Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6a/907ef6800f7bca71b525a05f1839b21f708c09043b1c6aa77b6b827b3996/librt-0.8.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6cfa7fe54fd4d1f47130017351a959fe5804bda7a0bc7e07a2cdbc3fdd28d34f", size = 66081, upload-time = "2026-02-17T16:12:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/1b/18/25e991cd5640c9fb0f8d91b18797b29066b792f17bf8493da183bf5caabe/librt-0.8.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:228c2409c079f8c11fb2e5d7b277077f694cb93443eb760e00b3b83cb8b3176c", size = 68309, upload-time = "2026-02-17T16:12:13.756Z" }, + { url = "https://files.pythonhosted.org/packages/a4/36/46820d03f058cfb5a9de5940640ba03165ed8aded69e0733c417bb04df34/librt-0.8.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7aae78ab5e3206181780e56912d1b9bb9f90a7249ce12f0e8bf531d0462dd0fc", size = 196804, upload-time = "2026-02-17T16:12:14.818Z" }, + { url = "https://files.pythonhosted.org/packages/59/18/5dd0d3b87b8ff9c061849fbdb347758d1f724b9a82241aa908e0ec54ccd0/librt-0.8.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:172d57ec04346b047ca6af181e1ea4858086c80bdf455f61994c4aa6fc3f866c", size = 206907, upload-time = "2026-02-17T16:12:16.513Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/ef04902aad1424fd7299b62d1890e803e6ab4018c3044dca5922319c4b97/librt-0.8.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b1977c4ea97ce5eb7755a78fae68d87e4102e4aaf54985e8b56806849cc06a3", size = 221217, upload-time = "2026-02-17T16:12:17.906Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ff/7e01f2dda84a8f5d280637a2e5827210a8acca9a567a54507ef1c75b342d/librt-0.8.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:10c42e1f6fd06733ef65ae7bebce2872bcafd8d6e6b0a08fe0a05a23b044fb14", size = 214622, upload-time = "2026-02-17T16:12:19.108Z" }, + { url = "https://files.pythonhosted.org/packages/1e/8c/5b093d08a13946034fed57619742f790faf77058558b14ca36a6e331161e/librt-0.8.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4c8dfa264b9193c4ee19113c985c95f876fae5e51f731494fc4e0cf594990ba7", size = 221987, upload-time = "2026-02-17T16:12:20.331Z" }, + { url = "https://files.pythonhosted.org/packages/d3/cc/86b0b3b151d40920ad45a94ce0171dec1aebba8a9d72bb3fa00c73ab25dd/librt-0.8.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:01170b6729a438f0dedc4a26ed342e3dc4f02d1000b4b19f980e1877f0c297e6", size = 215132, upload-time = "2026-02-17T16:12:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/fc/be/8588164a46edf1e69858d952654e216a9a91174688eeefb9efbb38a9c799/librt-0.8.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7b02679a0d783bdae30d443025b94465d8c3dc512f32f5b5031f93f57ac32071", size = 215195, upload-time = "2026-02-17T16:12:23.073Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f2/0b9279bea735c734d69344ecfe056c1ba211694a72df10f568745c899c76/librt-0.8.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:190b109bb69592a3401fe1ffdea41a2e73370ace2ffdc4a0e8e2b39cdea81b78", size = 237946, upload-time = "2026-02-17T16:12:24.275Z" }, + { url = "https://files.pythonhosted.org/packages/e9/cc/5f2a34fbc8aeb35314a3641f9956fa9051a947424652fad9882be7a97949/librt-0.8.1-cp314-cp314-win32.whl", hash = "sha256:e70a57ecf89a0f64c24e37f38d3fe217a58169d2fe6ed6d70554964042474023", size = 50689, upload-time = "2026-02-17T16:12:25.766Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/cd4d010ab2147339ca2b93e959c3686e964edc6de66ddacc935c325883d7/librt-0.8.1-cp314-cp314-win_amd64.whl", hash = "sha256:7e2f3edca35664499fbb36e4770650c4bd4a08abc1f4458eab9df4ec56389730", size = 57875, upload-time = "2026-02-17T16:12:27.465Z" }, + { url = "https://files.pythonhosted.org/packages/84/0f/2143cb3c3ca48bd3379dcd11817163ca50781927c4537345d608b5045998/librt-0.8.1-cp314-cp314-win_arm64.whl", hash = "sha256:0d2f82168e55ddefd27c01c654ce52379c0750ddc31ee86b4b266bcf4d65f2a3", size = 48058, upload-time = "2026-02-17T16:12:28.556Z" }, + { url = "https://files.pythonhosted.org/packages/d2/0e/9b23a87e37baf00311c3efe6b48d6b6c168c29902dfc3f04c338372fd7db/librt-0.8.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c74a2da57a094bd48d03fa5d196da83d2815678385d2978657499063709abe1", size = 68313, upload-time = "2026-02-17T16:12:29.659Z" }, + { url = "https://files.pythonhosted.org/packages/db/9a/859c41e5a4f1c84200a7d2b92f586aa27133c8243b6cac9926f6e54d01b9/librt-0.8.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a355d99c4c0d8e5b770313b8b247411ed40949ca44e33e46a4789b9293a907ee", size = 70994, upload-time = "2026-02-17T16:12:31.516Z" }, + { url = "https://files.pythonhosted.org/packages/4c/28/10605366ee599ed34223ac2bf66404c6fb59399f47108215d16d5ad751a8/librt-0.8.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2eb345e8b33fb748227409c9f1233d4df354d6e54091f0e8fc53acdb2ffedeb7", size = 220770, upload-time = "2026-02-17T16:12:33.294Z" }, + { url = "https://files.pythonhosted.org/packages/af/8d/16ed8fd452dafae9c48d17a6bc1ee3e818fd40ef718d149a8eff2c9f4ea2/librt-0.8.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9be2f15e53ce4e83cc08adc29b26fb5978db62ef2a366fbdf716c8a6c8901040", size = 235409, upload-time = "2026-02-17T16:12:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/89/1b/7bdf3e49349c134b25db816e4a3db6b94a47ac69d7d46b1e682c2c4949be/librt-0.8.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:785ae29c1f5c6e7c2cde2c7c0e148147f4503da3abc5d44d482068da5322fd9e", size = 246473, upload-time = "2026-02-17T16:12:36.656Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8a/91fab8e4fd2a24930a17188c7af5380eb27b203d72101c9cc000dbdfd95a/librt-0.8.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d3a7da44baf692f0c6aeb5b2a09c5e6fc7a703bca9ffa337ddd2e2da53f7732", size = 238866, upload-time = "2026-02-17T16:12:37.849Z" }, + { url = "https://files.pythonhosted.org/packages/b9/e0/c45a098843fc7c07e18a7f8a24ca8496aecbf7bdcd54980c6ca1aaa79a8e/librt-0.8.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fc48998000cbc39ec0d5311312dda93ecf92b39aaf184c5e817d5d440b29624", size = 250248, upload-time = "2026-02-17T16:12:39.445Z" }, + { url = "https://files.pythonhosted.org/packages/82/30/07627de23036640c952cce0c1fe78972e77d7d2f8fd54fa5ef4554ff4a56/librt-0.8.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:e96baa6820280077a78244b2e06e416480ed859bbd8e5d641cf5742919d8beb4", size = 240629, upload-time = "2026-02-17T16:12:40.889Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/55bfe1ee3542eba055616f9098eaf6eddb966efb0ca0f44eaa4aba327307/librt-0.8.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31362dbfe297b23590530007062c32c6f6176f6099646bb2c95ab1b00a57c382", size = 239615, upload-time = "2026-02-17T16:12:42.446Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/191d3d28abc26c9099b19852e6c99f7f6d400b82fa5a4e80291bd3803e19/librt-0.8.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc3656283d11540ab0ea01978378e73e10002145117055e03722417aeab30994", size = 263001, upload-time = "2026-02-17T16:12:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/b9/eb/7697f60fbe7042ab4e88f4ee6af496b7f222fffb0a4e3593ef1f29f81652/librt-0.8.1-cp314-cp314t-win32.whl", hash = "sha256:738f08021b3142c2918c03692608baed43bc51144c29e35807682f8070ee2a3a", size = 51328, upload-time = "2026-02-17T16:12:45.148Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/34bf2eb7a15414a23e5e70ecb9440c1d3179f393d9349338a91e2781c0fb/librt-0.8.1-cp314-cp314t-win_amd64.whl", hash = "sha256:89815a22daf9c51884fb5dbe4f1ef65ee6a146e0b6a8df05f753e2e4a9359bf4", size = 58722, upload-time = "2026-02-17T16:12:46.85Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c8/d148e041732d631fc76036f8b30fae4e77b027a1e95b7a84bb522481a940/librt-0.8.1-cp314-cp314t-win_arm64.whl", hash = "sha256:bf512a71a23504ed08103a13c941f763db13fb11177beb3d9244c98c29fb4a61", size = 48755, upload-time = "2026-02-17T16:12:47.943Z" }, +] + [[package]] name = "markupsafe" version = "3.0.3" @@ -268,6 +341,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "mypy" +version = "1.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/b0089fe7fef0a994ae5ee07029ced0526082c6cfaaa4c10d40a10e33b097/mypy-1.20.0.tar.gz", hash = "sha256:eb96c84efcc33f0b5e0e04beacf00129dd963b67226b01c00b9dfc8affb464c3", size = 3815028, upload-time = "2026-03-31T16:55:14.959Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/dd/3afa29b58c2e57c79116ed55d700721c3c3b15955e2b6251dd165d377c0e/mypy-1.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:002b613ae19f4ac7d18b7e168ffe1cb9013b37c57f7411984abbd3b817b0a214", size = 14509525, upload-time = "2026-03-31T16:55:01.824Z" }, + { url = "https://files.pythonhosted.org/packages/54/eb/227b516ab8cad9f2a13c5e7a98d28cd6aa75e9c83e82776ae6c1c4c046c7/mypy-1.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a9336b5e6712f4adaf5afc3203a99a40b379049104349d747eb3e5a3aa23ac2e", size = 13326469, upload-time = "2026-03-31T16:51:41.23Z" }, + { url = "https://files.pythonhosted.org/packages/57/d4/1ddb799860c1b5ac6117ec307b965f65deeb47044395ff01ab793248a591/mypy-1.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f13b3e41bce9d257eded794c0f12878af3129d80aacd8a3ee0dee51f3a978651", size = 13705953, upload-time = "2026-03-31T16:48:55.69Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b7/54a720f565a87b893182a2a393370289ae7149e4715859e10e1c05e49154/mypy-1.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9804c3ad27f78e54e58b32e7cb532d128b43dbfb9f3f9f06262b821a0f6bd3f5", size = 14710363, upload-time = "2026-03-31T16:53:26.948Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2a/74810274848d061f8a8ea4ac23aaad43bd3d8c1882457999c2e568341c57/mypy-1.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:697f102c5c1d526bdd761a69f17c6070f9892eebcb94b1a5963d679288c09e78", size = 14947005, upload-time = "2026-03-31T16:50:17.591Z" }, + { url = "https://files.pythonhosted.org/packages/77/91/21b8ba75f958bcda75690951ce6fa6b7138b03471618959529d74b8544e2/mypy-1.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ecd63f75fdd30327e4ad8b5704bd6d91fc6c1b2e029f8ee14705e1207212489", size = 10880616, upload-time = "2026-03-31T16:52:19.986Z" }, + { url = "https://files.pythonhosted.org/packages/8a/15/3d8198ef97c1ca03aea010cce4f1d4f3bc5d9849e8c0140111ca2ead9fdd/mypy-1.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:f194db59657c58593a3c47c6dfd7bad4ef4ac12dbc94d01b3a95521f78177e33", size = 9813091, upload-time = "2026-03-31T16:53:44.385Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a7/f64ea7bd592fa431cb597418b6dec4a47f7d0c36325fec7ac67bc8402b94/mypy-1.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b20c8b0fd5877abdf402e79a3af987053de07e6fb208c18df6659f708b535134", size = 14485344, upload-time = "2026-03-31T16:49:16.78Z" }, + { url = "https://files.pythonhosted.org/packages/bb/72/8927d84cfc90c6abea6e96663576e2e417589347eb538749a464c4c218a0/mypy-1.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:367e5c993ba34d5054d11937d0485ad6dfc60ba760fa326c01090fc256adf15c", size = 13327400, upload-time = "2026-03-31T16:53:08.02Z" }, + { url = "https://files.pythonhosted.org/packages/ab/4a/11ab99f9afa41aa350178d24a7d2da17043228ea10f6456523f64b5a6cf6/mypy-1.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f799d9db89fc00446f03281f84a221e50018fc40113a3ba9864b132895619ebe", size = 13706384, upload-time = "2026-03-31T16:52:28.577Z" }, + { url = "https://files.pythonhosted.org/packages/42/79/694ca73979cfb3535ebfe78733844cd5aff2e63304f59bf90585110d975a/mypy-1.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:555658c611099455b2da507582ea20d2043dfdfe7f5ad0add472b1c6238b433f", size = 14700378, upload-time = "2026-03-31T16:48:45.527Z" }, + { url = "https://files.pythonhosted.org/packages/84/24/a022ccab3a46e3d2cdf2e0e260648633640eb396c7e75d5a42818a8d3971/mypy-1.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:efe8d70949c3023698c3fca1e94527e7e790a361ab8116f90d11221421cd8726", size = 14932170, upload-time = "2026-03-31T16:49:36.038Z" }, + { url = "https://files.pythonhosted.org/packages/d8/9b/549228d88f574d04117e736f55958bd4908f980f9f5700a07aeb85df005b/mypy-1.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:f49590891d2c2f8a9de15614e32e459a794bcba84693c2394291a2038bbaaa69", size = 10888526, upload-time = "2026-03-31T16:50:59.827Z" }, + { url = "https://files.pythonhosted.org/packages/91/17/15095c0e54a8bc04d22d4ff06b2139d5f142c2e87520b4e39010c4862771/mypy-1.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:76a70bf840495729be47510856b978f1b0ec7d08f257ca38c9d932720bf6b43e", size = 9816456, upload-time = "2026-03-31T16:49:59.537Z" }, + { url = "https://files.pythonhosted.org/packages/4e/0e/6ca4a84cbed9e62384bc0b2974c90395ece5ed672393e553996501625fc5/mypy-1.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0f42dfaab7ec1baff3b383ad7af562ab0de573c5f6edb44b2dab016082b89948", size = 14483331, upload-time = "2026-03-31T16:52:57.999Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c5/5fe9d8a729dd9605064691816243ae6c49fde0bd28f6e5e17f6a24203c43/mypy-1.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:31b5dbb55293c1bd27c0fc813a0d2bb5ceef9d65ac5afa2e58f829dab7921fd5", size = 13342047, upload-time = "2026-03-31T16:54:21.555Z" }, + { url = "https://files.pythonhosted.org/packages/4c/33/e18bcfa338ca4e6b2771c85d4c5203e627d0c69d9de5c1a2cf2ba13320ba/mypy-1.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49d11c6f573a5a08f77fad13faff2139f6d0730ebed2cfa9b3d2702671dd7188", size = 13719585, upload-time = "2026-03-31T16:51:53.89Z" }, + { url = "https://files.pythonhosted.org/packages/6b/8d/93491ff7b79419edc7eabf95cb3b3f7490e2e574b2855c7c7e7394ff933f/mypy-1.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d3243c406773185144527f83be0e0aefc7bf4601b0b2b956665608bf7c98a83", size = 14685075, upload-time = "2026-03-31T16:54:04.464Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9d/d924b38a4923f8d164bf2b4ec98bf13beaf6e10a5348b4b137eadae40a6e/mypy-1.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a79c1eba7ac4209f2d850f0edd0a2f8bba88cbfdfefe6fb76a19e9d4fe5e71a2", size = 14919141, upload-time = "2026-03-31T16:54:51.785Z" }, + { url = "https://files.pythonhosted.org/packages/59/98/1da9977016678c0b99d43afe52ed00bb3c1a0c4c995d3e6acca1a6ebb9b4/mypy-1.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:00e047c74d3ec6e71a2eb88e9ea551a2edb90c21f993aefa9e0d2a898e0bb732", size = 11050925, upload-time = "2026-03-31T16:51:30.758Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e3/ba0b7a3143e49a9c4f5967dde6ea4bf8e0b10ecbbcca69af84027160ee89/mypy-1.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:931a7630bba591593dcf6e97224a21ff80fb357e7982628d25e3c618e7f598ef", size = 10001089, upload-time = "2026-03-31T16:49:43.632Z" }, + { url = "https://files.pythonhosted.org/packages/12/28/e617e67b3be9d213cda7277913269c874eb26472489f95d09d89765ce2d8/mypy-1.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:26c8b52627b6552f47ff11adb4e1509605f094e29815323e487fc0053ebe93d1", size = 15534710, upload-time = "2026-03-31T16:52:12.506Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0c/3b5f2d3e45dc7169b811adce8451679d9430399d03b168f9b0489f43adaa/mypy-1.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:39362cdb4ba5f916e7976fccecaab1ba3a83e35f60fa68b64e9a70e221bb2436", size = 14393013, upload-time = "2026-03-31T16:54:41.186Z" }, + { url = "https://files.pythonhosted.org/packages/a3/49/edc8b0aa145cc09c1c74f7ce2858eead9329931dcbbb26e2ad40906daa4e/mypy-1.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34506397dbf40c15dc567635d18a21d33827e9ab29014fb83d292a8f4f8953b6", size = 15047240, upload-time = "2026-03-31T16:54:31.955Z" }, + { url = "https://files.pythonhosted.org/packages/42/37/a946bb416e37a57fa752b3100fd5ede0e28df94f92366d1716555d47c454/mypy-1.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:555493c44a4f5a1b58d611a43333e71a9981c6dbe26270377b6f8174126a0526", size = 15858565, upload-time = "2026-03-31T16:53:36.997Z" }, + { url = "https://files.pythonhosted.org/packages/2f/99/7690b5b5b552db1bd4ff362e4c0eb3107b98d680835e65823fbe888c8b78/mypy-1.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2721f0ce49cb74a38f00c50da67cb7d36317b5eda38877a49614dc018e91c787", size = 16087874, upload-time = "2026-03-31T16:52:48.313Z" }, + { url = "https://files.pythonhosted.org/packages/aa/76/53e893a498138066acd28192b77495c9357e5a58cc4be753182846b43315/mypy-1.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:47781555a7aa5fedcc2d16bcd72e0dc83eb272c10dd657f9fb3f9cc08e2e6abb", size = 12572380, upload-time = "2026-03-31T16:49:52.454Z" }, + { url = "https://files.pythonhosted.org/packages/76/9c/6dbdae21f01b7aacddc2c0bbf3c5557aa547827fdf271770fe1e521e7093/mypy-1.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:c70380fe5d64010f79fb863b9081c7004dd65225d2277333c219d93a10dad4dd", size = 10381174, upload-time = "2026-03-31T16:51:20.179Z" }, + { url = "https://files.pythonhosted.org/packages/21/66/4d734961ce167f0fd8380769b3b7c06dbdd6ff54c2190f3f2ecd22528158/mypy-1.20.0-py3-none-any.whl", hash = "sha256:a6e0641147cbfa7e4e94efdb95c2dab1aff8cfc159ded13e07f308ddccc8c48e", size = 2636365, upload-time = "2026-03-31T16:51:44.911Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + [[package]] name = "packaging" version = "26.0" @@ -277,6 +402,24 @@ 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 = "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 = "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 = "pygments" version = "2.19.2" @@ -286,6 +429,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, ] +[[package]] +name = "pytest" +version = "9.0.3" +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/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + [[package]] name = "requests" version = "2.32.5" From 38b5247adc3101a799c2ca9de0799d7e80a22ff8 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:53:39 -0400 Subject: [PATCH 28/40] fix trailing whitespace --- comprehensiveconfig/toml.py | 2 +- tests/test_fields.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/comprehensiveconfig/toml.py b/comprehensiveconfig/toml.py index 8cb1c0f..8492bb3 100644 --- a/comprehensiveconfig/toml.py +++ b/comprehensiveconfig/toml.py @@ -107,7 +107,7 @@ def dump_field( if isinstance(value, spec.Section): return "\n".join(cls.dump_section(value)) - field_doc = " " if field._inline_doc else "\n" + field_doc = " " if field._inline_doc and field.doc else "\n" if field.doc: field_doc += f"# {"\n# ".join(field.doc.split("\n"))}" diff --git a/tests/test_fields.py b/tests/test_fields.py index b35e9bb..f09e43d 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -318,7 +318,7 @@ class Bar(comprehensiveconfig.spec.Section, name="burger"): assert '"clean" # Example doc 1' in toml_output # ensure exists and *is* inlined -@pytest.mark.parametrize(("filename", "writer"), [toml_config]) +@pytest.mark.parametrize(("filename", "writer"), parameterize_values) def test_trailing_spaces(filename: str, writer): """ensure final outputs have NO trailing spaces""" @@ -356,8 +356,15 @@ class Bar(comprehensiveconfig.spec.Section, name="burger"): [10, 20, 30], inner_type=comprehensiveconfig.spec.Integer() ) + test_enum_value = comprehensiveconfig.spec.ConfigEnum( + ExampleEnum, ExampleEnum.example + ) + test_enum_name = comprehensiveconfig.spec.ConfigEnum( + ExampleEnum, ExampleEnum.example, by_name=True + ) + output: str = writer.dumps(Foo._INST) assert output for line in output.split("\n"): - assert not line.endswith(" ") + assert not line.endswith(" "), f'"{line}"' From 8c0d8fd89f4b89861f3e850c285df3d7986b508a Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:00:09 -0400 Subject: [PATCH 29/40] attempt using gitlab ci --- .github/workflows/pytest.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/pytest.yml diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..2569f58 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,25 @@ +name: Run Python Tests + +on: + push: + branches: [ "master", "staging"] + pull_request: + branches: [ "master", "staging" ] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + + - name: Install the latest version of uv + uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 + with: + enable-cache: true + + - name: run tests + run: uv run pytest \ No newline at end of file From 94822a641968eb6cef8489977d99c1aa157ff3df Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:48:38 -0400 Subject: [PATCH 30/40] Allow for arbitrary object writing --- comprehensiveconfig/json.py | 25 ++++- comprehensiveconfig/spec.py | 70 +++++++++++++- comprehensiveconfig/toml.py | 163 ++++++++++++++++++++------------- comprehensiveconfig/utility.py | 58 ++++++++++++ tests/test_fields.py | 52 +++++++++++ tests/test_writers.py | 9 ++ 6 files changed, 307 insertions(+), 70 deletions(-) create mode 100644 comprehensiveconfig/utility.py create mode 100644 tests/test_writers.py diff --git a/comprehensiveconfig/json.py b/comprehensiveconfig/json.py index 2a372e8..16025ab 100644 --- a/comprehensiveconfig/json.py +++ b/comprehensiveconfig/json.py @@ -1,3 +1,4 @@ +from datetime import datetime import json from . import configio from . import spec @@ -13,10 +14,14 @@ def dump_section(cls, node: spec.Section): @classmethod def dump_value(cls, node: spec.AnyConfigField, value): + """convert value into json_serializable object that can be used as a key or value""" match node: - case type() | spec.Section(): + case spec.ConfigurationFieldABCMeta() | spec.Section(): return cls.dump_section(value) - case spec.Table(_, type() | spec.Section() | spec.ConfigUnion()): + case spec.Table( + _, + spec.ConfigurationFieldABCMeta() | spec.Section() | spec.ConfigUnion(), + ): return { cls.dump_value(key, key): cls.dump_value(val, val) for key, val in value.items() @@ -25,14 +30,28 @@ def dump_value(cls, node: spec.AnyConfigField, value): return value.name case spec.ConfigEnum(_, False): return value.value - case _: + case str() | int() | float() | datetime() | dict() | None: return value + case _: + # magic method to make writing new field types possible + if hasattr(node, "__write_json_value__"): + return value.__write_json_value__( + node, value + ) # return a json serializable object @classmethod def dumps(cls, node) -> str: match node: case spec.Section(): return json.dumps(cls.dump_section(node), indent=4) + case spec.ConfigurationField(): + if not node._has_default: + raise ValueError("Field has no default value") + + dumped_value = cls.dump_value(node, node._default_value) + if isinstance(dumped_value, dict): + return json.dumps(dumped_value) + return str(dumped_value) case _: raise ValueError(node) diff --git a/comprehensiveconfig/spec.py b/comprehensiveconfig/spec.py index 5575751..65988cd 100644 --- a/comprehensiveconfig/spec.py +++ b/comprehensiveconfig/spec.py @@ -3,7 +3,7 @@ import re from types import UnionType import types -from typing import Any, Self, Type, Union +from typing import Any, Protocol, Self, Type, Union import typing @@ -51,7 +51,7 @@ class BaseConfigurationField(ABC): __slots__ = ("_field_variable", "_parent", "_value") - _parent: Type[Self] | None + _parent: Type["BaseConfigurationField"] | None """The parent to this node""" _field_variable: None | str @@ -177,7 +177,7 @@ class Section(BaseConfigurationField, metaclass=ConfigurationFieldABCMeta): __slots__ = "_value" - _FIELDS: dict[str, AnyConfigField] + _FIELDS: dict[str, ConfigurationField] _SECTIONS: dict[str, Type] _ALL_FIELDS: dict[str, AnyConfigField | Type] _FIELD_NAME_MAP: dict[str, str] @@ -772,6 +772,69 @@ def _validate_value(self, value: Any, name: str | None = None, /): super()._validate_value(self.get_value(value), name) +class ConfigObjectType[T](Protocol): + """A protocol to define necessary methods for a ConfigObject field's type""" + + @classmethod + def from_config(cls, config_value: Any) -> T: + """A constructor for this object if the value we are using comes from configuration""" + ... + + +class ConfigObject[T: ConfigObjectType](ConfigurationField): + """A custom object field allowing you to write arbitrary objects that are supported by the writer you are using. + This can also be used for objects that implement writer-specific magic-methods. + + These include: + - `__write_toml_value__(field, value) -> str` (writing a regular toml-parsable value as a string) + - `__write_toml_full__(field, value) -> str` (Directly write line(s) of toml when encountering this object) + - `__write_json_value__(field, value) -> int | float | datetime() | str | None` \ + (When encountering this object- convert it to a json serializable format) + """ + + __slots__ = "_type" + __match_args__ = ("_type", "_by_name") + + _holds: T + + _type: Type[T] + """The object type""" + + def __init__( + self, + _type: Type[T], + default_value: T | _NoDefaultValueT = NoDefaultValue, + /, + *args, + **kwargs, + ): + self._type = _type + return super().__init__(default_value, *args, **kwargs) + + def get_value(self, value: Any): + if isinstance(value, self._type): + return value + return self.__call__(value) + + def __call__(self, value: Any): + if isinstance(value, self._type): + return value + return self._type.from_config(value) + + def __get__(self, instance, owner) -> T: + return super().__get__(instance, owner) + + def __set__(self, instance, value: T | Any): + if isinstance(value, self._type): + return super().__set__(instance, value) + super().__set__(instance, self.get_value(value)) + + def _validate_value(self, value: Any, name: str | None = None, /): + if isinstance(value, self._type): + super()._validate_value(value, name) + super()._validate_value(self.get_value(value), name) + + __all__ = [ "ConfigurationField", "NoDefaultValue", @@ -785,4 +848,5 @@ def _validate_value(self, value: Any, name: str | None = None, /): "TableSpec", "List", "ConfigEnum", + "ConfigObject", ] diff --git a/comprehensiveconfig/toml.py b/comprehensiveconfig/toml.py index 8492bb3..ada0aed 100644 --- a/comprehensiveconfig/toml.py +++ b/comprehensiveconfig/toml.py @@ -32,10 +32,13 @@ def full_section_name(node) -> list[str]: class TomlWriter(configio.ConfigurationWriter): @classmethod - def dump_section(cls, node) -> list: + def dump_section(cls, node) -> list[str]: + """Dump a spec.Section node and return a list of output lines.""" if " " in node._name: raise ValueError(node._name) + # "base" is all the junk/lines at the beginning of our section. + if node._parent is not None: base = [f"\n[{'.'.join(full_section_name(node)[1:])}]"] else: @@ -45,108 +48,139 @@ def dump_section(cls, node) -> list: for line in node.__doc__.split("\n"): base.append(f"# {line}") - sorted_values: dict[int, dict[str, Any]] = {} + sorted_values: dict[int, dict[spec.ConfigurationField, Any]] = {} for name, value in node._value.items(): field = node._ALL_FIELDS[name] if field._sorting_order not in sorted_values.keys(): - sorted_values[field._sorting_order] = {name: value} - continue - sorted_values[field._sorting_order][name] = value + sorted_values[field._sorting_order] = {} + sorted_values[field._sorting_order][field] = value return [ - *base, - *( + *base, # dump all base lines + *( # append all of these dumped fields as lines ( "\n".join(cls.dump_section(value)) if isinstance(value, spec.Section) - else cls.dump_field(node, name, node._FIELD_VAR_MAP[name], value) + else cls.dump_field(field, value) ) for sub_dict in sorted_values.values() - for name, value in sub_dict.items() + for field, value in sub_dict.items() ), ] @classmethod - def format_value(cls, value) -> str: + def format_value(cls, field, value) -> str: + """Format individual values into properly represented strings of valid toml values.""" match value: case int() | float(): return str(value) case str(): return f'"{escape(value)}"' case list(): - return f"[{", ".join([str(cls.format_value(inner_val)) for inner_val in value])}]" + return f"[{", ".join([str(cls.format_value(field, inner_val)) for inner_val in value])}]" case dict(): - return f"{{ {", ".join([f"{key} = {cls.format_value(inner_val)}" for key, inner_val in value.items()])} }}" + return f"{{ {", ".join([f"{key} = {cls.format_value(field, inner_val)}" for key, inner_val in value.items()])} }}" case enum.Enum(): - return f"{cls.format_value(value.value)}" + return f"{cls.format_value(field, value.value)}" case _: + # magic method to make writing new field types possible + if hasattr(value, "__write_toml_value__"): + return str(value.__write_toml_value__(field, value)) + # No known way exists to write this field: raise ValueError(value) @classmethod - def dump_field( - cls, node: spec.AnyConfigField, original_name: str, field_name: str, value - ) -> str: - if isinstance(node, spec.Section): - field = node.get_field(original_name) + def dump_table(cls, table_node: spec.Table, value) -> str: + for name, val in value.items(): + if not isinstance(val, spec.Section): + continue + val._name = name + val._parent = table_node + + section_name = ".".join(full_section_name(table_node)[1:]) + + return f"\n[{section_name}]\n{"\n".join(cls.dumps(val) for key, val in value.items())}" + + @classmethod + def create_basic_field_doc(cls, field: spec.ConfigurationField) -> str: + """generates our basic field_doc""" + if field._inline_doc and field.doc: + doc_comment = " " + elif field.doc: + doc_comment = "\n" else: - field = node - match field: - case spec.Table(spec.Text(), type() | spec.ConfigUnion()) as table_node: - for name, val in value.items(): - if not isinstance(val, spec.Section): - continue - val._name = name - val._parent = table_node + return "" + + return doc_comment + f"# {"\n# ".join(field.doc.split("\n"))}" + + @classmethod + def dump_enum(cls, field: spec.ConfigEnum, value): + if isinstance(value, spec.Section): + return "\n".join(cls.dump_section(value)) + + by_name = field._by_name + field_doc = " " if field._inline_doc and field.doc else "\n" + + if field.doc: + field_doc += f"# {"\n# ".join(field.doc.split("\n"))}" - section_name = ".".join(full_section_name(table_node)[1:]) + if field._enum.__doc__: + delimeter = "\n## - " + doc_comment = f"# {"\n# ".join(field._enum.__doc__.split("\n"))}\n#" + else: + delimeter = "\n# - " + doc_comment = "" + + doc_comment += f"# Available Options for {field._name}:{delimeter}" + if by_name: + doc_comment += delimeter.join( + member for member in field._enum.__members__.keys() + ) + return f"{field._name} = {cls.format_value(field, value.name)}{field_doc}\n{doc_comment}" + doc_comment += delimeter.join( + str(member.value) for member in field._enum.__members__.values() + ) + return f"{field._name} = {cls.format_value(field, value.value)}{field_doc}\n{doc_comment}" - return f"\n[{section_name}]\n{"\n".join(cls.dumps(val) if isinstance(val, spec.Section) else cls.dump_field(val, key, key, val) for key, val in value.items())}" + @classmethod + def dump_field(cls, field: spec.AnyConfigField, value) -> str: + """dump a field object given its value""" + match field: + case spec.Table(spec.Text(), type() | spec.ConfigUnion()) as table_node: + return cls.dump_table(table_node, value) case spec.Section(): - return "\n".join(cls.dump_section(node)) + return "\n".join(cls.dump_section(field)) case spec.ConfigEnum(_, by_name): - if isinstance(value, spec.Section): - return "\n".join(cls.dump_section(value)) + return cls.dump_enum(field, value) + case spec.ConfigurationField(): + # magic method to make writing new field types possible + if hasattr(field, "__write_toml_full__"): + return str(value.__write_toml_full__(field, value)) - field_doc = " " if field._inline_doc and field.doc else "\n" - - if field.doc: - field_doc += f"# {"\n# ".join(field.doc.split("\n"))}" - - if field._enum.__doc__: - delimeter = "\n## - " - doc_comment = f"# {"\n# ".join(field._enum.__doc__.split("\n"))}\n#" - else: - delimeter = "\n# - " - doc_comment = "" - - doc_comment += f"# Available Options for {field_name}:{delimeter}" - if by_name: - doc_comment += delimeter.join( - member for member in field._enum.__members__.keys() - ) - return f"{field_name} = {cls.format_value(value.name)}{field_doc}\n{doc_comment}" - doc_comment += delimeter.join( - str(member.value) for member in field._enum.__members__.values() - ) - return f"{field_name} = {cls.format_value(value.value)}{field_doc}\n{doc_comment}" - case _: if isinstance(value, spec.Section): return "\n".join(cls.dump_section(value)) - real_field = node._ALL_FIELDS[original_name] - doc_comment = ( - " " if real_field._inline_doc and real_field.doc else "\n" - ) - if real_field.doc: - doc_comment += f"# {"\n# ".join(real_field.doc.split("\n"))}" - return f"{field_name} = {cls.format_value(value)}{doc_comment}" + return f"{field._name} = {cls.format_value(field, value)}{cls.create_basic_field_doc(field)}" + case _: + # magic method to make writing new field types possible + if hasattr(field, "__write_toml_full__"): + return str(value.__write_toml_full__(field, value)) + # No known way exists to write this field: + raise ValueError(field) @classmethod def dumps(cls, node) -> str: match node: case spec.Section(): return "\n".join(cls.dump_section(node)) - + case spec.ConfigurationField(): + # Dump passed in node to the best of our ability. This typically looks like dumping its default value + if not node._has_default: + raise ValueError("Node does not have a default value") + return cls.dump_field( + node, + node._default_value, + ) case _: raise ValueError(node) @@ -161,5 +195,6 @@ def load(cls, file): return tomllib.load(f) return tomllib.load(file) - # just alias the name - loads = tomllib.loads + @classmethod + def loads(cls, data: str) -> dict[str, Any]: + return tomllib.loads(data) diff --git a/comprehensiveconfig/utility.py b/comprehensiveconfig/utility.py new file mode 100644 index 0000000..ec5cda4 --- /dev/null +++ b/comprehensiveconfig/utility.py @@ -0,0 +1,58 @@ +import enum +from typing import Type + +import comprehensiveconfig + + +class ExampleEnum(enum.Enum): + example = 10 + something_cool = 20 + + +class ConfigurationWriterTestCase( + comprehensiveconfig.ConfigSpec, + auto_load=False, + create_file=False, +): + """A Configuration Spec to be used as a test-case for any user-made config writers.""" + + class Bar(comprehensiveconfig.spec.Section, name="burger"): + """Section comment example""" + + test_section_item = comprehensiveconfig.spec.Text("clean", doc="Example doc 1") + + test_text = comprehensiveconfig.spec.Text( + "clean", doc="Example doc 2", inline_doc=False + ) + test_int = comprehensiveconfig.spec.Integer(20) + test_float = comprehensiveconfig.spec.Float(20.20) + test_dict = comprehensiveconfig.spec.Table( + {10: "burgers"}, + key_type=comprehensiveconfig.spec.Integer(), + value_type=comprehensiveconfig.spec.Text(), + ) + test_list = comprehensiveconfig.spec.List( + [10, 20, 30], inner_type=comprehensiveconfig.spec.Integer() + ) + + test_enum_value = comprehensiveconfig.spec.ConfigEnum( + ExampleEnum, ExampleEnum.example + ) + test_enum_name = comprehensiveconfig.spec.ConfigEnum( + ExampleEnum, ExampleEnum.example, by_name=True + ) + + +def test_writer_dumps(writer: Type[comprehensiveconfig.configio.ConfigurationWriter]): + """Test the dumps capabilities of a configuration writer""" + config_value = ConfigurationWriterTestCase(None) + + output: str = writer.dumps(config_value) + assert output # ensure an output is created + + for line in output.split("\n"): + assert not line.endswith(" "), f'"{line}"' # ensure no trailing whitespace + + # Ensure all nodes are individually writable + for node in config_value._FIELDS.values(): + assert writer.dumps(node), node diff --git a/tests/test_fields.py b/tests/test_fields.py index f09e43d..9bcd246 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,4 +1,5 @@ import enum +from typing import Any, Self import pytest @@ -248,6 +249,57 @@ class Bar(comprehensiveconfig.spec.Section, name="burger"): assert Foo.Bar.test == "bean" +@pytest.mark.parametrize(("filename", "writer"), parameterize_values) +def test_config_object(filename, writer): + class SomethingCool: + @classmethod + def from_config(cls, config_value: Any) -> Self: + if not isinstance(config_value, str): + raise ValueError(config_value) + return cls(config_value) + + def __init__(self, value: str): + self.value = value + + def __write_toml_value__(self, field, value: str) -> str: + return self.value + + def __write_json_value__(self, field, value: str) -> str: + return self.value + + def __eq__(self, other): + return isinstance(other, SomethingCool) and self.value == other.value + + class Foo( + comprehensiveconfig.ConfigSpec, + auto_load=True, + writer=writer, + default_file=filename, + create_file=True, + ): + """Basic config""" + + test = comprehensiveconfig.spec.ConfigObject( + SomethingCool, SomethingCool("jenkins") + ) + + assert Foo.test.value == "jenkins" + assert isinstance(Foo.test, SomethingCool) + + assert Foo.test == SomethingCool("jenkins") + Foo.test = SomethingCool("flankins") + assert Foo.test == SomethingCool("flankins") + + try: + Foo.test = 12 + assert False + except ValueError: + assert Foo.test == SomethingCool("flankins") + + output: str = writer.dumps(Foo._INST) + assert output + + @pytest.mark.parametrize(("filename", "writer"), parameterize_values) def test_enum(filename, writer): class ExampleEnum(enum.Enum): diff --git a/tests/test_writers.py b/tests/test_writers.py new file mode 100644 index 0000000..8418757 --- /dev/null +++ b/tests/test_writers.py @@ -0,0 +1,9 @@ +import pytest +import comprehensiveconfig.utility +from tests.conftest import parameterize_values + + +@pytest.mark.parametrize(("filename", "writer"), parameterize_values) +def test_run_utilities_tester_dumps(filename: str, writer): + """Run comprehensiveconfig.utilities.test_writer_dumps""" + comprehensiveconfig.utility.test_writer_dumps(writer) From 1d5b0ce5abe1ca1557d98a8fa622f76bc7a47382 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:07:07 -0400 Subject: [PATCH 31/40] Document new objects feature --- comprehensiveconfig/spec.py | 2 +- docs/source/fields.rst | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/comprehensiveconfig/spec.py b/comprehensiveconfig/spec.py index 65988cd..eb04311 100644 --- a/comprehensiveconfig/spec.py +++ b/comprehensiveconfig/spec.py @@ -788,7 +788,7 @@ class ConfigObject[T: ConfigObjectType](ConfigurationField): These include: - `__write_toml_value__(field, value) -> str` (writing a regular toml-parsable value as a string) - `__write_toml_full__(field, value) -> str` (Directly write line(s) of toml when encountering this object) - - `__write_json_value__(field, value) -> int | float | datetime() | str | None` \ + - `__write_json_value__(field, value) -> int | float | datetime | str | None` \ (When encountering this object- convert it to a json serializable format) """ diff --git a/docs/source/fields.rst b/docs/source/fields.rst index 2b1b18a..1b8909e 100644 --- a/docs/source/fields.rst +++ b/docs/source/fields.rst @@ -4,6 +4,9 @@ Fields .. py:currentmodule:: comprehensiveconfig.spec +.. role:: pycode(code) + :language: python + Fields are the most basic unit in Comprehensive Config. They are used to define named values in your configuration file. @@ -237,3 +240,37 @@ Module :type: dict[Any, T] A reversed mapping of values and enum variants (instances) in the enumeration type + +.. py:class:: comprehensiveconfig.spec.ConfigObjectType[T](Protocol) + + A protocol that describes exactly what any serializable object must contain. + + .. py:classmethod:: from_config(config_value: Any) + + :param Any ConfigValue: The incoming value when we want to construct an object of :py:type:`T` from a configuration file being loaded. + + + +.. py:class:: comprehensiveconfig.spec.ConfigObject[T: ConfigObjectType](_type: Type[T], default_value: T | _NoDefaultValueT = NoDefaultValue, /, *args, **kwargs) + + :param Type[T] _type: The class of the enum we want to represent in this field. + :param T | NoDefaultValue default_value: Default value of our field + + This is a way to use an existing python enum (:py:class:`enum.Enum`) as a validated field. + + .. important:: + + Objects must be supported by the specific writer (:py:class:`comprehensiveconfig.configio.ConfigurationWriter`) or must + implement any necessary magic methods required to have them work generically in an existing config-writer. + + This (by default) includes: + - :pycode:`def __write_toml_value__(self, field, value) -> str` (writing a regular toml-parsable value as a string) + - :pycode:`def __write_toml_full__(self, field, value) -> str` (Directly write line(s) of toml when encountering this object) + - :pycode:`def __write_json_value__(self, field, value) -> int | float | datetime | str | None` (When encountering this object- convert it to a json serializable format) + + .. py:attribute:: _holds + :type: T + + .. py:attribute:: _Type + :type: Type[T] + From 2ad06f1b802faccbac590b36d142ac2c19f9ea04 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:16:30 -0400 Subject: [PATCH 32/40] document new utility module --- docs/source/globaltoc.rst | 9 ++++++++- docs/source/index.rst | 7 +++++++ docs/source/utilities.rst | 29 +++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 docs/source/utilities.rst diff --git a/docs/source/globaltoc.rst b/docs/source/globaltoc.rst index 3f4a6d5..8b43376 100644 --- a/docs/source/globaltoc.rst +++ b/docs/source/globaltoc.rst @@ -18,4 +18,11 @@ writers.rst json_writer.rst - toml_writer.rst \ No newline at end of file + toml_writer.rst + +.. toctree:: + :glob: + :maxdepth: 2 + :caption: Utilities: + + utilities.rst \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 17bc1c8..a3c088a 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -35,6 +35,13 @@ loading as well as complex validators for incoming configuration values. json_writer.rst toml_writer.rst +.. toctree:: + :glob: + :maxdepth: 3 + :caption: Utilities: + + utilities.rst + Module ******** diff --git a/docs/source/utilities.rst b/docs/source/utilities.rst new file mode 100644 index 0000000..4586422 --- /dev/null +++ b/docs/source/utilities.rst @@ -0,0 +1,29 @@ +Utilities +=========== + +.. py:currentmodule:: comprehensiveconfig.utility + + +This module is additional utility functions that are useful for building/extending functionality on top of this library. + +.. important:: + This module is NOT included in the `__init__.py` for comprehensiveconfig. + + This means you must import it like this: :code:`import comprehensiveconfig.utility` + +Module +******** + +.. py:function:: test_writer_dumps(writer: Type[ConfigurationWriter]) + + :param Type[ConfigurationWriter] writer: The writer we are testing the dumping functionality of. + + Test that a writer is functioning properly. + This currently means: + - no trailing whitespace + - being able to dump ALL node types (Not just :py:class:`comprehensiveconfig.spec.Section`) + + .. warning:: + + This is NOT a full test suite. This runs a simple case to ensure that what you are using is *mostly* working. + This just makes writing smaller custom writer's easier. If you plan to publish a larger writer on pypi or github, then write more tests! \ No newline at end of file From ac13714369ea5635cecaff42f4f694b5fbfa9baf Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:26:55 -0400 Subject: [PATCH 33/40] fix type goofiness --- comprehensiveconfig/json.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/comprehensiveconfig/json.py b/comprehensiveconfig/json.py index 16025ab..e061341 100644 --- a/comprehensiveconfig/json.py +++ b/comprehensiveconfig/json.py @@ -1,5 +1,6 @@ from datetime import datetime import json +from typing import Any from . import configio from . import spec @@ -66,5 +67,6 @@ def load(cls, file): return json.load(f) return json.load(file) - # just alias the name - loads = json.loads + @classmethod + def loads(cls, data: str) -> dict[str, Any]: + return json.loads(data) From ecd0a41e8be086f7e44eef94c570f5c7c7ffb69e Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:44:21 -0400 Subject: [PATCH 34/40] publish to testpypi --- .github/workflows/pytest.yml | 4 ++-- .github/workflows/test_publish.yml | 32 ++++++++++++++++++++++++++++++ pyproject.toml | 6 ++++++ 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/test_publish.yml diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 2569f58..85f5e4a 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -2,9 +2,9 @@ name: Run Python Tests on: push: - branches: [ "master", "staging"] + branches: [ "main", "staging"] pull_request: - branches: [ "master", "staging" ] + branches: [ "main", "staging" ] permissions: contents: read diff --git a/.github/workflows/test_publish.yml b/.github/workflows/test_publish.yml new file mode 100644 index 0000000..7d75d4d --- /dev/null +++ b/.github/workflows/test_publish.yml @@ -0,0 +1,32 @@ +name: "Publish" + +on: + push: + branches: [ "main", "staging"] + pull_request: + branches: [ "main", "staging" ] + +jobs: + run: + runs-on: ubuntu-latest + environment: + name: pypi + permissions: + id-token: write + contents: read + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Install uv + uses: astral-sh/setup-uv@v7 + - name: Install Python 3.13 + run: uv python install 3.13 + - name: Build + run: uv build + # Check that basic features work and we didn't miss to include crucial files + - name: Smoke test (wheel) + run: uv run --isolated --no-project --with dist/*.whl pytest + - name: Smoke test (source distribution) + run: uv run --isolated --no-project --with dist/*.tar.gz pytest + - name: Publish + run: uv publish --index testpypi diff --git a/pyproject.toml b/pyproject.toml index dd13764..b9b9251 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,3 +60,9 @@ dev = [ "mypy>=1.19.1", "pytest>=9.0.2", ] + +[[tool.uv.index]] +name = "testpypi" +url = "https://test.pypi.org/simple/" +publish-url = "https://test.pypi.org/legacy/" +explicit = true From 3b3e3a469aeb23772f58f9ba8bfbd2b19bd8f7b7 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:45:32 -0400 Subject: [PATCH 35/40] fix things --- .github/workflows/test_publish.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_publish.yml b/.github/workflows/test_publish.yml index 7d75d4d..850987f 100644 --- a/.github/workflows/test_publish.yml +++ b/.github/workflows/test_publish.yml @@ -25,8 +25,8 @@ jobs: run: uv build # Check that basic features work and we didn't miss to include crucial files - name: Smoke test (wheel) - run: uv run --isolated --no-project --with dist/*.whl pytest + run: uv run --isolated --no-project --with dist/*.whl testing.py - name: Smoke test (source distribution) - run: uv run --isolated --no-project --with dist/*.tar.gz pytest + run: uv run --isolated --no-project --with dist/*.tar.gz testing.py - name: Publish run: uv publish --index testpypi From ca6b2fae08c67645a7375fcae180faa50ac10e1f Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:50:05 -0400 Subject: [PATCH 36/40] rename workflow --- .github/workflows/test_publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_publish.yml b/.github/workflows/test_publish.yml index 850987f..c436471 100644 --- a/.github/workflows/test_publish.yml +++ b/.github/workflows/test_publish.yml @@ -1,4 +1,4 @@ -name: "Publish" +name: "Test Publish" on: push: From 7cea8a85f93283c545eedb4c12d85893149b7e7d Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 7 Apr 2026 21:57:49 -0400 Subject: [PATCH 37/40] attempt using testpypi secret key --- .github/workflows/test_publish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_publish.yml b/.github/workflows/test_publish.yml index c436471..a450e0c 100644 --- a/.github/workflows/test_publish.yml +++ b/.github/workflows/test_publish.yml @@ -29,4 +29,4 @@ jobs: - name: Smoke test (source distribution) run: uv run --isolated --no-project --with dist/*.tar.gz testing.py - name: Publish - run: uv publish --index testpypi + run: uv publish --index testpypi --token $TEST_PYPI_SECRET_KEY From 957d8bbc61d7fa782284fcc510c225ea50ee9680 Mon Sep 17 00:00:00 2001 From: Abby Austin <38941820+spidertyler2005@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:00:44 -0400 Subject: [PATCH 38/40] try to correctly use github secret --- .github/workflows/test_publish.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test_publish.yml b/.github/workflows/test_publish.yml index a450e0c..92196ae 100644 --- a/.github/workflows/test_publish.yml +++ b/.github/workflows/test_publish.yml @@ -29,4 +29,6 @@ jobs: - name: Smoke test (source distribution) run: uv run --isolated --no-project --with dist/*.tar.gz testing.py - name: Publish + env: + TEST_PYPI_SECRET_KEY: ${{ secrets.TEST_PYPI_SECRET_KEY }} run: uv publish --index testpypi --token $TEST_PYPI_SECRET_KEY From 45748aef4f5778cdaee1bb843f96d623a406a045 Mon Sep 17 00:00:00 2001 From: ArachnidAbby <38941820+ArachnidAbby@users.noreply.github.com> Date: Tue, 14 Apr 2026 22:54:11 -0400 Subject: [PATCH 39/40] Random version ID's for testpypi pushes --- .github/workflows/test_publish.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_publish.yml b/.github/workflows/test_publish.yml index 92196ae..3eb7798 100644 --- a/.github/workflows/test_publish.yml +++ b/.github/workflows/test_publish.yml @@ -22,7 +22,9 @@ jobs: - name: Install Python 3.13 run: uv python install 3.13 - name: Build - run: uv build + run: | + uv version --bump patch --bump dev=$RANDOM # random version id + uv build # Check that basic features work and we didn't miss to include crucial files - name: Smoke test (wheel) run: uv run --isolated --no-project --with dist/*.whl testing.py From a6e3a25a3adc97bad5631e23dc2a0615c29a61d3 Mon Sep 17 00:00:00 2001 From: ArachnidAbby <38941820+ArachnidAbby@users.noreply.github.com> Date: Mon, 20 Apr 2026 19:54:37 -0400 Subject: [PATCH 40/40] Remove useless line --- tests/test_fields.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_fields.py b/tests/test_fields.py index 9bcd246..8c9973b 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -28,7 +28,6 @@ class Foo(comprehensiveconfig.ConfigSpec, auto_load=True): assert False except ValueError: - type Foo = object assert True