diff --git a/clients/java/bindings/src/main/kotlin/uniffi/superposition_client/superposition_client.kt b/clients/java/bindings/src/main/kotlin/uniffi/superposition_client/superposition_client.kt index 3faa5b687..7a8a62516 100644 --- a/clients/java/bindings/src/main/kotlin/uniffi/superposition_client/superposition_client.kt +++ b/clients/java/bindings/src/main/kotlin/uniffi/superposition_client/superposition_client.kt @@ -767,6 +767,10 @@ internal interface UniffiForeignFutureCompleteVoid : com.sun.jna.Callback { + + + + @@ -793,6 +797,8 @@ fun uniffi_superposition_core_checksum_func_ffi_eval_config_with_reasoning( ): Short fun uniffi_superposition_core_checksum_func_ffi_get_applicable_variants( ): Short +fun uniffi_superposition_core_checksum_func_ffi_parse_config_file_with_filters( +): Short fun uniffi_superposition_core_checksum_func_ffi_parse_json_config( ): Short fun uniffi_superposition_core_checksum_func_ffi_parse_toml_config( @@ -803,6 +809,8 @@ fun uniffi_superposition_core_checksum_method_providercache_filter_config( ): Short fun uniffi_superposition_core_checksum_method_providercache_filter_experiment( ): Short +fun uniffi_superposition_core_checksum_method_providercache_get_applicable_variants( +): Short fun uniffi_superposition_core_checksum_method_providercache_init_config( ): Short fun uniffi_superposition_core_checksum_method_providercache_init_experiments( @@ -871,6 +879,8 @@ fun uniffi_superposition_core_fn_method_providercache_filter_config(`ptr`: Point ): RustBufferConfig.ByValue fun uniffi_superposition_core_fn_method_providercache_filter_experiment(`ptr`: Pointer,`dimensionData`: RustBuffer.ByValue,`prefix`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue +fun uniffi_superposition_core_fn_method_providercache_get_applicable_variants(`ptr`: Pointer,`dimensionData`: RustBuffer.ByValue,`prefix`: RustBuffer.ByValue,`targetingKey`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, +): RustBuffer.ByValue fun uniffi_superposition_core_fn_method_providercache_init_config(`ptr`: Pointer,`defaultConfig`: RustBuffer.ByValue,`contexts`: RustBuffer.ByValue,`overrides`: RustBuffer.ByValue,`dimensions`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, ): Unit fun uniffi_superposition_core_fn_method_providercache_init_experiments(`ptr`: Pointer,`experiments`: RustBuffer.ByValue,`experimentGroups`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, @@ -881,6 +891,8 @@ fun uniffi_superposition_core_fn_func_ffi_eval_config_with_reasoning(`defaultCon ): RustBuffer.ByValue fun uniffi_superposition_core_fn_func_ffi_get_applicable_variants(`eargs`: RustBuffer.ByValue,`dimensionsInfo`: RustBuffer.ByValue,`queryData`: RustBuffer.ByValue,`prefix`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue +fun uniffi_superposition_core_fn_func_ffi_parse_config_file_with_filters(`fileContent`: RustBuffer.ByValue,`format`: RustBuffer.ByValue,`dimensionData`: RustBuffer.ByValue,`prefix`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, +): RustBufferConfig.ByValue fun uniffi_superposition_core_fn_func_ffi_parse_json_config(`jsonContent`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, ): RustBufferConfig.ByValue fun uniffi_superposition_core_fn_func_ffi_parse_toml_config(`tomlContent`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, @@ -1020,6 +1032,9 @@ private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) { if (lib.uniffi_superposition_core_checksum_func_ffi_get_applicable_variants() != 58234.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_superposition_core_checksum_func_ffi_parse_config_file_with_filters() != 63728.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_superposition_core_checksum_func_ffi_parse_json_config() != 30321.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -1035,6 +1050,9 @@ private fun uniffiCheckApiChecksums(lib: IntegrityCheckingUniffiLib) { if (lib.uniffi_superposition_core_checksum_method_providercache_filter_experiment() != 60575.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } + if (lib.uniffi_superposition_core_checksum_method_providercache_get_applicable_variants() != 12269.toShort()) { + throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + } if (lib.uniffi_superposition_core_checksum_method_providercache_init_config() != 28151.toShort()) { throw RuntimeException("UniFFI API checksum mismatch: try cleaning and rebuilding your project") } @@ -1374,6 +1392,8 @@ public interface ProviderCacheInterface { fun `filterExperiment`(`dimensionData`: Map?, `prefix`: List?): ExperimentConfig + fun `getApplicableVariants`(`dimensionData`: Map?, `prefix`: List?, `targetingKey`: kotlin.String): List + fun `initConfig`(`defaultConfig`: Map, `contexts`: List, `overrides`: Map, `dimensions`: Map) fun `initExperiments`(`experiments`: List, `experimentGroups`: List) @@ -1510,6 +1530,19 @@ open class ProviderCache: Disposable, AutoCloseable, ProviderCacheInterface + @Throws(OperationException::class)override fun `getApplicableVariants`(`dimensionData`: Map?, `prefix`: List?, `targetingKey`: kotlin.String): List { + return FfiConverterSequenceString.lift( + callWithPointer { + uniffiRustCallWithError(OperationException) { _status -> + UniffiLib.INSTANCE.uniffi_superposition_core_fn_method_providercache_get_applicable_variants( + it, FfiConverterOptionalMapStringString.lower(`dimensionData`),FfiConverterOptionalSequenceString.lower(`prefix`),FfiConverterString.lower(`targetingKey`),_status) +} + } + ) + } + + + @Throws(OperationException::class)override fun `initConfig`(`defaultConfig`: Map, `contexts`: List, `overrides`: Map, `dimensions`: Map) = callWithPointer { @@ -2286,6 +2319,16 @@ public object FfiConverterMapStringTypeOverrides: FfiConverterRustBuffer?, `prefix`: List?): Config { + return FfiConverterTypeConfig.lift( + uniffiRustCallWithError(OperationException) { _status -> + UniffiLib.INSTANCE.uniffi_superposition_core_fn_func_ffi_parse_config_file_with_filters( + FfiConverterString.lower(`fileContent`),FfiConverterString.lower(`format`),FfiConverterOptionalMapStringString.lower(`dimensionData`),FfiConverterOptionalSequenceString.lower(`prefix`),_status) +} + ) + } + + /** * Parse JSON configuration string * diff --git a/clients/python/bindings/superposition_bindings/superposition_client.py b/clients/python/bindings/superposition_bindings/superposition_client.py index a433e985e..2a8bed303 100644 --- a/clients/python/bindings/superposition_bindings/superposition_client.py +++ b/clients/python/bindings/superposition_bindings/superposition_client.py @@ -501,6 +501,8 @@ def _uniffi_check_api_checksums(lib): raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_superposition_core_checksum_func_ffi_get_applicable_variants() != 58234: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_superposition_core_checksum_func_ffi_parse_config_file_with_filters() != 63728: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_superposition_core_checksum_func_ffi_parse_json_config() != 30321: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_superposition_core_checksum_func_ffi_parse_toml_config() != 1558: @@ -511,6 +513,8 @@ def _uniffi_check_api_checksums(lib): raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_superposition_core_checksum_method_providercache_filter_experiment() != 60575: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") + if lib.uniffi_superposition_core_checksum_method_providercache_get_applicable_variants() != 12269: + raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_superposition_core_checksum_method_providercache_init_config() != 28151: raise InternalError("UniFFI API checksum mismatch: try cleaning and rebuilding your project") if lib.uniffi_superposition_core_checksum_method_providercache_init_experiments() != 55579: @@ -660,6 +664,14 @@ class _UniffiForeignFutureStructVoid(ctypes.Structure): ctypes.POINTER(_UniffiRustCallStatus), ) _UniffiLib.uniffi_superposition_core_fn_method_providercache_filter_experiment.restype = _UniffiRustBuffer +_UniffiLib.uniffi_superposition_core_fn_method_providercache_get_applicable_variants.argtypes = ( + ctypes.c_void_p, + _UniffiRustBuffer, + _UniffiRustBuffer, + _UniffiRustBuffer, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_superposition_core_fn_method_providercache_get_applicable_variants.restype = _UniffiRustBuffer _UniffiLib.uniffi_superposition_core_fn_method_providercache_init_config.argtypes = ( ctypes.c_void_p, _UniffiRustBuffer, @@ -708,6 +720,14 @@ class _UniffiForeignFutureStructVoid(ctypes.Structure): ctypes.POINTER(_UniffiRustCallStatus), ) _UniffiLib.uniffi_superposition_core_fn_func_ffi_get_applicable_variants.restype = _UniffiRustBuffer +_UniffiLib.uniffi_superposition_core_fn_func_ffi_parse_config_file_with_filters.argtypes = ( + _UniffiRustBuffer, + _UniffiRustBuffer, + _UniffiRustBuffer, + _UniffiRustBuffer, + ctypes.POINTER(_UniffiRustCallStatus), +) +_UniffiLib.uniffi_superposition_core_fn_func_ffi_parse_config_file_with_filters.restype = _UniffiRustBufferConfig _UniffiLib.uniffi_superposition_core_fn_func_ffi_parse_json_config.argtypes = ( _UniffiRustBuffer, ctypes.POINTER(_UniffiRustCallStatus), @@ -995,6 +1015,9 @@ class _UniffiForeignFutureStructVoid(ctypes.Structure): _UniffiLib.uniffi_superposition_core_checksum_func_ffi_get_applicable_variants.argtypes = ( ) _UniffiLib.uniffi_superposition_core_checksum_func_ffi_get_applicable_variants.restype = ctypes.c_uint16 +_UniffiLib.uniffi_superposition_core_checksum_func_ffi_parse_config_file_with_filters.argtypes = ( +) +_UniffiLib.uniffi_superposition_core_checksum_func_ffi_parse_config_file_with_filters.restype = ctypes.c_uint16 _UniffiLib.uniffi_superposition_core_checksum_func_ffi_parse_json_config.argtypes = ( ) _UniffiLib.uniffi_superposition_core_checksum_func_ffi_parse_json_config.restype = ctypes.c_uint16 @@ -1010,6 +1033,9 @@ class _UniffiForeignFutureStructVoid(ctypes.Structure): _UniffiLib.uniffi_superposition_core_checksum_method_providercache_filter_experiment.argtypes = ( ) _UniffiLib.uniffi_superposition_core_checksum_method_providercache_filter_experiment.restype = ctypes.c_uint16 +_UniffiLib.uniffi_superposition_core_checksum_method_providercache_get_applicable_variants.argtypes = ( +) +_UniffiLib.uniffi_superposition_core_checksum_method_providercache_get_applicable_variants.restype = ctypes.c_uint16 _UniffiLib.uniffi_superposition_core_checksum_method_providercache_init_config.argtypes = ( ) _UniffiLib.uniffi_superposition_core_checksum_method_providercache_init_config.restype = ctypes.c_uint16 @@ -1723,6 +1749,8 @@ def filter_config(self, dimension_data: "typing.Optional[dict[str, str]]",prefix raise NotImplementedError def filter_experiment(self, dimension_data: "typing.Optional[dict[str, str]]",prefix: "typing.Optional[typing.List[str]]"): raise NotImplementedError + def get_applicable_variants(self, dimension_data: "typing.Optional[dict[str, str]]",prefix: "typing.Optional[typing.List[str]]",targeting_key: "str"): + raise NotImplementedError def init_config(self, default_config: "dict[str, str]",contexts: "typing.List[Context]",overrides: "dict[str, Overrides]",dimensions: "dict[str, DimensionInfo]"): raise NotImplementedError def init_experiments(self, experiments: "typing.List[FfiExperiment]",experiment_groups: "typing.List[FfiExperimentGroup]"): @@ -1803,6 +1831,24 @@ def filter_experiment(self, dimension_data: "typing.Optional[dict[str, str]]",pr + def get_applicable_variants(self, dimension_data: "typing.Optional[dict[str, str]]",prefix: "typing.Optional[typing.List[str]]",targeting_key: "str") -> "typing.List[str]": + _UniffiConverterOptionalMapStringString.check_lower(dimension_data) + + _UniffiConverterOptionalSequenceString.check_lower(prefix) + + _UniffiConverterString.check_lower(targeting_key) + + return _UniffiConverterSequenceString.lift( + _uniffi_rust_call_with_error(_UniffiConverterTypeOperationError,_UniffiLib.uniffi_superposition_core_fn_method_providercache_get_applicable_variants,self._uniffi_clone_pointer(), + _UniffiConverterOptionalMapStringString.lower(dimension_data), + _UniffiConverterOptionalSequenceString.lower(prefix), + _UniffiConverterString.lower(targeting_key)) + ) + + + + + def init_config(self, default_config: "dict[str, str]",contexts: "typing.List[Context]",overrides: "dict[str, Overrides]",dimensions: "dict[str, DimensionInfo]") -> None: _UniffiConverterMapStringString.check_lower(default_config) @@ -1964,6 +2010,22 @@ def ffi_get_applicable_variants(eargs: "ExperimentationArgs",dimensions_info: "d _UniffiConverterOptionalSequenceString.lower(prefix))) +def ffi_parse_config_file_with_filters(file_content: "str",format: "str",dimension_data: "typing.Optional[dict[str, str]]",prefix: "typing.Optional[typing.List[str]]") -> "Config": + _UniffiConverterString.check_lower(file_content) + + _UniffiConverterString.check_lower(format) + + _UniffiConverterOptionalMapStringString.check_lower(dimension_data) + + _UniffiConverterOptionalSequenceString.check_lower(prefix) + + return _UniffiConverterTypeConfig.lift(_uniffi_rust_call_with_error(_UniffiConverterTypeOperationError,_UniffiLib.uniffi_superposition_core_fn_func_ffi_parse_config_file_with_filters, + _UniffiConverterString.lower(file_content), + _UniffiConverterString.lower(format), + _UniffiConverterOptionalMapStringString.lower(dimension_data), + _UniffiConverterOptionalSequenceString.lower(prefix))) + + def ffi_parse_json_config(json_content: "str") -> "Config": """ Parse JSON configuration string @@ -2041,6 +2103,7 @@ def ffi_parse_toml_config(toml_content: "str") -> "Config": "ffi_eval_config", "ffi_eval_config_with_reasoning", "ffi_get_applicable_variants", + "ffi_parse_config_file_with_filters", "ffi_parse_json_config", "ffi_parse_toml_config", "ProviderCache", diff --git a/clients/python/provider-sdk-tests/config.toml b/clients/python/provider-sdk-tests/config.toml new file mode 100644 index 000000000..ea596515c --- /dev/null +++ b/clients/python/provider-sdk-tests/config.toml @@ -0,0 +1,59 @@ +[default-configs] +currency = { value = "Rupee", schema = { enum = [ + "Rupee", + "Dollar", + "Euro", +], type = "string" } } +price = { value = 10000, schema = { minimum = 0, type = "number" } } + + +[dimensions] +city = { position = 3, schema = { type = "string" }, type = "REGULAR" } +customers = { position = 1, schema = { definitions = { gold = { in = [ + { var = "name" }, + [ + "Angit", + "Bhrey", + ], +] }, platinum = { in = [ + { var = "name" }, + [ + "Agush", + "Sauyav", + ], +] } }, enum = [ + "platinum", + "gold", + "otherwise", +], type = "string" }, type = "LOCAL_COHORT:name" } +name = { position = 2, schema = { type = "string" }, type = "REGULAR" } +variantIds = { position = 0, schema = { pattern = ".*", type = "string" }, type = "REGULAR" } + + +[[overrides]] +_context_ = { customers = "platinum" } +price = 5000 + +[[overrides]] +_context_ = { customers = "gold" } +price = 8000 + +[[overrides]] +_context_ = { name = "karbik" } +price = 1 + +[[overrides]] +_context_ = { city = "Boston" } +currency = "Dollar" + +[[overrides]] +_context_ = { city = "Berlin" } +currency = "Euro" + +[[overrides]] +_context_ = { city = "Kolkata", variantIds = "7445772794710855680-test-control" } +price = 8000 + +[[overrides]] +_context_ = { city = "Kolkata", variantIds = "7445772794710855680-test-experimental" } +price = 7000 diff --git a/clients/python/provider-sdk-tests/main.py b/clients/python/provider-sdk-tests/main.py index 577b68b21..1a9f46375 100644 --- a/clients/python/provider-sdk-tests/main.py +++ b/clients/python/provider-sdk-tests/main.py @@ -5,12 +5,9 @@ from openfeature import api from openfeature.evaluation_context import EvaluationContext from smithy_core.documents import Document -from superposition_provider.provider import SuperpositionProvider -from superposition_provider.types import ( - ExperimentationOptions, - PollingStrategy, - SuperpositionProviderOptions, -) + +from superposition_provider import LocalResolutionProvider, HttpDataSource, SuperpositionOptions, PollingStrategy, \ + SuperpositionAPIProvider, FileDataSource from superposition_sdk.client import ( Config, Superposition, @@ -316,34 +313,37 @@ async def setup_with_sdk(client, org_id: str, workspace_id: str): async def run_demo(org_id: str, workspace_id: str): - provider_options = SuperpositionProviderOptions( - refresh_strategy=PollingStrategy( - interval=5, # Poll every 5 seconds - timeout=3, - ), - experimentation_options=ExperimentationOptions( - refresh_strategy=PollingStrategy( - interval=5, # Poll every 5 seconds - timeout=3, # Timeout after 3 seconds - ) - ), - fallback_config=None, - evaluation_cache_options=None, + refresh_strategy = PollingStrategy( + interval=5, + timeout=3, + ) + http_options = SuperpositionOptions( endpoint="http://localhost:8080", token="12345678", org_id=org_id, workspace_id=workspace_id, ) + wrong_http_options = SuperpositionOptions( + endpoint="http://localhost:8080", + token="12345678", + org_id=org_id, + workspace_id="workspace_id", + ) + primary_source = HttpDataSource(http_options) + fallback_source = FileDataSource("config.toml") + try: print("\n=== Starting OpenFeature tests ===\n") print(f"Running on CPU architecture: {platform.machine()}") - provider = SuperpositionProvider(provider_options) + print("Testing local provider with HTTP data source and polling refresh strategy") + + provider = LocalResolutionProvider(primary_source=primary_source, refresh_strategy=refresh_strategy) print("Provider created successfully") # Initialize the provider - await provider.initialize() + await provider.initialize(EvaluationContext()) api.set_provider(provider) print("Provider initialized successfully\n") @@ -351,7 +351,7 @@ async def run_demo(org_id: str, workspace_id: str): # Test 1: Default values (no context) print("Test 1: Default values (no context)") - evaluation_context = EvaluationContext(attributes={}) + evaluation_context = EvaluationContext() price = client.get_integer_value("price", 0, evaluation_context) currency = client.get_string_value("currency", "", evaluation_context) print(f" - Retrieved price: {price}, currency: {currency}") @@ -443,7 +443,124 @@ async def run_demo(org_id: str, workspace_id: str): assert price in [8000, 7000], "Price should be either 8000 (control) or 7000 (experimental) in Kolkata" assert currency == "Rupee", "Currency should be Rupee in Kolkata" print(" ✓ Experiment Test passed\n") + api.shutdown() + print("\n=== All tests passed! ===\n") + except Exception as error: + print(f"\n❌ Error running tests: {error}") + raise error + finally: + print("OpenFeature closed successfully") + + + try: + print("\n=== Starting OpenFeature tests ===\n") + print(f"Running on CPU architecture: {platform.machine()}") + + print("Testing SuperpositionAPIProvider") + + provider = SuperpositionAPIProvider(http_options) + print("Provider created successfully") + + api.set_provider(provider) + print("Provider initialized successfully\n") + + client = api.get_client() + + # Test 1: Default values (no context) + print("Test 1: Default values (no context)") + evaluation_context = EvaluationContext() + price = await client.get_integer_value_async("price", 0, evaluation_context) + currency = await client.get_string_value_async("currency", "", evaluation_context) + print(f" - Retrieved price: {price}, currency: {currency}") + assert price == 10000, "Default price should be 10000" + assert currency == "Rupee", "Default currency should be Rupee" + print(" ✓ Test passed\n") + + # Test 2: Platinum customer - Agush, no city + print("Test 2: Platinum customer - Agush (no city)") + evaluation_context = EvaluationContext(attributes={"name": "Agush"}) + price = await client.get_integer_value_async("price", 0, evaluation_context) + currency = await client.get_string_value_async("currency", "", evaluation_context) + print(f" - Retrieved price: {price}, currency: {currency}") + assert price == 5000, "Price should be default 5000 (platinum customer)" + assert currency == "Rupee", "Currency should be default Rupee" + print(" ✓ Test passed\n") + + # Test 3: Platinum customer - Sauyav, no city + print("Test 3: Platinum customer - Sauyav (no city)") + evaluation_context = EvaluationContext( + attributes={"name": "Sauyav", "city": "Boston"} + ) + price = await client.get_integer_value_async("price", 0, evaluation_context) + currency = await client.get_string_value_async("currency", "", evaluation_context) + print(f" - Retrieved price: {price}, currency: {currency}") + assert price == 5000, "Price should be 5000" + assert currency == "Dollar", "Currency should be dollar" + print(" ✓ Test passed\n") + + print("Test 4: Regular customer - John (no city)") + evaluation_context = EvaluationContext(attributes={"name": "John"}) + price = await client.get_integer_value_async("price", 0, evaluation_context) + currency = await client.get_string_value_async("currency", "", evaluation_context) + print(f" - Retrieved price: {price}, currency: {currency}") + assert price == 10000, "Price should be default 10000" + assert currency == "Rupee", "Currency should be default Rupee" + print(" ✓ Test passed\n") + + print("Test 5: Platinum customer - Sauyav with city Berlin") + evaluation_context = EvaluationContext( + attributes={"name": "Sauyav", "city": "Berlin"} + ) + price = await client.get_integer_value_async("price", 0, evaluation_context) + currency = await client.get_string_value_async("currency", "", evaluation_context) + print(f" - Retrieved price: {price}, currency: {currency}") + assert price == 5000, "Price should be 5000" + assert currency == "Euro", "Currency should be Euro in Berlin" + print(" ✓ Test passed\n") + + print("Test 6: Regular customer - John with city Boston") + evaluation_context = EvaluationContext( + attributes={"name": "John", "city": "Boston"} + ) + price = await client.get_integer_value_async("price", 0, evaluation_context) + currency = await client.get_string_value_async("currency", "", evaluation_context) + print(f" - Retrieved price: {price}, currency: {currency}") + assert price == 10000, "Price should be default 10000" + assert currency == "Dollar", "Currency should be Dollar in Boston" + print(" ✓ Test passed\n") + + print("Test 7: Edge case customer - karbik (specific override)") + evaluation_context = EvaluationContext(attributes={"name": "karbik"}) + price = await client.get_integer_value_async("price", 0, evaluation_context) + currency = await client.get_string_value_async("currency", "", evaluation_context) + print(f" - Retrieved price: {price}, currency: {currency}") + assert price == 1, "Price should be 1 for karbik" + assert currency == "Rupee", "Currency should be default Rupee" + print(" ✓ Test passed\n") + + print("Test 8: Edge case customer - karbik with city Boston") + evaluation_context = EvaluationContext( + attributes={"name": "karbik", "city": "Boston"} + ) + price = await client.get_integer_value_async("price", 0, evaluation_context) + currency = await client.get_string_value_async("currency", "", evaluation_context) + print(f" - Retrieved price: {price}, currency: {currency}") + assert price == 1, "Price should be 1 for karbik" + assert currency == "Dollar", "Currency should be Dollar in Boston" + print(" ✓ Test passed\n") + print("Test 9: Experiment case: Kolkata pricing") + evaluation_context = EvaluationContext( + targeting_key= "test", + attributes={"city": "Kolkata"} + ) + price = await client.get_integer_value_async("price", 0, evaluation_context) + currency = await client.get_string_value_async("currency", "", evaluation_context) + print(f" - Retrieved price: {price}, currency: {currency}") + assert price in [8000, 7000], "Price should be either 8000 (control) or 7000 (experimental) in Kolkata" + assert currency == "Rupee", "Currency should be Rupee in Kolkata" + print(" ✓ Experiment Test passed\n") + api.shutdown() print("\n=== All tests passed! ===\n") except Exception as error: print(f"\n❌ Error running tests: {error}") @@ -451,6 +568,124 @@ async def run_demo(org_id: str, workspace_id: str): finally: print("OpenFeature closed successfully") + try: + print("\n=== Starting OpenFeature tests ===\n") + print(f"Running on CPU architecture: {platform.machine()}") + + print("Testing local provider with wrong HTTP data source and polling refresh strategy, with fallback to file data source") + + provider = LocalResolutionProvider(primary_source=HttpDataSource(wrong_http_options), fallback_source=fallback_source, refresh_strategy=refresh_strategy) + print("Provider created successfully") + + # Initialize the provider + await provider.initialize(EvaluationContext()) + api.set_provider(provider) + print("Provider initialized successfully\n") + + client = api.get_client() + + # Test 1: Default values (no context) + print("Test 1: Default values (no context)") + evaluation_context = EvaluationContext() + price = client.get_integer_value("price", 0, evaluation_context) + currency = client.get_string_value("currency", "", evaluation_context) + print(f" - Retrieved price: {price}, currency: {currency}") + assert price == 10000, "Default price should be 10000" + assert currency == "Rupee", "Default currency should be Rupee" + print(" ✓ Test passed\n") + + # Test 2: Platinum customer - Agush, no city + print("Test 2: Platinum customer - Agush (no city)") + evaluation_context = EvaluationContext(attributes={"name": "Agush"}) + price = client.get_integer_value("price", 0, evaluation_context) + currency = client.get_string_value("currency", "", evaluation_context) + print(f" - Retrieved price: {price}, currency: {currency}") + assert price == 5000, "Price should be default 5000 (platinum customer)" + assert currency == "Rupee", "Currency should be default Rupee" + print(" ✓ Test passed\n") + + # Test 3: Platinum customer - Sauyav, no city + print("Test 3: Platinum customer - Sauyav (no city)") + evaluation_context = EvaluationContext( + attributes={"name": "Sauyav", "city": "Boston"} + ) + price = client.get_integer_value("price", 0, evaluation_context) + currency = client.get_string_value("currency", "", evaluation_context) + print(f" - Retrieved price: {price}, currency: {currency}") + assert price == 5000, "Price should be 5000" + assert currency == "Dollar", "Currency should be dollar" + print(" ✓ Test passed\n") + + print("Test 4: Regular customer - John (no city)") + evaluation_context = EvaluationContext(attributes={"name": "John"}) + price = client.get_integer_value("price", 0, evaluation_context) + currency = client.get_string_value("currency", "", evaluation_context) + print(f" - Retrieved price: {price}, currency: {currency}") + assert price == 10000, "Price should be default 10000" + assert currency == "Rupee", "Currency should be default Rupee" + print(" ✓ Test passed\n") + + print("Test 5: Platinum customer - Sauyav with city Berlin") + evaluation_context = EvaluationContext( + attributes={"name": "Sauyav", "city": "Berlin"} + ) + price = client.get_integer_value("price", 0, evaluation_context) + currency = client.get_string_value("currency", "", evaluation_context) + print(f" - Retrieved price: {price}, currency: {currency}") + assert price == 5000, "Price should be 5000" + assert currency == "Euro", "Currency should be Euro in Berlin" + print(" ✓ Test passed\n") + + print("Test 6: Regular customer - John with city Boston") + evaluation_context = EvaluationContext( + attributes={"name": "John", "city": "Boston"} + ) + price = client.get_integer_value("price", 0, evaluation_context) + currency = client.get_string_value("currency", "", evaluation_context) + print(f" - Retrieved price: {price}, currency: {currency}") + assert price == 10000, "Price should be default 10000" + assert currency == "Dollar", "Currency should be Dollar in Boston" + print(" ✓ Test passed\n") + + print("Test 7: Edge case customer - karbik (specific override)") + evaluation_context = EvaluationContext(attributes={"name": "karbik"}) + price = client.get_integer_value("price", 0, evaluation_context) + currency = client.get_string_value("currency", "", evaluation_context) + print(f" - Retrieved price: {price}, currency: {currency}") + assert price == 1, "Price should be 1 for karbik" + assert currency == "Rupee", "Currency should be default Rupee" + print(" ✓ Test passed\n") + + print("Test 8: Edge case customer - karbik with city Boston") + evaluation_context = EvaluationContext( + attributes={"name": "karbik", "city": "Boston"} + ) + price = client.get_integer_value("price", 0, evaluation_context) + currency = client.get_string_value("currency", "", evaluation_context) + print(f" - Retrieved price: {price}, currency: {currency}") + assert price == 1, "Price should be 1 for karbik" + assert currency == "Dollar", "Currency should be Dollar in Boston" + print(" ✓ Test passed\n") + + print("Experiment not supported in file data source, skipping experiment test") + # print("Test 9: Experiment case: Kolkata pricing") + # evaluation_context = EvaluationContext( + # targeting_key= "test", + # attributes={"city": "Kolkata"} + # ) + # price = client.get_integer_value("price", 0, evaluation_context) + # currency = client.get_string_value("currency", "", evaluation_context) + # print(f" - Retrieved price: {price}, currency: {currency}") + # assert price in [8000, 7000], "Price should be either 8000 (control) or 7000 (experimental) in Kolkata" + # assert currency == "Rupee", "Currency should be Rupee in Kolkata" + # print(" ✓ Experiment Test passed\n") + api.shutdown() + print("\n=== All tests passed! ===\n") + except Exception as error: + print(f"\n❌ Error running tests: {error}") + raise error + finally: + print("OpenFeature closed successfully") async def main(): print("Starting Superposition OpenFeature demo and tests (Python)...") diff --git a/clients/python/provider-sdk-tests/uv.lock b/clients/python/provider-sdk-tests/uv.lock index c57960c17..608a41e90 100644 --- a/clients/python/provider-sdk-tests/uv.lock +++ b/clients/python/provider-sdk-tests/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12" [[package]] @@ -510,16 +510,20 @@ wheels = [ name = "superposition-provider" source = { editable = "../provider" } dependencies = [ + { name = "openfeature-sdk" }, { name = "smithy-core" }, { name = "smithy-http", extra = ["aiohttp"] }, { name = "smithy-json" }, + { name = "watchdog" }, ] [package.metadata] requires-dist = [ + { name = "openfeature-sdk", specifier = "==0.8.3" }, { name = "smithy-core", specifier = "==0.0.1" }, { name = "smithy-http", extras = ["aiohttp"], specifier = "==0.0.1" }, { name = "smithy-json", specifier = "==0.0.1" }, + { name = "watchdog", specifier = "==6.0.0" }, ] [[package]] @@ -552,6 +556,30 @@ 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 = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + [[package]] name = "yarl" version = "1.22.0" diff --git a/clients/python/provider/examples/README.md b/clients/python/provider/examples/README.md new file mode 100644 index 000000000..299830e01 --- /dev/null +++ b/clients/python/provider/examples/README.md @@ -0,0 +1,277 @@ +# Superposition Provider - Python Examples + +This directory contains Python examples demonstrating the Superposition Python provider library, showing various configuration loading and refreshing strategies. + +## Examples + +### 1. `local_file_example.py` - Load from Local File + +**Purpose:** Load configuration from a local TOML file with on-demand refresh. + +**Key Features:** + +- File-based configuration loading +- On-demand refresh strategy (loads once, refreshes only when data becomes stale) +- Simple evaluation context with dimensions + +**Usage:** + +```bash +python local_file_example.py +``` + +**Output:** + +- Prints resolved configuration +- Shows specific flag values (timeout, currency) + +### 2. `local_file_watch_example.py` - Watch File for Changes + +**Purpose:** Watch a local configuration file and automatically reload when it changes. + +**Key Features:** + +- File watching capability +- Automatic reload on file modification +- Periodic polling to display current values + +**Usage:** + +```bash +# Terminal 1: Run the example +python local_file_watch_example.py + +# Terminal 2: Edit config.toml +# The running example will display updated values within 2 seconds +``` + +**Output:** + +- Prints updated configuration every 2 seconds +- Shows when changes are detected + +### 3. `local_http_example.py` - Load from HTTP with Polling + +**Purpose:** Fetch configuration from Superposition API with periodic polling. + +**Prerequisites:** + +- Superposition server running at `http://localhost:8080` + +**Key Features:** + +- HTTP-based configuration fetching +- Polling refresh strategy (checks for updates every 30 seconds) +- Experiment variant resolution + +**Usage:** + +```bash +python local_http_example.py +``` + +**Output:** + +- Prints resolved configuration from API +- Shows applicable experiment variants + +### 4. `local_with_fallback_example.py` - Primary + Fallback Sources + +**Purpose:** Resilient configuration with HTTP primary and local file fallback. + +**Prerequisites:** + +- Superposition server (optional) at `http://localhost:8080` +- `config.toml` file as fallback + +**Key Features:** + +- Primary data source (HTTP) +- Automatic fallback to local file if primary fails +- Polling strategy with 10-second interval +- Demonstrates resilience patterns + +**Usage:** + +```bash +python local_with_fallback_example.py +``` + +**Output:** + +- Prints resolved configuration every 5 seconds +- Uses HTTP when available, falls back to file if needed + +### 5. `polling_example.py` - Configurable Polling via Environment Variables + +**Purpose:** Production-ready example with flexible environment variable configuration. + +**Prerequisites:** + +- Superposition server (optional) at `http://localhost:8080` + +**Key Features:** + +- All settings configurable via environment variables +- Comprehensive logging +- Tracks value changes and logs when they happen +- Suitable for long-running monitoring + +**Configuration via Environment Variables:** + +```bash +export SUPERPOSITION_ENDPOINT="http://localhost:8080" # API endpoint +export SUPERPOSITION_TOKEN="token" # API token +export SUPERPOSITION_ORG_ID="localorg" # Organization ID +export SUPERPOSITION_WORKSPACE="dev" # Workspace ID +export POLL_INTERVAL="10" # Poll frequency (seconds) +export PRINT_INTERVAL="5" # Print frequency (seconds) +export CONFIG_KEY="max_connections" # Key to watch +``` + +**Usage:** + +```bash +# With defaults +python polling_example.py + +# With custom settings +POLL_INTERVAL=5 PRINT_INTERVAL=3 CONFIG_KEY=timeout python polling_example.py + +# Full custom configuration +export SUPERPOSITION_ENDPOINT="http://my-server:8080" +export SUPERPOSITION_ORG_ID="myorg" +export SUPERPOSITION_WORKSPACE="production" +python polling_example.py +``` + +**Output:** + +- Logs configuration changes with timestamps +- Shows which values have been updated +- Tracks polling cycles + +## Sample Configuration (config.toml) + +The `config.toml` file contains sample feature flag configuration: + +```toml +[default-configs] +timeout = { value = 30, schema = { type = "integer" } } +currency = { value = "Rupee", schema = { type = "string", enum = [...] } } +price = { value = 10000, schema = { type = "integer", minimum = 0 } } + +[dimensions] +os = { position = 1, schema = { type = "string" } } +city = { position = 2, schema = { type = "string" } } + +[[overrides]] +_context_ = { os = "linux" } +timeout = 45 + +[[overrides]] +_context_ = { city = "Boston" } +currency = "Dollar" + +[[overrides]] +_context_ = { city = "Berlin" } +currency = "Euro" +``` + +## Running the Examples + +### Setup + +1. Install dependencies: + +```bash +pip install superposition-sdk openfeature +``` + +2. For HTTP examples, ensure Superposition server is running: + +```bash +# Local development server +docker run -p 8080:8080 superposition:latest +``` + +### Run Individual Examples + +```bash +# Local file example +python local_file_example.py + +# File watching example +python local_file_watch_example.py + +# HTTP polling example +python local_http_example.py + +# Fallback example +python local_with_fallback_example.py + +# Environment-based polling +POLL_INTERVAL=5 python polling_example.py +``` + +## Refresh Strategies + +The examples demonstrate different refresh strategies: + +| Strategy | Use Case | Example | +| ------------ | --------------------------------- | --------------------------------------------- | +| **OnDemand** | Load once, refresh when stale | `local_file_example.py` | +| **Watch** | React to file changes immediately | `local_file_watch_example.py` | +| **Polling** | Regular interval checks | `local_http_example.py`, `polling_example.py` | +| **Manual** | Caller-driven refresh | (Custom implementation) | + +## Evaluation Contexts + +Examples show how to create evaluation contexts with: + +- **Targeting Key:** Identifies the user/entity +- **Attributes:** Dimensional data for flag evaluation + +```python +context = EvaluationContext( + targeting_key="user-1234", + attributes={ + "os": "linux", + "city": "Boston", + "source": "mobile_app", + }, +) +``` + +## Error Handling + +All examples implement proper error handling: + +- Graceful shutdown with `Ctrl-C` +- Connection error resilience +- Logging for debugging + +## Next Steps + +- Explore different refresh strategies for your use case +- Combine with OpenFeature client for standardized flag evaluation +- Implement custom data sources for specialized backends +- Monitor performance metrics from provider statistics + +## Troubleshooting + +**Provider won't initialize:** + +- Check that required dependencies are installed +- For HTTP examples, verify Superposition server is reachable + +**Config not updating:** + +- Verify file is being modified (watch example) +- Check polling interval is not too long +- Review logs for error messages + +**Flags not resolved:** + +- Ensure evaluation context has required dimensions +- Check config file syntax for TOML errors diff --git a/clients/python/provider/examples/__init__.py b/clients/python/provider/examples/__init__.py new file mode 100644 index 000000000..9a1653daf --- /dev/null +++ b/clients/python/provider/examples/__init__.py @@ -0,0 +1,13 @@ +""" +Superposition Provider Examples + +This package contains practical examples demonstrating the Superposition Python provider +with different refresh strategies and data sources. + +Examples included: +- local_file_example: File-based configuration with on-demand refresh +- local_file_watch_example: Watch local files for changes +- local_http_example: HTTP-based polling with Superposition API +- local_with_fallback_example: Primary HTTP + local file fallback +- polling_example: Configurable polling with environment variables +""" diff --git a/clients/python/provider/examples/config.toml b/clients/python/provider/examples/config.toml new file mode 100644 index 000000000..2ea4a65cc --- /dev/null +++ b/clients/python/provider/examples/config.toml @@ -0,0 +1,24 @@ +[default-configs] +timeout = { value = 30, schema = { type = "integer" } } +currency = { value = "Rupee", schema = { type = "string", enum = [ + "Rupee", + "Dollar", + "Euro", +] } } +price = { value = 10000, schema = { type = "integer", minimum = 0 } } + +[dimensions] +os = { position = 1, schema = { type = "string" } } +city = { position = 2, schema = { type = "string" } } + +[[overrides]] +_context_ = { os = "linux" } +timeout = 45 + +[[overrides]] +_context_ = { city = "Boston" } +currency = "Dollar" + +[[overrides]] +_context_ = { city = "Berlin" } +currency = "Euro" diff --git a/clients/python/provider/examples/local_file_example.py b/clients/python/provider/examples/local_file_example.py new file mode 100644 index 000000000..96ca9aa35 --- /dev/null +++ b/clients/python/provider/examples/local_file_example.py @@ -0,0 +1,62 @@ +""" +Local File Example - Load configuration from a local TOML file. + +Demonstrates basic file-based configuration loading with on-demand refresh strategy. +The provider loads the config once and refreshes only when data becomes stale (TTL). +""" + +import asyncio +import logging +from pathlib import Path + +from openfeature.evaluation_context import EvaluationContext + +from superposition_provider import FileDataSource, LocalResolutionProvider +from superposition_provider.types import OnDemandStrategy + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +async def main(): + """Run the local file example.""" + # Get the path to the example config file + example_dir = Path(__file__).parent + config_path = example_dir / "config.toml" + + logger.info(f"Loading config from: {config_path}") + + # Create a provider using FileDataSource with OnDemandStrategy + provider = LocalResolutionProvider( + primary_source=FileDataSource(config_path.__str__()), + refresh_strategy=OnDemandStrategy(ttl=60), + ) + + # Initialize the provider + await provider.initialize(EvaluationContext()) + + # Create an evaluation context + context = EvaluationContext( + targeting_key=None, + attributes={ + "os": "linux", + "city": "Boston", + }, + ) + + # Resolve all features + config = provider.resolve_all_features(context) + logger.info(f"Config: {config}") + + # Resolve a specific feature + timeout = provider.resolve_integer_details("timeout", 0, context) + currency = provider.resolve_string_details("currency", "No Value", context) + logger.info(f"Timeout: {timeout.value}, Currency: {currency.value}") + + # Shutdown + await provider.shutdown() + logger.info("Provider shutdown complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/clients/python/provider/examples/local_file_watch_example.py b/clients/python/provider/examples/local_file_watch_example.py new file mode 100644 index 000000000..92b2e32bd --- /dev/null +++ b/clients/python/provider/examples/local_file_watch_example.py @@ -0,0 +1,64 @@ +""" +Local File Watch Example - Watch a local config file for changes. + +Demonstrates file watching capability with automatic reload when the file changes. +Edit the config file in another terminal to see the changes reflected in the running example. +""" + +import asyncio +import logging +from pathlib import Path + +from openfeature.evaluation_context import EvaluationContext + +from superposition_provider import LocalResolutionProvider, FileDataSource +from superposition_provider.types import WatchStrategy + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +async def main(): + """Run the file watch example.""" + # Get the path to the example config file + example_dir = Path(__file__).parent + config_path = example_dir / "config.toml" + + logger.info(f"Watching config file: {config_path}") + logger.info("Edit the file in another terminal to see changes.\n") + + # Create a provider using FileDataSource with WatchStrategy + provider = LocalResolutionProvider( + primary_source=FileDataSource(config_path.__str__()), + refresh_strategy=WatchStrategy(debounce_ms=1000), + ) + + # Initialize the provider + await provider.initialize(EvaluationContext()) + + # Create an evaluation context + context = EvaluationContext( + targeting_key=None, + attributes={ + "os": "linux", + "city": "Boston", + }, + ) + + logger.info("Polling config every 2 seconds. Press Ctrl-C to stop.\n") + + try: + # Poll in a loop to show updated values after file changes + while True: + config = provider.resolve_all_features(context) + logger.info(f"Config: {config}") + await asyncio.sleep(2) + except KeyboardInterrupt: + logger.info("\nShutting down...") + finally: + await provider.shutdown() + logger.info("Provider shutdown complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/clients/python/provider/examples/local_http_example.py b/clients/python/provider/examples/local_http_example.py new file mode 100644 index 000000000..dbb2e812c --- /dev/null +++ b/clients/python/provider/examples/local_http_example.py @@ -0,0 +1,68 @@ +""" +Local HTTP Example - Load configuration from Superposition API with polling. + +Demonstrates HTTP-based configuration fetching with periodic polling. +The provider polls the Superposition server for config updates every 30 seconds. + +Prerequisites: + - Superposition server running at http://localhost:8080 +""" + +import asyncio +import logging + +from openfeature.evaluation_context import EvaluationContext + +from superposition_provider import LocalResolutionProvider, HttpDataSource +from superposition_provider.types import SuperpositionOptions, PollingStrategy + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +async def main(): + """Run the HTTP example.""" + # Configure Superposition API connection + options = SuperpositionOptions( + endpoint="http://localhost:8080", + token="token", + org_id="localorg", + workspace_id="dev", + ) + + logger.info("Creating HTTP-based provider with polling strategy...") + + # Create a provider using HttpDataSource with PollingStrategy + provider = LocalResolutionProvider( + primary_source=HttpDataSource(options), + refresh_strategy=PollingStrategy(interval=30, timeout=10), + ) + + # Initialize the provider + await provider.initialize(EvaluationContext()) + + # Create an evaluation context with targeting key and dimensions + context = EvaluationContext( + targeting_key="user-1234", + attributes={ + "dimension": "d2", + }, + ) + + # Resolve all features + logger.info("Resolving all features...") + all_config = provider.resolve_all_features(context) + logger.info(f"All config: {all_config}") + + # Resolve applicable experiment variants + logger.info("Getting applicable variants...") + variants = await provider.get_applicable_variants(context) + logger.info(f"Applicable variants: {variants}") + + # Shutdown + await provider.shutdown() + logger.info("Provider shutdown complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/clients/python/provider/examples/local_with_fallback_example.py b/clients/python/provider/examples/local_with_fallback_example.py new file mode 100644 index 000000000..d86a6514b --- /dev/null +++ b/clients/python/provider/examples/local_with_fallback_example.py @@ -0,0 +1,85 @@ +""" +Local with Fallback Example - Use HTTP with local file as fallback. + +Demonstrates resilience with primary + fallback data sources. +If the HTTP server fails, the provider automatically falls back to local file config. + +Prerequisites: + - Superposition server (optional) at http://localhost:8080 + - config.toml file for fallback +""" + +import asyncio +import logging +from pathlib import Path + +from openfeature.evaluation_context import EvaluationContext + +from superposition_provider import LocalResolutionProvider, HttpDataSource, FileDataSource +from superposition_provider.types import SuperpositionOptions, PollingStrategy + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +async def main(): + """Run the local with fallback example.""" + # Get the path to the example config file + example_dir = Path(__file__).parent + config_path = example_dir / "config.toml" + + logger.info("=== Superposition Fallback + Polling Example ===") + logger.info(f"Primary: HTTP (localhost:8080)") + logger.info(f"Fallback: {config_path}") + logger.info("Polling every 10s.\n") + + # Configure Superposition API connection + options = SuperpositionOptions( + endpoint="http://localhost:8080", + token="token", + org_id="localorg", + workspace_id="dev", + ) + + # Create a provider with both HTTP (primary) and file (fallback) sources + provider = LocalResolutionProvider( + primary_source=HttpDataSource(options), + refresh_strategy=PollingStrategy(interval=10, timeout=10), + fallback_source=FileDataSource(config_path.__str__()), + ) + + # Initialize the provider + await provider.initialize(EvaluationContext()) + + # Create an evaluation context + context = EvaluationContext( + targeting_key="user-456", + attributes={ + "os": "linux", + "city": "Berlin", + }, + ) + + logger.info("Polling config every 5s. Press Ctrl-C to stop.\n") + + try: + # Poll in a loop to show resolved config + while True: + logger.info("Fetching config...") + config = provider.resolve_all_features(context) + logger.info(f"Resolved config: {config}") + + # Also show a specific value + currency = provider.resolve_string_details("currency", "No value", context) + logger.info(f"Currency: {currency.value}\n") + + await asyncio.sleep(5) + except KeyboardInterrupt: + logger.info("\nShutting down...") + finally: + await provider.shutdown() + logger.info("Provider shutdown complete") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/clients/python/provider/examples/polling_example.py b/clients/python/provider/examples/polling_example.py new file mode 100644 index 000000000..3347abeb4 --- /dev/null +++ b/clients/python/provider/examples/polling_example.py @@ -0,0 +1,131 @@ +""" +Polling Example - HTTP polling with environment variable configuration. + +Demonstrates the Polling refresh strategy with LocalResolutionProvider +using environment variables for flexible configuration. + +Change the config on the server and watch printed values update automatically. + +Usage: + python polling_example.py + +Environment variables (all optional, with defaults shown): + SUPERPOSITION_ENDPOINT http://localhost:8080 + SUPERPOSITION_TOKEN token + SUPERPOSITION_ORG_ID localorg + SUPERPOSITION_WORKSPACE dev + POLL_INTERVAL 10 (seconds between server polls) + PRINT_INTERVAL 5 (seconds between printing the value) + CONFIG_KEY max_connections (the config key to watch) +""" + +import asyncio +import logging +import os +from typing import Optional + +from openfeature.evaluation_context import EvaluationContext + +from superposition_provider import LocalResolutionProvider, HttpDataSource +from superposition_provider.types import SuperpositionOptions, PollingStrategy + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", +) +logger = logging.getLogger(__name__) + + +def env_or(key: str, default: str) -> str: + """Get environment variable or return default.""" + return os.environ.get(key, default) + + +async def main(): + """Run the polling example.""" + # Read configuration from environment variables + endpoint = env_or("SUPERPOSITION_ENDPOINT", "http://localhost:8080") + token = env_or("SUPERPOSITION_TOKEN", "token") + org_id = env_or("SUPERPOSITION_ORG_ID", "localorg") + workspace = env_or("SUPERPOSITION_WORKSPACE", "dev") + poll_interval = int(env_or("POLL_INTERVAL", "10")) + print_interval = int(env_or("PRINT_INTERVAL", "5")) + config_key = env_or("CONFIG_KEY", "max_connections") + + logger.info("=== Superposition Polling Example ===") + logger.info(f"Endpoint: {endpoint}") + logger.info(f"Org / Workspace: {org_id} / {workspace}") + logger.info(f"Poll interval: {poll_interval}s") + logger.info(f"Print interval: {print_interval}s") + logger.info(f"Watching key: {config_key}") + logger.info("") + + # Create Superposition API options + options = SuperpositionOptions( + endpoint=endpoint, + token=token, + org_id=org_id, + workspace_id=workspace, + ) + + # Create provider with polling strategy + logger.info("Creating provider with polling strategy...") + provider = LocalResolutionProvider( + primary_source=HttpDataSource(options), + refresh_strategy=PollingStrategy( + interval=poll_interval, + timeout=10, + ), + ) + + # Initialize + await provider.initialize(EvaluationContext()) + logger.info("Provider initialized.\n") + + # Create evaluation context + context = EvaluationContext( + targeting_key="polling-example-user", + attributes={ + "source": "polling_example", + }, + ) + + logger.info("Polling config. Press Ctrl-C to stop.\n") + + try: + last_value: Optional[str] = None + iteration = 0 + + while True: + iteration += 1 + + # Every print_interval seconds, read and print the watched key + if iteration % max(1, (print_interval // poll_interval)) == 0: + try: + # Resolve the watched config key + result = provider.resolve_all_features(context) + + if config_key in result: + current_value = str(result[config_key]) + if current_value != last_value: + logger.info(f"[UPDATE] {config_key}: {current_value}") + last_value = current_value + else: + logger.info(f"[UNCHANGED] {config_key}: {current_value}") + else: + logger.warning(f"Key '{config_key}' not found in config") + logger.info(f"Available keys: {list(result.keys())}") + except Exception as e: + logger.error(f"Error resolving config: {e}") + + await asyncio.sleep(poll_interval) + + except KeyboardInterrupt: + logger.info("\n\nShutting down as requested...") + finally: + await provider.shutdown() + logger.info("Cleanup complete. Goodbye!") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/clients/python/provider/examples/pyproject.toml b/clients/python/provider/examples/pyproject.toml new file mode 100644 index 000000000..a649dd5fe --- /dev/null +++ b/clients/python/provider/examples/pyproject.toml @@ -0,0 +1,19 @@ +[project] +name = "provider-examples" +version = "1.0.0" +description = "Examples for using Superposition Provider SDK" +readme = "README.md" +requires-python = ">=3.12" +authors = [{ name = "Superposition Team" }] +license = { text = "Apache-2.0" } + +dependencies = [ + "superposition-provider", + "superposition-sdk", + "openfeature-sdk==0.8.3", +] + +[tool.uv.sources] +superposition-provider = { path = "../../provider", editable = true } +superposition-sdk = { path = "../../sdk", editable = true } +superposition-bindings = { path = "../../bindings", editable = true } diff --git a/clients/python/provider/examples/uv.lock b/clients/python/provider/examples/uv.lock new file mode 100644 index 000000000..a1c46875b --- /dev/null +++ b/clients/python/provider/examples/uv.lock @@ -0,0 +1,707 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, + { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, + { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, + { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, + { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, + { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, + { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, + { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, + { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[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 = "ijson" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/57/60d1a6a512f2f0508d0bc8b4f1cc5616fd3196619b66bd6a01f9155a1292/ijson-3.5.0.tar.gz", hash = "sha256:94688760720e3f5212731b3cb8d30267f9a045fb38fb3870254e7b9504246f31", size = 68658, upload-time = "2026-02-24T03:58:30.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/17/9c63c7688025f3a8c47ea717b8306649c8c7244e49e20a2be4e3515dc75c/ijson-3.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1ebefbe149a6106cc848a3eaf536af51a9b5ccc9082de801389f152dba6ab755", size = 88536, upload-time = "2026-02-24T03:57:06.809Z" }, + { url = "https://files.pythonhosted.org/packages/6f/dd/e15c2400244c117b06585452ebc63ae254f5a6964f712306afd1422daae0/ijson-3.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:19e30d9f00f82e64de689c0b8651b9cfed879c184b139d7e1ea5030cec401c21", size = 60499, upload-time = "2026-02-24T03:57:09.155Z" }, + { url = "https://files.pythonhosted.org/packages/77/a9/bf4fe3538a0c965f16b406f180a06105b875da83f0743e36246be64ef550/ijson-3.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a04a33ee78a6f27b9b8528c1ca3c207b1df3b8b867a4cf2fcc4109986f35c227", size = 60330, upload-time = "2026-02-24T03:57:10.574Z" }, + { url = "https://files.pythonhosted.org/packages/31/76/6f91bdb019dd978fce1bc5ea1cd620cfc096d258126c91db2c03a20a7f34/ijson-3.5.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7d48dc2984af02eb3c56edfb3f13b3f62f2f3e4fe36f058c8cfc75d93adf4fed", size = 138977, upload-time = "2026-02-24T03:57:11.932Z" }, + { url = "https://files.pythonhosted.org/packages/11/be/bbc983059e48a54b0121ee60042979faed7674490bbe7b2c41560db3f436/ijson-3.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1e73a44844d9adbca9cf2c4132cd875933e83f3d4b23881fcaf82be83644c7d", size = 149785, upload-time = "2026-02-24T03:57:13.255Z" }, + { url = "https://files.pythonhosted.org/packages/6d/81/2fee58f9024a3449aee83edfa7167fb5ccd7e1af2557300e28531bb68e16/ijson-3.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7389a56b8562a19948bdf1d7bae3a2edc8c7f86fb59834dcb1c4c722818e645a", size = 149729, upload-time = "2026-02-24T03:57:14.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/56/f1706761fcc096c9d414b3dcd000b1e6e5c24364c21cfba429837f98ee8d/ijson-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3176f23f8ebec83f374ed0c3b4e5a0c4db7ede54c005864efebbed46da123608", size = 150697, upload-time = "2026-02-24T03:57:15.855Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6e/ee0d9c875a0193b632b3e9ccd1b22a50685fb510256ad57ba483b6529f77/ijson-3.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6babd88e508630c6ef86c9bebaaf13bb2fb8ec1d8f8868773a03c20253f599bc", size = 142873, upload-time = "2026-02-24T03:57:16.831Z" }, + { url = "https://files.pythonhosted.org/packages/d2/bf/f9d4399d0e6e3fd615035290a71e97c843f17f329b43638c0a01cf112d73/ijson-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dc1b3836b174b6db2fa8319f1926fb5445abd195dc963368092103f8579cb8ed", size = 151583, upload-time = "2026-02-24T03:57:17.757Z" }, + { url = "https://files.pythonhosted.org/packages/b2/71/a7254a065933c0e2ffd3586f46187d84830d3d7b6f41cfa5901820a4f87d/ijson-3.5.0-cp312-cp312-win32.whl", hash = "sha256:6673de9395fb9893c1c79a43becd8c8fbee0a250be6ea324bfd1487bb5e9ee4c", size = 53079, upload-time = "2026-02-24T03:57:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/8f/7b/2edca79b359fc9f95d774616867a03ecccdf333797baf5b3eea79733918c/ijson-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f4f7fabd653459dcb004175235f310435959b1bb5dfa8878578391c6cc9ad944", size = 55500, upload-time = "2026-02-24T03:57:20.428Z" }, + { url = "https://files.pythonhosted.org/packages/a2/71/d67e764a712c3590627480643a3b51efcc3afa4ef3cb54ee4c989073c97e/ijson-3.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e9cedc10e40dd6023c351ed8bfc7dcfce58204f15c321c3c1546b9c7b12562a4", size = 88544, upload-time = "2026-02-24T03:57:21.293Z" }, + { url = "https://files.pythonhosted.org/packages/1a/39/f1c299371686153fa3cf5c0736b96247a87a1bee1b7145e6d21f359c505a/ijson-3.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3647649f782ee06c97490b43680371186651f3f69bebe64c6083ee7615d185e5", size = 60495, upload-time = "2026-02-24T03:57:22.501Z" }, + { url = "https://files.pythonhosted.org/packages/16/94/b1438e204d75e01541bebe3e668fe3e68612d210e9931ae1611062dd0a56/ijson-3.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:90e74be1dce05fce73451c62d1118671f78f47c9f6be3991c82b91063bf01fc9", size = 60325, upload-time = "2026-02-24T03:57:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/30/e2/4aa9c116fa86cc8b0f574f3c3a47409edc1cd4face05d0e589a5a176b05d/ijson-3.5.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:78e9ad73e7be2dd80627504bd5cbf512348c55ce2c06e362ed7683b5220e8568", size = 138774, upload-time = "2026-02-24T03:57:24.683Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d2/738b88752a70c3be1505faa4dcd7110668c2712e582a6a36488ed1e295d4/ijson-3.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9577449313cc94be89a4fe4b3e716c65f09cc19636d5a6b2861c4e80dddebd58", size = 149820, upload-time = "2026-02-24T03:57:26.062Z" }, + { url = "https://files.pythonhosted.org/packages/ed/df/0b3ab9f393ca8f72ea03bc896ba9fdc987e90ae08cdb51c32a4ee0c14d5e/ijson-3.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e4c1178fb50aff5f5701a30a5152ead82a14e189ce0f6102fa1b5f10b2f54ff", size = 149747, upload-time = "2026-02-24T03:57:27.308Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a3/b0037119f75131b78cb00acc2657b1a9d0435475f1f2c5f8f5a170b66b9c/ijson-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0eb402ab026ffb37a918d75af2b7260fe6cfbce13232cc83728a714dd30bd81d", size = 151027, upload-time = "2026-02-24T03:57:28.522Z" }, + { url = "https://files.pythonhosted.org/packages/22/a0/cb344de1862bf09d8f769c9d25c944078c87dd59a1b496feec5ad96309a4/ijson-3.5.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5b08ee08355f9f729612a8eb9bf69cc14f9310c3b2a487c6f1c3c65d85216ec4", size = 142996, upload-time = "2026-02-24T03:57:29.774Z" }, + { url = "https://files.pythonhosted.org/packages/ca/32/a8ffd67182e02ea61f70f62daf43ded4fa8a830a2520a851d2782460aba8/ijson-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bda62b6d48442903e7bf56152108afb7f0f1293c2b9bef2f2c369defea76ab18", size = 152068, upload-time = "2026-02-24T03:57:30.969Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d1/3578df8e75d446aab0ae92e27f641341f586b85e1988536adebc65300cb4/ijson-3.5.0-cp313-cp313-win32.whl", hash = "sha256:8d073d9b13574cfa11083cc7267c238b7a6ed563c2661e79192da4a25f09c82c", size = 53065, upload-time = "2026-02-24T03:57:31.93Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a2/f7cdaf5896710da3e69e982e44f015a83d168aa0f3a89b6f074b5426779d/ijson-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:2419f9e32e0968a876b04d8f26aeac042abd16f582810b576936bbc4c6015069", size = 55499, upload-time = "2026-02-24T03:57:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/13e2492d17e19a2084523e18716dc2809159f2287fd2700c735f311e76c4/ijson-3.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:4d4b0cd676b8c842f7648c1a783448fac5cd3b98289abd83711b3e275e143524", size = 93019, upload-time = "2026-02-24T03:57:33.976Z" }, + { url = "https://files.pythonhosted.org/packages/33/92/483fc97ece0c3f1cecabf48f6a7a36e89d19369eec462faaeaa34c788992/ijson-3.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:252dec3680a48bb82d475e36b4ae1b3a9d7eb690b951bb98a76c5fe519e30188", size = 62714, upload-time = "2026-02-24T03:57:34.819Z" }, + { url = "https://files.pythonhosted.org/packages/4b/88/793fe020a0fe9d9eed4c285cf4a5cfdb0a935708b3bde0d72f35c794b513/ijson-3.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:aa1b5dca97d323931fde2501172337384c958914d81a9dac7f00f0d4bfc76bc7", size = 62460, upload-time = "2026-02-24T03:57:35.874Z" }, + { url = "https://files.pythonhosted.org/packages/51/69/f1a2690aa8d4df1f4e262b385e65a933ffdc250b091531bac9a449c19e16/ijson-3.5.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7a5ec7fd86d606094bba6f6f8f87494897102fa4584ef653f3005c51a784c320", size = 199273, upload-time = "2026-02-24T03:57:37.07Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a2/f1346d5299e79b988ab472dc773d5381ec2d57c23cb2f1af3ede4a810e62/ijson-3.5.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:009f41443e1521847701c6d87fa3923c0b1961be3c7e7de90947c8cb92ea7c44", size = 216884, upload-time = "2026-02-24T03:57:38.346Z" }, + { url = "https://files.pythonhosted.org/packages/28/3c/8b637e869be87799e6c2c3c275a30a546f086b1aed77e2b7f11512168c5a/ijson-3.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4c3651d1f9fe2839a93fdf8fd1d5ca3a54975349894249f3b1b572bcc4bd577", size = 207306, upload-time = "2026-02-24T03:57:39.718Z" }, + { url = "https://files.pythonhosted.org/packages/7f/7c/18b1c1df6951ca056782d7580ec40cea4ff9a27a0947d92640d1cc8c4ae3/ijson-3.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:945b7abcfcfeae2cde17d8d900870f03536494245dda7ad4f8d056faa303256c", size = 211364, upload-time = "2026-02-24T03:57:40.953Z" }, + { url = "https://files.pythonhosted.org/packages/f3/55/e795812e82851574a9dba8a53fde045378f531ef14110c6fb55dbd23b443/ijson-3.5.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0574b0a841ff97495c13e9d7260fbf3d85358b061f540c52a123db9dbbaa2ed6", size = 200608, upload-time = "2026-02-24T03:57:42.272Z" }, + { url = "https://files.pythonhosted.org/packages/5c/cd/013c85b4749b57a4cb4c2670014d1b32b8db4ab1a7be92ea7aeb5d7fe7b5/ijson-3.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f969ffb2b89c5cdf686652d7fb66252bc72126fa54d416317411497276056a18", size = 205127, upload-time = "2026-02-24T03:57:43.286Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7c/faf643733e3ab677f180018f6a855c4ef70b7c46540987424c563c959e42/ijson-3.5.0-cp313-cp313t-win32.whl", hash = "sha256:59d3f9f46deed1332ad669518b8099920512a78bda64c1f021fcd2aff2b36693", size = 55282, upload-time = "2026-02-24T03:57:44.353Z" }, + { url = "https://files.pythonhosted.org/packages/69/22/94ddb47c24b491377aca06cd8fc9202cad6ab50619842457d2beefde21ea/ijson-3.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c2839fa233746d8aad3b8cd2354e441613f5df66d721d59da4a09394bd1db2b", size = 58016, upload-time = "2026-02-24T03:57:45.237Z" }, + { url = "https://files.pythonhosted.org/packages/7a/93/0868efe753dc1df80cc405cf0c1f2527a6991643607c741bff8dcb899b3b/ijson-3.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:25a5a6b2045c90bb83061df27cfa43572afa43ba9408611d7bfe237c20a731a9", size = 89094, upload-time = "2026-02-24T03:57:46.115Z" }, + { url = "https://files.pythonhosted.org/packages/24/94/fd5a832a0df52ef5e4e740f14ac8640725d61034a1b0c561e8b5fb424706/ijson-3.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:8976c54c0b864bc82b951bae06567566ac77ef63b90a773a69cd73aab47f4f4f", size = 60715, upload-time = "2026-02-24T03:57:47.552Z" }, + { url = "https://files.pythonhosted.org/packages/70/79/1b9a90af5732491f9eec751ee211b86b11011e1158c555c06576d52c3919/ijson-3.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:859eb2038f7f1b0664df4241957694cc35e6295992d71c98659b22c69b3cbc10", size = 60638, upload-time = "2026-02-24T03:57:48.428Z" }, + { url = "https://files.pythonhosted.org/packages/23/6f/2c551ea980fe56f68710a8d5389cfbd015fc45aaafd17c3c52c346db6aa1/ijson-3.5.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c911aa02991c7c0d3639b6619b93a93210ff1e7f58bf7225d613abea10adc78e", size = 140667, upload-time = "2026-02-24T03:57:49.314Z" }, + { url = "https://files.pythonhosted.org/packages/25/0e/27b887879ba6a5bc29766e3c5af4942638c952220fd63e1e442674f7883a/ijson-3.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:903cbdc350173605220edc19796fbea9b2203c8b3951fb7335abfa8ed37afda8", size = 149850, upload-time = "2026-02-24T03:57:50.329Z" }, + { url = "https://files.pythonhosted.org/packages/da/1e/23e10e1bc04bf31193b21e2960dce14b17dbd5d0c62204e8401c59d62c08/ijson-3.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4549d96ded5b8efa71639b2160235415f6bdb8c83367615e2dbabcb72755c33", size = 149206, upload-time = "2026-02-24T03:57:51.261Z" }, + { url = "https://files.pythonhosted.org/packages/8e/90/e552f6495063b235cf7fa2c592f6597c057077195e517b842a0374fd470c/ijson-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6b2dcf6349e6042d83f3f8c39ce84823cf7577eba25bac5aae5e39bbbbbe9c1c", size = 150438, upload-time = "2026-02-24T03:57:52.198Z" }, + { url = "https://files.pythonhosted.org/packages/5c/18/45bf8f297c41b42a1c231d261141097babd953d2c28a07be57ae4c3a1a02/ijson-3.5.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e44af39e6f8a17e5627dcd89715d8279bf3474153ff99aae031a936e5c5572e5", size = 144369, upload-time = "2026-02-24T03:57:53.22Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/deb9772bb2c0cead7ad64f00c3598eec9072bdf511818e70e2c512eeabbe/ijson-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9260332304b7e7828db56d43f08fc970a3ab741bf84ff10189361ea1b60c395b", size = 151352, upload-time = "2026-02-24T03:57:54.375Z" }, + { url = "https://files.pythonhosted.org/packages/e4/51/67f4d80cd58ad7eab0cd1af5fe28b961886338956b2f88c0979e21914346/ijson-3.5.0-cp314-cp314-win32.whl", hash = "sha256:63bc8121bb422f6969ced270173a3fa692c29d4ae30c860a2309941abd81012a", size = 53610, upload-time = "2026-02-24T03:57:55.655Z" }, + { url = "https://files.pythonhosted.org/packages/70/d3/263672ea22983ba3940f1534316dbc9200952c1c2a2332d7a664e4eaa7ae/ijson-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:01b6dad72b7b7df225ef970d334556dfad46c696a2c6767fb5d9ed8889728bca", size = 56301, upload-time = "2026-02-24T03:57:56.584Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d9/86f7fac35e0835faa188085ae0579e813493d5261ce056484015ad533445/ijson-3.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:2ea4b676ec98e374c1df400a47929859e4fa1239274339024df4716e802aa7e4", size = 93069, upload-time = "2026-02-24T03:57:57.849Z" }, + { url = "https://files.pythonhosted.org/packages/33/d2/e7366ed9c6e60228d35baf4404bac01a126e7775ea8ce57f560125ed190a/ijson-3.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:014586eec043e23c80be9a923c56c3a0920a0f1f7d17478ce7bc20ba443968ef", size = 62767, upload-time = "2026-02-24T03:57:58.758Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/3e703e8cc4b3ada79f13b28070b51d9550c578f76d1968657905857b2ddd/ijson-3.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5b8b886b0248652d437f66e7c5ac318bbdcb2c7137a7e5327a68ca00b286f5f", size = 62467, upload-time = "2026-02-24T03:58:00.261Z" }, + { url = "https://files.pythonhosted.org/packages/21/42/0c91af32c1ee8a957fdac2e051b5780756d05fd34e4b60d94a08d51bac1d/ijson-3.5.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:498fd46ae2349297e43acf97cdc421e711dbd7198418677259393d2acdc62d78", size = 200447, upload-time = "2026-02-24T03:58:01.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/80/796ea0e391b7e2d45c5b1b451734bba03f81c2984cf955ea5eaa6c4920ad/ijson-3.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22a51b4f9b81f12793731cf226266d1de2112c3c04ba4a04117ad4e466897e05", size = 217820, upload-time = "2026-02-24T03:58:02.598Z" }, + { url = "https://files.pythonhosted.org/packages/38/14/52b6613fdda4078c62eb5b4fe3efc724ddc55a4ad524c93de51830107aa3/ijson-3.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9636c710dc4ac4a281baa266a64f323b4cc165cec26836af702c44328b59a515", size = 208310, upload-time = "2026-02-24T03:58:04.759Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ad/8b3105a78774fd4a65e534a21d975ef3a77e189489fe3029ebcaeba5e243/ijson-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f7168a39e8211107666d71b25693fd1b2bac0b33735ef744114c403c6cac21e1", size = 211843, upload-time = "2026-02-24T03:58:05.836Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/a2739f6072d6e1160581bc3ed32da614c8cced023dcd519d9c5fa66e0425/ijson-3.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8696454245415bc617ab03b0dc3ae4c86987df5dc6a90bad378fe72c5409d89e", size = 200906, upload-time = "2026-02-24T03:58:07.788Z" }, + { url = "https://files.pythonhosted.org/packages/6d/5e/e06c2de3c3d4a9cfb655c1ad08a68fb72838d271072cdd3196576ac4431a/ijson-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c21bfb61f71f191565885bf1bc29e0a186292d866b4880637b833848360bdc1b", size = 205495, upload-time = "2026-02-24T03:58:09.163Z" }, + { url = "https://files.pythonhosted.org/packages/7c/11/778201eb2e202ddd76b36b0fb29bf3d8e3c167389d8aa883c62524e49f47/ijson-3.5.0-cp314-cp314t-win32.whl", hash = "sha256:a2619460d6795b70d0155e5bf016200ac8a63ab5397aa33588bb02b6c21759e6", size = 56280, upload-time = "2026-02-24T03:58:10.116Z" }, + { url = "https://files.pythonhosted.org/packages/23/28/96711503245339084c8086b892c47415895eba49782d6cc52d9f4ee50301/ijson-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4f24b78d4ef028d17eb57ad1b16c0aed4a17bdd9badbf232dc5d9305b7e13854", size = 58965, upload-time = "2026-02-24T03:58:11.278Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, +] + +[[package]] +name = "openfeature-sdk" +version = "0.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/19/f1319f665e4a5163cbda29aa0a56ca4c0899dbcf3ad6d929670157498ee0/openfeature_sdk-0.8.3.tar.gz", hash = "sha256:37c379e3e5da39567f07b9410fca9153882cf0a95da81e03ccf7559fca054fd2", size = 33299, upload-time = "2025-09-21T09:08:55.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/f5/707a5b144115de1a49bf5761a63af2545fef0a1824f72db39ddea0a3438f/openfeature_sdk-0.8.3-py3-none-any.whl", hash = "sha256:28e817514c5398e2243d0a158f3306624383757ba833032336ceba2b3cbcddd6", size = 35561, upload-time = "2025-09-21T09:08:54.072Z" }, +] + +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + +[[package]] +name = "provider-examples" +version = "1.0.0" +source = { virtual = "." } +dependencies = [ + { name = "openfeature-sdk" }, + { name = "superposition-provider" }, + { name = "superposition-sdk" }, +] + +[package.metadata] +requires-dist = [ + { name = "openfeature-sdk", specifier = "==0.8.3" }, + { name = "superposition-provider", editable = "../" }, + { name = "superposition-sdk", editable = "../../sdk" }, +] + +[[package]] +name = "smithy-core" +version = "0.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/b1/9431707674599aca4d5e90d7d608cf1a884397ad466ec83ea137fb9058f2/smithy_core-0.0.1.tar.gz", hash = "sha256:7607e53781cc74548d40781471d4bceff1d259340b21a6d313fe6913d622569a", size = 41173, upload-time = "2025-04-07T19:43:38.656Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/cd/e6233de52a04f36e4b18093c97b9a2d6b8832a3eca3ed92191406590d6aa/smithy_core-0.0.1-py3-none-any.whl", hash = "sha256:804ddea754213a00337f2a86b349912f9bc9e341039b9ea8dfdbc1b21b4f0b0c", size = 53743, upload-time = "2025-04-07T19:43:36.927Z" }, +] + +[[package]] +name = "smithy-http" +version = "0.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smithy-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/29/670680189584f6f282bf97c3d016660a04037119600fa1e1eb438e2a76b7/smithy_http-0.0.1.tar.gz", hash = "sha256:214d0f45a75078654c80ec13d518dcb690dcbec8b11a9a65b4cc2fe108c9bc33", size = 25050, upload-time = "2025-04-07T19:43:59.388Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/7b/64efb9237630e6ec64f6c6ee6eef1dfbfd1aa5013788ea72edee378cdf39/smithy_http-0.0.1-py3-none-any.whl", hash = "sha256:b25ff39604de6998adc842455138c58411d2adac9b1130d58f115eea2f109f77", size = 35604, upload-time = "2025-04-07T19:43:58.024Z" }, +] + +[package.optional-dependencies] +aiohttp = [ + { name = "aiohttp" }, + { name = "yarl" }, +] + +[[package]] +name = "smithy-json" +version = "0.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ijson" }, + { name = "smithy-core" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/51/a0d795aac06233c93fbf97cfd54c6962e868f5611e5fb20e8a45e4bcc56f/smithy_json-0.0.1.tar.gz", hash = "sha256:97c559e559654892dbcf561a3e5fb73ebffc45ed6329cba08792f2a12e6487ff", size = 6095, upload-time = "2025-04-07T19:44:16.41Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/b4/c5d207759ad967684a97d6ff2f24835d926a5fe4f96db8ea244e141a71b4/smithy_json-0.0.1-py3-none-any.whl", hash = "sha256:50d3b441c369bc16507f271699ad4f73e3961fec762c0d827a0e17709424948c", size = 8903, upload-time = "2025-04-07T19:44:15.138Z" }, +] + +[[package]] +name = "superposition-provider" +source = { editable = "../" } +dependencies = [ + { name = "openfeature-sdk" }, + { name = "smithy-core" }, + { name = "smithy-http", extra = ["aiohttp"] }, + { name = "smithy-json" }, + { name = "watchdog" }, +] + +[package.metadata] +requires-dist = [ + { name = "openfeature-sdk", specifier = "==0.8.3" }, + { name = "smithy-core", specifier = "==0.0.1" }, + { name = "smithy-http", extras = ["aiohttp"], specifier = "==0.0.1" }, + { name = "smithy-json", specifier = "==0.0.1" }, + { name = "watchdog", specifier = "==6.0.0" }, +] + +[[package]] +name = "superposition-sdk" +source = { editable = "../../sdk" } +dependencies = [ + { name = "smithy-core" }, + { name = "smithy-http", extra = ["aiohttp"] }, + { name = "smithy-json" }, +] + +[package.metadata] +requires-dist = [ + { name = "pydata-sphinx-theme", marker = "extra == 'docs'", specifier = ">=0.16.1" }, + { name = "pytest", marker = "extra == 'tests'", specifier = ">=7.2.0,<8.0.0" }, + { name = "pytest-asyncio", marker = "extra == 'tests'", specifier = ">=0.20.3,<0.21.0" }, + { name = "smithy-core", specifier = "==0.0.1" }, + { name = "smithy-http", extras = ["aiohttp"], specifier = "==0.0.1" }, + { name = "smithy-json", specifier = "==0.0.1" }, + { name = "sphinx", marker = "extra == 'docs'", specifier = ">=8.2.3" }, +] +provides-extras = ["docs", "tests"] + +[[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 = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +] diff --git a/clients/python/provider/pyproject.toml b/clients/python/provider/pyproject.toml index 71d03e0cf..7ade84768 100644 --- a/clients/python/provider/pyproject.toml +++ b/clients/python/provider/pyproject.toml @@ -11,6 +11,8 @@ dependencies = [ "smithy_core==0.0.1", "smithy_http[aiohttp]==0.0.1", "smithy_json==0.0.1", + "openfeature-sdk==0.8.3", + "watchdog==6.0.0", ] [build-system] diff --git a/clients/python/provider/superposition_provider/__init__.py b/clients/python/provider/superposition_provider/__init__.py index e69de29bb..3ee99e8c4 100644 --- a/clients/python/provider/superposition_provider/__init__.py +++ b/clients/python/provider/superposition_provider/__init__.py @@ -0,0 +1,69 @@ +""" +Superposition OpenFeature Provider. + +Provides OpenFeature-compliant feature flag providers with support for: +- Local resolution with caching (LocalResolutionProvider) +- Remote evaluation without caching (SuperpositionAPIProvider) +- Configurable refresh strategies (Polling, OnDemand, Watch, Manual) +- File-based and HTTP-based data sources +- Full FFI integration for performance +""" + +from .data_source import ( + SuperpositionDataSource, + FetchResponse, + ConfigData, + ExperimentData, +) +from .file_data_source import FileDataSource +from .http_data_source import HttpDataSource + +from .interfaces import AllFeatureProvider, FeatureExperimentMeta + +from .local_provider import LocalResolutionProvider, RefreshStrategy +from .remote_provider import SuperpositionAPIProvider + +from .types import ( + SuperpositionOptions, + EvaluationCacheOptions, + PollingStrategy, + OnDemandStrategy, + WatchStrategy, + ManualStrategy, + RefreshStrategy as RefreshStrategyType, + ConfigurationOptions, + ExperimentationOptions, + LocalProviderOptions, + RemoteProviderOptions, + SuperpositionProviderOptions, +) + +__all__ = [ + # Data sources + "SuperpositionDataSource", + "FetchResponse", + "ConfigData", + "ExperimentData", + "FileDataSource", + "HttpDataSource", + # Traits + "AllFeatureProvider", + "FeatureExperimentMeta", + # Providers + "LocalResolutionProvider", + "SuperpositionAPIProvider", + "RefreshStrategy", + # Types + "SuperpositionOptions", + "EvaluationCacheOptions", + "PollingStrategy", + "OnDemandStrategy", + "WatchStrategy", + "ManualStrategy", + "RefreshStrategyType", + "ConfigurationOptions", + "ExperimentationOptions", + "LocalProviderOptions", + "RemoteProviderOptions", + "SuperpositionProviderOptions", +] diff --git a/clients/python/provider/superposition_provider/cac_config.py b/clients/python/provider/superposition_provider/cac_config.py index 37a74f287..e3bf81eee 100644 --- a/clients/python/provider/superposition_provider/cac_config.py +++ b/clients/python/provider/superposition_provider/cac_config.py @@ -4,18 +4,14 @@ from decimal import Decimal from typing import Any, Dict, Optional, TypeVar +from .conversions import to_dimension_type, document_to_python_value from .types import OnDemandStrategy, PollingStrategy, SuperpositionOptions, ConfigurationOptions from superposition_sdk.client import Superposition, GetConfigInput from superposition_sdk.config import Config from superposition_sdk.auth_helpers import bearer_auth_config -from superposition_sdk.models import ( - DimensionType as SDKDimensionType, - DimensionTypeLOCAL_COHORT, - DimensionTypeREMOTE_COHORT, -) import asyncio from datetime import datetime, timedelta -from superposition_bindings.superposition_types import Context, DimensionInfo, DimensionType +from superposition_bindings.superposition_types import Context, DimensionInfo T = TypeVar("T") logger = logging.getLogger(__name__) @@ -35,54 +31,6 @@ def safe_json_dumps(obj: Any) -> str: """Safely serialize object to JSON, handling Decimal types""" return json.dumps(obj, cls=DecimalEncoder) -def document_to_python_value(doc: Document) -> Any: - """Recursively unwrap smithy_core.Document into plain Python values.""" - if doc.is_none(): - return None - - match doc.shape_type: - case ShapeType.BOOLEAN: - return doc.as_boolean() - case ShapeType.STRING: - return doc.as_string() - case ShapeType.BLOB: - return doc.as_blob() - case ShapeType.TIMESTAMP: - return doc.as_timestamp() - case ShapeType.BYTE | ShapeType.SHORT | ShapeType.INTEGER | ShapeType.LONG | ShapeType.BIG_INTEGER: - return doc.as_integer() - case ShapeType.FLOAT | ShapeType.DOUBLE: - return doc.as_float() - case ShapeType.BIG_DECIMAL: - # Convert Decimal to float for JSON compatibility - decimal_val = doc.as_decimal() - return float(decimal_val) if decimal_val is not None else None - case ShapeType.LIST: - return [document_to_python_value(e) for e in doc.as_list()] - case ShapeType.STRUCTURE | ShapeType.UNION | ShapeType.MAP: - return { - key: document_to_python_value(value) - for key, value in doc.as_map().items() - } - case _: - # Fallback to doc.as_value() if unknown shape or primitive - val = doc.as_value() - # Handle Decimal in fallback case too - if isinstance(val, Decimal): - return float(val) - return val - -def to_dimension_type(sdk_dim_type: SDKDimensionType) -> DimensionType: - match sdk_dim_type: - case DimensionTypeLOCAL_COHORT(): - return DimensionType.LOCAL_COHORT(sdk_dim_type.value) - - case DimensionTypeREMOTE_COHORT(): - return DimensionType.REMOTE_COHORT(sdk_dim_type.value) - - case _: - return DimensionType.REGULAR() - def convert_fallback_config(fallback_config: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: """Convert fallback config to the expected format.""" if not fallback_config: diff --git a/clients/python/provider/superposition_provider/conversions.py b/clients/python/provider/superposition_provider/conversions.py new file mode 100644 index 000000000..f3a3394bc --- /dev/null +++ b/clients/python/provider/superposition_provider/conversions.py @@ -0,0 +1,198 @@ +""" +Conversion utilities for FFI and data type transformations. + +Handles conversions between: +- Smithy Document types (from SDK) to Python types +- Evaluation contexts to query data dictionaries +- Configuration formats +""" + +import json +import logging +from decimal import Decimal +from typing import Any + +from smithy_core.documents import Document +from smithy_core.shapes import ShapeType +from superposition_bindings.superposition_client import FfiExperiment, FfiExperimentGroup +from superposition_bindings.superposition_types import GroupType, ExperimentStatusType, Variant, VariantType, Config, \ + Context, DimensionInfo, DimensionType +from superposition_sdk.models import ExperimentStatusType as SDKExperimentStatusType, GroupType as SDKGroupType, \ + ExperimentResponse, ExperimentGroupResponse, GetConfigOutput +from superposition_sdk.models import ( + DimensionType as SDKDimensionType, + DimensionTypeLOCAL_COHORT, + DimensionTypeREMOTE_COHORT, +) + +logger = logging.getLogger(__name__) + +def config_response_to_ffi_config(response: GetConfigOutput) -> Config: + default_configs: dict[str, str] = {} + for key, value in response.default_configs.items(): + default_configs[key] = json.dumps(document_to_python_value(value)) + + overrides = {} + for (key, value) in response.overrides.items(): + override = {} + for (key1, value1) in value.items(): + override[key1] = json.dumps(document_to_python_value(value1)) + overrides[key] = override + + context = [] + for ele in response.contexts: + condition = {} + for key, value in ele.condition.items(): + condition[key] = json.dumps(document_to_python_value(value)) + cv = Context( + id=ele.id, + priority=ele.priority, + weight=ele.weight, + override_with_keys=ele.override_with_keys, + condition=condition + ) + context.append(cv) + + dimensions = {} + for (key, dim_info) in response.dimensions.items(): + dimension = DimensionInfo( + schema={k: json.dumps(document_to_python_value(v)) for k, v in dim_info.schema.items()}, + position=dim_info.position, + dimension_type=to_dimension_type(dim_info.dimension_type), + dependency_graph=dim_info.dependency_graph, + value_compute_function_name=dim_info.value_compute_function_name, + ) + dimensions[key] = dimension + + + return Config( + contexts=context, + overrides=overrides, + default_configs=default_configs, + dimensions=dimensions, + ) + +def to_dimension_type(sdk_dim_type: SDKDimensionType) -> DimensionType: + match sdk_dim_type: + case DimensionTypeLOCAL_COHORT(): + return DimensionType.LOCAL_COHORT(sdk_dim_type.value) + + case DimensionTypeREMOTE_COHORT(): + return DimensionType.REMOTE_COHORT(sdk_dim_type.value) + + case _: + return DimensionType.REGULAR() + +def experiments_to_ffi_experiments(exp_list: list[ExperimentResponse]) -> list[FfiExperiment]: + trimmed_exp_list = [] + for exp in exp_list: + condition = {} + for key, value in exp.context.items(): + condition[key] = json.dumps(document_to_python_value(value)) + + variants = [] + + for variant in exp.variants: + variant_type = VariantType.CONTROL if variant.variant_type == "CONTROL" else VariantType.EXPERIMENTAL + overrides = { + key: json.dumps(document_to_python_value(value)) + for key, value in variant.overrides.items() + } + variants.append( + Variant( + id=variant.id, + variant_type=variant_type, + context_id=variant.context_id, + override_id=variant.override_id, + overrides=overrides + ) + ) + + trimmed_exp = FfiExperiment( + id=exp.id, + context=condition, + variants=variants, + traffic_percentage=exp.traffic_percentage, + status=to_experiment_status_type(exp.status), + ) + + trimmed_exp_list.append(trimmed_exp) + + return trimmed_exp_list + +def exp_grps_to_ffi_exp_grps(exp_grp_list: list[ExperimentGroupResponse]) -> list[FfiExperimentGroup]: + trimmed_exp_grp_list = [] + for exp_gr in exp_grp_list: + condition = {} + for key, value in exp_gr.context.items(): + condition[key] = json.dumps(document_to_python_value(value)) + + trimmed_exp_grp = FfiExperimentGroup( + id=exp_gr.id, + context=condition, + member_experiment_ids=exp_gr.member_experiment_ids, + traffic_percentage=exp_gr.traffic_percentage, + group_type=to_group_type(exp_gr.group_type), + buckets=exp_gr.buckets + ) + + trimmed_exp_grp_list.append(trimmed_exp_grp) + + return trimmed_exp_grp_list + +def document_to_python_value(doc: Document) -> Any: + """Recursively unwrap smithy_core.Document into plain Python values.""" + if doc.is_none(): + return None + + match doc.shape_type: + case ShapeType.BOOLEAN: + return doc.as_boolean() + case ShapeType.STRING: + return doc.as_string() + case ShapeType.BLOB: + return doc.as_blob() + case ShapeType.TIMESTAMP: + return doc.as_timestamp() + case ShapeType.BYTE | ShapeType.SHORT | ShapeType.INTEGER | ShapeType.LONG | ShapeType.BIG_INTEGER: + return doc.as_integer() + case ShapeType.FLOAT | ShapeType.DOUBLE: + return doc.as_float() + case ShapeType.BIG_DECIMAL: + # Convert Decimal to float for JSON compatibility + decimal_val = doc.as_decimal() + return float(decimal_val) if decimal_val is not None else None + case ShapeType.LIST: + return [document_to_python_value(e) for e in doc.as_list()] + case ShapeType.STRUCTURE | ShapeType.UNION | ShapeType.MAP: + return { + key: document_to_python_value(value) + for key, value in doc.as_map().items() + } + case _: + # Fallback to doc.as_value() if unknown shape or primitive + val = doc.as_value() + # Handle Decimal in fallback case too + if isinstance(val, Decimal): + return float(val) + return val + +def to_group_type(sdk_group_type: str) -> GroupType: + match sdk_group_type: + case SDKGroupType.USER_CREATED: + return GroupType.USER_CREATED + case _: + return GroupType.SYSTEM_GENERATED + +def to_experiment_status_type(sdk_status_type: str) -> ExperimentStatusType: + match sdk_status_type: + case SDKExperimentStatusType.CREATED: + return ExperimentStatusType.CREATED + case SDKExperimentStatusType.CONCLUDED: + return ExperimentStatusType.CONCLUDED + case SDKExperimentStatusType.INPROGRESS: + return ExperimentStatusType.INPROGRESS + case SDKExperimentStatusType.PAUSED: + return ExperimentStatusType.PAUSED + case _: + return ExperimentStatusType.DISCARDED diff --git a/clients/python/provider/superposition_provider/data_source.py b/clients/python/provider/superposition_provider/data_source.py new file mode 100644 index 000000000..a6fa7660b --- /dev/null +++ b/clients/python/provider/superposition_provider/data_source.py @@ -0,0 +1,182 @@ +""" +Data source abstraction for fetching configuration and experiment data. + +Provides a unified interface for different transport mechanisms (HTTP, file-based) +to fetch configuration and experiment data from a Superposition backend. +""" + +import logging +from abc import ABC, abstractmethod +from dataclasses import dataclass +from datetime import datetime +from typing import Dict, List, Optional, Any, TypeVar, Generic, AsyncGenerator + +from superposition_bindings.superposition_types import Config +from superposition_bindings.superposition_client import ExperimentConfig + +logger = logging.getLogger(__name__) + +T = TypeVar('T') + +class FetchResponse(Generic[T]): + """Represents a fetch response with optional data, supporting 304 Not Modified.""" + + def __init__(self, data: Optional[T] = None): + self._data = data + + @staticmethod + def not_modified(): + """Create a 304 Not Modified response.""" + return FetchResponse(data=None) + + @staticmethod + def data(data: T): + """Create a successful response with data.""" + return FetchResponse(data=data) + + def is_not_modified(self) -> bool: + """Check if this is a 304 Not Modified response.""" + return self._data is None + + def get_data(self) -> Optional[T]: + """Get the response data, or None if not modified.""" + return self._data + + +@dataclass +class ConfigData: + """Configuration data with fetch metadata.""" + data: Config + fetched_at: datetime + + def __str__(self): + return (f"ConfigData(fetched_at: {self.fetched_at}, " + f"contexts: {len(self.data.contexts)}, " + f"overrides: {len(self.data.overrides)}, " + f"default_configs: {len(self.data.default_configs)}, " + f"dimensions: {len(self.data.dimensions)})") + + +@dataclass +class ExperimentData: + """Experiment data with fetch metadata.""" + data: ExperimentConfig + fetched_at: datetime + + def __str__(self): + return (f"ExperimentData(experiments: {len(self.data.experiments)}, " + f"experiment_groups: {len(self.data.experiment_groups)}, " + f"fetched_at: {self.fetched_at})") + + +class SuperpositionDataSource(ABC): + """ + Abstract interface for fetching configuration and experiment data. + + Implementors provide the transport mechanism (HTTP, file-based, etc.) + while consumers interact with this unified interface. + """ + + @abstractmethod + async def fetch_config( + self, + if_modified_since: Optional[datetime] = None, + ) -> FetchResponse[ConfigData]: + """Fetch the full resolved configuration. + + Args: + if_modified_since: Optional timestamp for 304 Not Modified check. + + Returns: + FetchResponse with ConfigData or NotModified status. + """ + pass + + @abstractmethod + async def fetch_filtered_config( + self, + context: Optional[Dict[str, Any]] = None, + prefix_filter: Optional[List[str]] = None, + if_modified_since: Optional[datetime] = None, + ) -> FetchResponse[ConfigData]: + """Fetch resolved configuration filtered by context and prefixes. + + Args: + context: Optional context for filtering. + prefix_filter: Optional list of key prefixes to include. + if_modified_since: Optional timestamp for 304 Not Modified check. + + Returns: + FetchResponse with ConfigData or NotModified status. + """ + pass + + @abstractmethod + async def fetch_active_experiments( + self, + if_modified_since: Optional[datetime] = None, + ) -> FetchResponse[ExperimentData]: + """Fetch all active experiments. + + Args: + if_modified_since: Optional timestamp for 304 Not Modified check. + + Returns: + FetchResponse with ExperimentData or NotModified status. + """ + pass + + @abstractmethod + async def fetch_candidate_active_experiments( + self, + context: Optional[Dict[str, Any]] = None, + prefix_filter: Optional[List[str]] = None, + if_modified_since: Optional[datetime] = None, + ) -> FetchResponse[ExperimentData]: + """Fetch active experiments with conditions matching the context. + + Args: + context: Optional context for filtering. + prefix_filter: Optional list of key prefixes to include. + if_modified_since: Optional timestamp for 304 Not Modified check. + + Returns: + FetchResponse with ExperimentData or NotModified status. + """ + pass + + @abstractmethod + async def fetch_matching_active_experiments( + self, + context: Optional[Dict[str, Any]] = None, + prefix_filter: Optional[List[str]] = None, + if_modified_since: Optional[datetime] = None, + ) -> FetchResponse[ExperimentData]: + """Fetch active experiments that match the context. + + Args: + context: Optional context for filtering. + prefix_filter: Optional list of key prefixes to include. + if_modified_since: Optional timestamp for 304 Not Modified check. + + Returns: + FetchResponse with ExperimentData or NotModified status. + """ + pass + + def supports_experiments(self) -> bool: + """Whether this data source supports experiments.""" + return False + + async def watch(self) -> Optional[AsyncGenerator[str, None]]: + """Set up file watching for changes. + + Returns: + Optional watch stream for change notifications, or None if not supported. + """ + return None + + @abstractmethod + async def close(self) -> None: + """Clean up any resources held by this data source.""" + pass diff --git a/clients/python/provider/superposition_provider/exp_config.py b/clients/python/provider/superposition_provider/exp_config.py index de30f9078..d4eaf8a7f 100644 --- a/clients/python/provider/superposition_provider/exp_config.py +++ b/clients/python/provider/superposition_provider/exp_config.py @@ -3,18 +3,16 @@ import weakref from decimal import Decimal from typing import Any, Dict, Optional, TypeVar -from unittest import case from superposition_bindings.superposition_client import FfiExperiment, FfiExperimentGroup -from superposition_sdk.models import ExperimentStatusType as SDKExperimentStatusType, GroupType as SDKGroupType -from superposition_bindings.superposition_types import GroupType, ExperimentStatusType +from superposition_sdk.models import ExperimentStatusType as SDKExperimentStatusType from .types import OnDemandStrategy, PollingStrategy, SuperpositionOptions, ExperimentationOptions from superposition_sdk.client import Superposition, ListExperimentInput, ListExperimentGroupsInput from superposition_sdk.config import Config from superposition_sdk.auth_helpers import bearer_auth_config import asyncio from datetime import datetime, timedelta -from superposition_bindings.superposition_types import Variant, VariantType +from .conversions import exp_grps_to_ffi_exp_grps, experiments_to_ffi_experiments T = TypeVar("T") logger = logging.getLogger(__name__) @@ -22,9 +20,6 @@ if not name.startswith("python"): # e.g., "my_project" logging.getLogger(name).setLevel(logging.INFO) -from smithy_core.documents import Document -from smithy_core.shapes import ShapeType - class DecimalEncoder(json.JSONEncoder): """Custom JSON encoder that handles Decimal types""" @@ -39,65 +34,6 @@ def safe_json_dumps(obj: Any) -> str: """Safely serialize object to JSON, handling Decimal types""" return json.dumps(obj, cls=DecimalEncoder) - -def document_to_python_value(doc: Document) -> Any: - """Recursively unwrap smithy_core.Document into plain Python values.""" - if doc.is_none(): - return None - - match doc.shape_type: - case ShapeType.BOOLEAN: - return doc.as_boolean() - case ShapeType.STRING: - return doc.as_string() - case ShapeType.BLOB: - return doc.as_blob() - case ShapeType.TIMESTAMP: - return doc.as_timestamp() - case ShapeType.BYTE | ShapeType.SHORT | ShapeType.INTEGER | ShapeType.LONG | ShapeType.BIG_INTEGER: - return doc.as_integer() - case ShapeType.FLOAT | ShapeType.DOUBLE: - return doc.as_float() - case ShapeType.BIG_DECIMAL: - # Convert Decimal to float for JSON compatibility - decimal_val = doc.as_decimal() - return float(decimal_val) if decimal_val is not None else None - case ShapeType.LIST: - return [document_to_python_value(e) for e in doc.as_list()] - case ShapeType.STRUCTURE | ShapeType.UNION | ShapeType.MAP: - return { - key: document_to_python_value(value) - for key, value in doc.as_map().items() - } - case _: - # Fallback to doc.as_value() if unknown shape or primitive - val = doc.as_value() - # Handle Decimal in fallback case too - if isinstance(val, Decimal): - return float(val) - return val - - -def to_group_type(sdk_group_type: str) -> GroupType: - match sdk_group_type: - case SDKGroupType.USER_CREATED: - return SDKGroupType.USER_CREATED - case _: - return GroupType.SYSTEM_GENERATED - -def to_experiment_status_type(sdk_status_type: str) -> ExperimentStatusType: - match sdk_status_type: - case SDKExperimentStatusType.CREATED: - return ExperimentStatusType.CREATED - case SDKExperimentStatusType.CONCLUDED: - return ExperimentStatusType.CONCLUDED - case SDKExperimentStatusType.INPROGRESS: - return ExperimentStatusType.INPROGRESS - case SDKExperimentStatusType.PAUSED: - return ExperimentStatusType.PAUSED - case _: - return ExperimentStatusType.DISCARDED - class ExperimentationConfig(): def __init__(self, superposition_options: SuperpositionOptions, experiment_options: ExperimentationOptions, on_config_change=None): self.superposition_options = superposition_options @@ -177,7 +113,7 @@ async def _get_experiments(superposition_options: SuperpositionOptions) -> Optio """ try: # Create SDK config with bearer token authentication - (resolver, schemes) = bearer_auth_config( + (resolver, schemes) = bearer_auth_config( token=superposition_options.token ) sdk_config = Config( @@ -200,41 +136,7 @@ async def _get_experiments(superposition_options: SuperpositionOptions) -> Optio exp_list = response.data logger.info(f"Fetched {len(exp_list)} experiments from Superposition") - trimmed_exp_list = [] - for exp in exp_list: - condition = {} - for key, value in exp.context.items(): - condition[key] = json.dumps(document_to_python_value(value)) - - variants = [] - - for variant in exp.variants: - variant_type = VariantType.CONTROL if variant.variant_type == "CONTROL" else VariantType.EXPERIMENTAL - overrides = { - key: json.dumps(document_to_python_value(value)) - for key, value in variant.overrides.items() - } - variants.append( - Variant( - id=variant.id, - variant_type=variant_type, - context_id=variant.context_id, - override_id=variant.override_id, - overrides=overrides - ) - ) - - trimmed_exp = FfiExperiment( - id=exp.id, - context=condition, - variants=variants, - traffic_percentage=exp.traffic_percentage, - status=to_experiment_status_type(exp.status), - ) - - trimmed_exp_list.append(trimmed_exp) - - return trimmed_exp_list + return experiments_to_ffi_experiments(exp_list) except Exception as e: # Log the error and return empty config as fallback @@ -254,7 +156,7 @@ async def _get_experiment_groups(superposition_options: SuperpositionOptions) -> """ try: # Create SDK config with bearer token authentication - (resolver, schemes) = bearer_auth_config( + (resolver, schemes) = bearer_auth_config( token=superposition_options.token ) sdk_config = Config( @@ -276,24 +178,7 @@ async def _get_experiment_groups(superposition_options: SuperpositionOptions) -> exp_grp_list = response.data logger.info(f"Fetched {len(exp_grp_list)} experiment groups from Superposition") - trimmed_exp_grp_list = [] - for exp_gr in exp_grp_list: - condition = {} - for key, value in exp_gr.context.items(): - condition[key] = json.dumps(document_to_python_value(value)) - - trimmed_exp_grp = FfiExperimentGroup( - id=exp_gr.id, - context=condition, - member_experiment_ids=exp_gr.member_experiment_ids, - traffic_percentage=exp_gr.traffic_percentage, - group_type=to_group_type(exp_gr.group_type), - buckets=exp_gr.buckets - ) - - trimmed_exp_grp_list.append(trimmed_exp_grp) - - return trimmed_exp_grp_list + return exp_grps_to_ffi_exp_grps(exp_grp_list) except Exception as e: # Log the error and return empty config as fallback diff --git a/clients/python/provider/superposition_provider/file_data_source.py b/clients/python/provider/superposition_provider/file_data_source.py new file mode 100644 index 000000000..10f4512a6 --- /dev/null +++ b/clients/python/provider/superposition_provider/file_data_source.py @@ -0,0 +1,262 @@ +""" +File-based data source for loading configuration from local TOML/JSON files. + +Supports watching files for changes and 304 Not Modified via file modification times. +Uses native OS file system notifications (inotify on Linux, FSEvents on macOS, etc.) +via the watchdog library when available, with polling fallback. +""" + +import logging +import os +import json +from datetime import datetime, timezone +from typing import Dict, List, Optional, Any, AsyncGenerator +import asyncio + +from superposition_bindings.superposition_client import ffi_parse_config_file_with_filters +from superposition_bindings.superposition_types import Config + +from watchdog.observers import Observer +from watchdog.events import FileSystemEventHandler + +from .data_source import ( + SuperpositionDataSource, + FetchResponse, + ConfigData, + ExperimentData, +) + +logger = logging.getLogger(__name__) + +class _FileEventHandler(FileSystemEventHandler): + """Handler for file system events from watchdog.""" + + def __init__(self, file_path: str, loop: asyncio.AbstractEventLoop, queue: asyncio.Queue): + """Initialize handler. + + Args: + file_path: Path to the file to watch. + loop: The asyncio event loop to schedule callbacks on. + queue: The asyncio queue to put change events into. + """ + super().__init__() + self.file_path = file_path + self._loop = loop + self._queue = queue + + def on_modified(self, event): + """Called when a file is modified (from watchdog's background thread).""" + if event.is_directory: + return + + # Check if the modified file matches our watched file + if os.path.abspath(event.src_path) == os.path.abspath(self.file_path): + logger.info(f"File changed (watchdog event): {self.file_path}") + # Thread-safe: schedule the queue put from the watchdog thread + self._loop.call_soon_threadsafe(self._queue.put_nowait, self.file_path) + +def _parse_config_file( + content: str, + file_format: str, + context: Optional[Dict[str, Any]] = None, + prefix_filter: Optional[List[str]] = None +) -> Config: + """Parse TOML or JSON configuration file. + + Uses FFI functions ffi_parse_config_file_with_filters + from superposition_core for proper parsing. + """ + try: + query_data = {k: json.dumps(v) for k, v in context.items()} if context else None + return ffi_parse_config_file_with_filters(content, file_format, query_data, prefix_filter) + except Exception as e: + logger.error(f"Failed to parse config file: {e}") + raise + +class FileDataSource(SuperpositionDataSource): + """File-based data source for configuration and experiment data. + + Loads configuration from local TOML or JSON files. Supports: + - File modification time-based caching (304 Not Modified) + - File watching for automatic reload + - Fallback configurations + """ + + def __init__( + self, + file_path: Optional[str] = None, + ): + """Initialize file data source. + + Args: + file_path: Path to configuration TOML/JSON file. + """ + self._watch_task = None + self.file_path = file_path + if file_path.endswith('.toml'): + self.file_format = "toml" + elif file_path.endswith('.json'): + self.file_format = "json" + else: + raise ValueError(f"Unsupported file format: {file_path}") + + async def _fetch_config_with_filters( + self, + context: Optional[Dict[str, Any]] = None, + prefix_filter: Optional[List[str]] = None, + if_modified_since: Optional[datetime] = None, + ) -> FetchResponse[ConfigData]: + """Fetch configuration from file, applying filters and 304 Not Modified logic. + Args: + context: Optional context for filtering (ignored). + prefix_filter: Optional key prefixes to include. + if_modified_since: Timestamp for 304 Not Modified check. + """ + if if_modified_since is not None: + logger.debug("FileDataSource: ignoring if_modified_since, always reading fresh from file") + + try: + now = datetime.now(timezone.utc) + # Read and parse file + with open(self.file_path, 'r') as f: + content = f.read() + + config = _parse_config_file(content, self.file_format, context, prefix_filter) + + return FetchResponse.data(ConfigData( + data=config, + fetched_at=now, + )) + except Exception as e: + logger.error(f"Failed to fetch config from {self.file_path}: {e}") + raise + + async def fetch_config( + self, + if_modified_since: Optional[datetime] = None, + ) -> FetchResponse[ConfigData]: + """Fetch configuration from file. + + Args: + if_modified_since: Timestamp for 304 Not Modified check. + + Returns: + FetchResponse with ConfigData or NotModified status. + """ + return await self._fetch_config_with_filters(if_modified_since=if_modified_since) + + async def fetch_filtered_config( + self, + context: Optional[Dict[str, Any]] = None, + prefix_filter: Optional[List[str]] = None, + if_modified_since: Optional[datetime] = None, + ) -> FetchResponse[ConfigData]: + """Fetch configuration, optionally filtered. + + Note: File-based filtering is not efficient; consider using HttpDataSource + for production configurations that need filtering. + + Args: + context: Optional context for filtering (ignored). + prefix_filter: Optional key prefixes to include. + if_modified_since: Timestamp for 304 Not Modified check. + + Returns: + FetchResponse with ConfigData or NotModified status. + """ + return await self._fetch_config_with_filters(context, prefix_filter, if_modified_since) + + async def fetch_active_experiments( + self, + if_modified_since: Optional[datetime] = None, + ) -> FetchResponse[ExperimentData]: + """Fetch experiments from file. + + Args: + if_modified_since: Timestamp for 304 Not Modified check. + + Returns: + FetchResponse with ExperimentData or NotModified status. + """ + raise NotImplementedError("Experiments not supported by FileDataSource") + + async def fetch_candidate_active_experiments( + self, + context: Optional[Dict[str, Any]] = None, + prefix_filter: Optional[List[str]] = None, + if_modified_since: Optional[datetime] = None, + ) -> FetchResponse[ExperimentData]: + """Fetch candidate active experiments.""" + raise NotImplementedError("Experiments not supported by FileDataSource") + + async def fetch_matching_active_experiments( + self, + context: Optional[Dict[str, Any]] = None, + prefix_filter: Optional[List[str]] = None, + if_modified_since: Optional[datetime] = None, + ) -> FetchResponse[ExperimentData]: + """Fetch matching active experiments.""" + raise NotImplementedError("Experiments not supported by FileDataSource") + + def supports_experiments(self) -> bool: + """File source supports experiments if path is configured.""" + return False + + async def watch(self) -> Optional[AsyncGenerator[str, None]]: + """Set up file watching for changes using native file system notifications. + + Uses watchdog library which provides: + - inotify on Linux + - FSEvents on macOS + - ReadDirectoryChangesW on Windows + + Returns: + Async generator yielding changed file paths. + """ + if not self.file_path: + return + + try: + # Create a queue to bridge sync events to async + event_queue: asyncio.Queue = asyncio.Queue() + loop = asyncio.get_running_loop() + + # Set up watchdog observer + self._watch_task = Observer() + event_handler = _FileEventHandler(self.file_path, loop, event_queue) + + # Watch the directory containing the file + watch_dir = os.path.dirname(os.path.abspath(self.file_path)) + self._watch_task.schedule(event_handler, watch_dir, recursive=False) + self._watch_task.start() + + logger.info(f"Watching file: {self.file_path} using inotify (watchdog)") + + # Yield events as they arrive + while True: + try: + # Wait for file change event (with timeout to allow cancellation) + changed_path = await asyncio.wait_for( + event_queue.get(), + timeout=5.0 + ) + yield changed_path + except asyncio.TimeoutError: + # No events, continue watching + continue + except asyncio.CancelledError: + logger.debug("File watch cancelled") + break + finally: + # Clean up observer + if self._watch_task: + self._watch_task.stop() + self._watch_task.join(timeout=1.0) + logger.debug("File watcher stopped") + + async def close(self) -> None: + """Stop watching and clean up resources.""" + if self._watch_task: + self._watch_task.stop() + self._watch_task.join(timeout=1.0) + self._watch_task = None diff --git a/clients/python/provider/superposition_provider/http_data_source.py b/clients/python/provider/superposition_provider/http_data_source.py new file mode 100644 index 000000000..8166b8257 --- /dev/null +++ b/clients/python/provider/superposition_provider/http_data_source.py @@ -0,0 +1,224 @@ +""" +HTTP data source for fetching configuration and experiment data from Superposition API. + +Communicates with the Superposition service via HTTP using the SDK client. +""" + +import logging +from datetime import datetime +from typing import Dict, List, Optional, Any + +from smithy_core.documents import Document +from superposition_bindings.superposition_client import ExperimentConfig + +from superposition_sdk.client import Superposition +from superposition_sdk.config import Config as SdkConfig +from superposition_sdk.auth_helpers import bearer_auth_config +from superposition_sdk.models import GetConfigInput, DimensionMatchStrategy, \ + GetExperimentConfigInput, UnknownApiError +from .conversions import experiments_to_ffi_experiments, exp_grps_to_ffi_exp_grps, config_response_to_ffi_config + +from .types import SuperpositionOptions +from .data_source import ( + SuperpositionDataSource, + FetchResponse, + ConfigData, + ExperimentData, +) + +logger = logging.getLogger(__name__) + + +class HttpDataSource(SuperpositionDataSource): + """HTTP-based data source for Superposition API. + + Fetches configuration and experiment data from the Superposition HTTP API + using the SDK client. Supports conditional requests via Last-Modified timestamps. + """ + + def __init__( + self, + options: SuperpositionOptions, + ): + """Initialize HTTP data source. + + Args: + options: Superposition options. + """ + self.options = options + self.client: Superposition = self._create_client() + + def _create_client(self) -> Superposition: + """Create and configure the SDK client.""" + (resolver, schemes) = bearer_auth_config( + token=self.options.token + ) + sdk_config = SdkConfig( + endpoint_uri=self.options.endpoint, + http_auth_scheme_resolver=resolver, + http_auth_schemes=schemes + ) + + # Create Superposition client + return Superposition(config=sdk_config) + + async def _fetch_config_with_filters( + self, + context: Optional[Dict[str, Any]] = None, + prefix_filter: Optional[List[str]] = None, + if_modified_since: Optional[datetime] = None + ) -> FetchResponse[ConfigData]: + try: + context = {k: Document(v) for k, v in context.items()} if context else None + response = await self.client.get_config( + input=GetConfigInput( + workspace_id=self.options.workspace_id, + org_id=self.options.org_id, + context=context, + prefix=prefix_filter, + if_modified_since=if_modified_since, + ) + ) + return FetchResponse.data(ConfigData( + fetched_at=response.last_modified, + data=config_response_to_ffi_config(response), + )) + except UnknownApiError as e: + # this is a hack for now, need to fix it properly by checking the content of UnkownApiError + return FetchResponse.not_modified() + except Exception as e: + raise e + + async def fetch_config( + self, + if_modified_since: Optional[datetime] = None, + ) -> FetchResponse: + """Fetch full resolved configuration. + + Args: + if_modified_since: Optional timestamp for 304 Not Modified check. + + Returns: + FetchResponse with ConfigData or NotModified status. + """ + return await self._fetch_config_with_filters(if_modified_since=if_modified_since) + + async def fetch_filtered_config( + self, + context: Optional[Dict[str, Any]] = None, + prefix_filter: Optional[List[str]] = None, + if_modified_since: Optional[datetime] = None, + ) -> FetchResponse: + """Fetch resolved configuration filtered by context and prefixes. + + Args: + context: Optional context for filtering. + prefix_filter: Optional list of key prefixes to include. + if_modified_since: Optional timestamp for 304 Not Modified check. + + Returns: + FetchResponse with ConfigData or NotModified status. + """ + return await self._fetch_config_with_filters(context, prefix_filter, if_modified_since) + + async def _fetch_filtered_experiment( + self, + context: Optional[Dict[str, Any]] = None, + prefix_filter: Optional[List[str]] = None, + if_modified_since: Optional[datetime] = None, + dimension_match_strategy: Optional[DimensionMatchStrategy] = None, + ) -> FetchResponse[ExperimentData]: + """Fetch resolved experiment filtered by context and prefixes.""" + try: + context = {k: Document(v) for k, v in context.items()} if context else None + response = await self.client.get_experiment_config( + input=GetExperimentConfigInput( + workspace_id=self.options.workspace_id, + org_id=self.options.org_id, + context=context, + prefix=prefix_filter, + if_modified_since=if_modified_since, + dimension_match_strategy=dimension_match_strategy, + ) + ) + return FetchResponse.data(ExperimentData( + fetched_at=response.last_modified, + data=ExperimentConfig( + experiments=experiments_to_ffi_experiments(response.experiments), + experiment_groups=exp_grps_to_ffi_exp_grps(response.experiment_groups), + ) + )) + except UnknownApiError as e: + # this is a hack for now, need to fix it properly by checking the content of UnkownApiError + return FetchResponse.not_modified() + except Exception as e: + raise e + + async def fetch_active_experiments( + self, + if_modified_since: Optional[datetime] = None, + ) -> FetchResponse[ExperimentData]: + """Fetch all active experiments. + + Args: + if_modified_since: Optional timestamp for 304 Not Modified check. + + Returns: + FetchResponse with ExperimentData or NotModified status. + """ + return await self._fetch_filtered_experiment(if_modified_since=if_modified_since) + + async def fetch_candidate_active_experiments( + self, + context: Optional[Dict[str, Any]] = None, + prefix_filter: Optional[List[str]] = None, + if_modified_since: Optional[datetime] = None, + ) -> FetchResponse: + """Fetch active experiments with candidate conditions. + + Args: + context: Optional context for filtering. + prefix_filter: Optional list of key prefixes to include. + if_modified_since: Optional timestamp for 304 Not Modified check. + + Returns: + FetchResponse with ExperimentData or NotModified status. + """ + return await self._fetch_filtered_experiment( + context, + prefix_filter, + if_modified_since, + DimensionMatchStrategy.EXACT + ) + + async def fetch_matching_active_experiments( + self, + context: Optional[Dict[str, Any]] = None, + prefix_filter: Optional[List[str]] = None, + if_modified_since: Optional[datetime] = None, + ) -> FetchResponse[ExperimentData]: + """Fetch active experiments matching the context. + + Args: + context: Optional context for filtering. + prefix_filter: Optional list of key prefixes to include. + if_modified_since: Optional timestamp for 304 Not Modified check. + + Returns: + FetchResponse with ExperimentData or NotModified status. + """ + return await self._fetch_filtered_experiment( + context, + prefix_filter, + if_modified_since, + DimensionMatchStrategy.SUBSET + ) + + def supports_experiments(self) -> bool: + """HTTP data source supports experiments.""" + return True + + async def close(self) -> None: + """Close the HTTP client.""" + if self.client: + self.client = None diff --git a/clients/python/provider/superposition_provider/interfaces.py b/clients/python/provider/superposition_provider/interfaces.py new file mode 100644 index 000000000..5ab383a8d --- /dev/null +++ b/clients/python/provider/superposition_provider/interfaces.py @@ -0,0 +1,348 @@ +""" +Interface definitions for feature resolution and experiment metadata. + +These interfaces define the core capabilities for flag evaluation and variant resolution. +""" + +import logging +from abc import ABC, abstractmethod +from typing import Dict, List, Optional, Any, Union, Mapping, Sequence + +from openfeature.evaluation_context import EvaluationContext +from openfeature.exception import ErrorCode +from openfeature.flag_evaluation import FlagResolutionDetails + +logger = logging.getLogger(__name__) + + +class FeatureExperimentMeta(ABC): + """Trait for experiment variant resolution. + + Implementors provide the ability to determine which experiment variants + are applicable for a given evaluation context. + """ + + @abstractmethod + async def get_applicable_variants( + self, + context: Optional[EvaluationContext], + prefix_filter: Optional[List[str]] = None, + ) -> List[str]: + """Get the list of applicable experiment variant IDs for the given context. + + Args: + context: Evaluation context with targeting key and attributes. + prefix_filter: Optional list of variant ID prefixes to filter by. + + Returns: + List of variant IDs that apply to this context. + """ + pass + + +class AllFeatureProvider(ABC): + """Trait for bulk configuration resolution. + + Implementors provide the ability to resolve all feature flags at once, + optionally filtered by key prefixes. + """ + + def resolve_all_features( + self, + context: Optional[EvaluationContext], + ) -> Dict[str, Any]: + """Resolve all features for the given evaluation context. + + Args: + context: Evaluation context with targeting key and attributes. + + Returns: + Map of all resolved feature flags and their values. + """ + return self.resolve_all_features_with_filter(context, None) + + @abstractmethod + def resolve_all_features_with_filter( + self, + context: Optional[EvaluationContext], + prefix_filter: Optional[List[str]] = None, + ) -> Dict[str, Any]: + """Resolve all features, optionally filtered by key prefixes. + + Args: + context: Evaluation context with targeting key and attributes. + prefix_filter: Optional list of key prefixes to include. + + Returns: + Map of filtered resolved feature flags and their values. + """ + pass + + def resolve_typed( + self, + flag_key: str, + evaluation_context: Optional[EvaluationContext], + type_name: str, + extractor, + default + ) -> FlagResolutionDetails: + """Generic typed resolution using an extractor function. + + Resolves all features and extracts a specific flag, applying type conversion. + + Args: + flag_key: The flag key to resolve. + evaluation_context: Evaluation context. + type_name: Type name for error messages. + extractor: Function to extract the correct type from the value. + default: The default value to use if the flag key is not found. + + Returns: + FlagResolutionDetails with the typed value or error. + """ + try: + config = self.resolve_all_features(evaluation_context) + if flag_key not in config: + return FlagResolutionDetails(default) + + value = config[flag_key] + extracted = extractor(value) + if extracted is None: + return FlagResolutionDetails(default) + return FlagResolutionDetails(extracted) + except Exception as e: + logger.error(f"Error evaluating {type_name} flag {flag_key}: {e}") + return FlagResolutionDetails( + default, + error_code=ErrorCode.GENERAL, + error_message=f"Error evaluating flag '{flag_key}': {e}" + ) + + def resolve_bool( + self, + flag_key: str, + default_value: bool, + evaluation_context: Optional[EvaluationContext], + ) -> FlagResolutionDetails[bool]: + """Resolve a boolean flag.""" + return self.resolve_typed( + flag_key, + evaluation_context, + "boolean", + lambda v: _to_bool(v), + default_value + ) + + def resolve_string( + self, + flag_key: str, + default_value: str, + evaluation_context: Optional[EvaluationContext], + ) -> FlagResolutionDetails[str]: + """Resolve a string flag.""" + return self.resolve_typed( + flag_key, + evaluation_context, + "string", + lambda v: v if isinstance(v, str) else None, + default_value + ) + + def resolve_int( + self, + flag_key: str, + default_value: int, + evaluation_context: Optional[EvaluationContext], + ) -> FlagResolutionDetails[int]: + """Resolve an integer flag.""" + return self.resolve_typed( + flag_key, + evaluation_context, + "integer", + lambda v: v if isinstance(v, int) and not isinstance(v, bool) else None, + default_value + ) + + def resolve_float( + self, + flag_key: str, + default_value: float, + evaluation_context: Optional[EvaluationContext], + ) -> FlagResolutionDetails[float]: + """Resolve a float flag.""" + return self.resolve_typed( + flag_key, + evaluation_context, + "float", + lambda v: v if isinstance(v, (int, float)) and not isinstance(v, bool) else None, + default_value + ) + + def resolve_object( + self, + flag_key: str, + default_value: Union[Mapping, Sequence], + evaluation_context: Optional[EvaluationContext], + ) -> FlagResolutionDetails[Union[Mapping, Sequence]]: + """Resolve a struct/object flag.""" + return self.resolve_typed( + flag_key, + evaluation_context, + "struct", + lambda v: v if isinstance(v, dict) or isinstance(v, list) else None, + default_value + ) + + async def resolve_all_features_async( + self, + context: Optional[EvaluationContext], + ) -> Dict[str, Any]: + """Resolve all features for the given evaluation context. + + Args: + context: Evaluation context with targeting key and attributes. + + Returns: + Map of all resolved feature flags and their values. + """ + return await self.resolve_all_features_with_filter_async(context, None) + + @abstractmethod + async def resolve_all_features_with_filter_async( + self, + context: Optional[EvaluationContext], + prefix_filter: Optional[List[str]] = None, + ) -> Dict[str, Any]: + """Resolve all features, optionally filtered by key prefixes. + + Args: + context: Evaluation context with targeting key and attributes. + prefix_filter: Optional list of key prefixes to include. + + Returns: + Map of filtered resolved feature flags and their values. + """ + pass + + async def resolve_typed_async( + self, + flag_key: str, + evaluation_context: Optional[EvaluationContext], + type_name: str, + extractor, + default + ) -> FlagResolutionDetails: + """Generic typed resolution using an extractor function. + + Resolves all features and extracts a specific flag, applying type conversion. + + Args: + flag_key: The flag key to resolve. + evaluation_context: Evaluation context. + type_name: Type name for error messages. + extractor: Function to extract the correct type from the value. + default: The default value to return if the type cannot be extracted. + + Returns: + FlagResolutionDetails with the typed value or error. + """ + try: + config = await self.resolve_all_features_async(evaluation_context) + if flag_key not in config: + return FlagResolutionDetails(default) + + value = config[flag_key] + extracted = extractor(value) + if extracted is None: + return FlagResolutionDetails(default) + return FlagResolutionDetails(extracted) + except Exception as e: + logger.error(f"Error evaluating {type_name} flag {flag_key}: {e}") + return FlagResolutionDetails( + default, + error_code=ErrorCode.GENERAL, + error_message=f"Error evaluating flag '{flag_key}': {e}" + ) + + async def resolve_bool_async( + self, + flag_key: str, + default_value: bool, + evaluation_context: Optional[EvaluationContext], + ) -> FlagResolutionDetails[bool]: + """Resolve a boolean flag.""" + return await self.resolve_typed_async( + flag_key, + evaluation_context, + "boolean", + lambda v: _to_bool(v), + default_value + ) + + async def resolve_string_async( + self, + flag_key: str, + default_value: str, + evaluation_context: Optional[EvaluationContext], + ) -> FlagResolutionDetails[str]: + """Resolve a string flag.""" + return await self.resolve_typed_async( + flag_key, + evaluation_context, + "string", + lambda v: v if isinstance(v, str) else None, + default_value + ) + + async def resolve_int_async( + self, + flag_key: str, + default_value: int, + evaluation_context: Optional[EvaluationContext], + ) -> FlagResolutionDetails[int]: + """Resolve an integer flag.""" + return await self.resolve_typed_async( + flag_key, + evaluation_context, + "integer", + lambda v: v if isinstance(v, int) and not isinstance(v, bool) else None, + default_value + ) + + async def resolve_float_async( + self, + flag_key: str, + default_value: float, + evaluation_context: Optional[EvaluationContext], + ) -> FlagResolutionDetails[float]: + """Resolve a float flag.""" + return await self.resolve_typed_async( + flag_key, + evaluation_context, + "float", + lambda v: v if isinstance(v, (int, float)) and not isinstance(v, bool) else None, + default_value + ) + + async def resolve_object_async( + self, + flag_key: str, + default_value: Union[Mapping, Sequence], + evaluation_context: Optional[EvaluationContext], + ) -> FlagResolutionDetails[Union[Mapping, Sequence]]: + """Resolve a struct/object flag.""" + return await self.resolve_typed_async( + flag_key, + evaluation_context, + "struct", + lambda v: v if isinstance(v, dict) or isinstance(v, list) else None, + default_value + ) + +def _to_bool(value: Any) -> Optional[bool]: + if isinstance(value, bool): return value + if isinstance(value, str): + if value.lower() == "true": return True + if value.lower() == "false": return False + if isinstance(value, (int, float)): return value != 0 + return None diff --git a/clients/python/provider/superposition_provider/local_provider.py b/clients/python/provider/superposition_provider/local_provider.py new file mode 100644 index 000000000..66c5f9fd1 --- /dev/null +++ b/clients/python/provider/superposition_provider/local_provider.py @@ -0,0 +1,533 @@ +""" +LocalResolutionProvider - Local in-process evaluation with caching. + +Implements feature resolution with configurable refresh strategies and +support for primary + fallback data sources. +""" + +import logging +import asyncio +import json +from datetime import datetime, timezone +from typing import Dict, List, Optional, Any, Tuple, Union, Sequence, Mapping + +from openfeature.provider import ( + AbstractProvider, + Metadata, + ProviderStatus, +) +from openfeature.evaluation_context import EvaluationContext +from openfeature.flag_evaluation import FlagResolutionDetails + +from superposition_bindings.superposition_client import ProviderCache +from superposition_bindings.superposition_types import MergeStrategy + +from .data_source import SuperpositionDataSource, ConfigData, ExperimentData +from .interfaces import AllFeatureProvider, FeatureExperimentMeta +from .types import RefreshStrategy, OnDemandStrategy, WatchStrategy, PollingStrategy, ManualStrategy, default_on_demand_strategy + +logger = logging.getLogger(__name__) + +class LocalResolutionProvider(AbstractProvider, AllFeatureProvider, FeatureExperimentMeta): + """Local in-process OpenFeature provider with caching and refresh strategies. + + Features: + - Configurable refresh strategies (Polling, OnDemand, Watch, Manual) + - Primary + fallback data sources + - Atomic cache updates via thread-safe references + - FFI-based local evaluation for performance + """ + + def __init__( + self, + primary_source: SuperpositionDataSource, + fallback_source: Optional[SuperpositionDataSource] = None, + refresh_strategy: RefreshStrategy = default_on_demand_strategy(), + ): + """Initialize local resolution provider. + + Args: + primary_source: Primary data source for config/experiments. + fallback_source: Optional fallback data source. + refresh_strategy: How often to refresh data. + """ + self.primary_source = primary_source + self.fallback_source = fallback_source + self.refresh_strategy = refresh_strategy + + self.metadata = Metadata(name="LocalResolutionProvider") + self.status = ProviderStatus.NOT_READY + self.global_context = EvaluationContext() + + # Caches (atomic updates via simple assignments) + self.cached_config: Optional[ConfigData] = None + self.cached_experiments: Optional[ExperimentData] = None + self.ffi_cache: Optional[ProviderCache] = None + + # Background task for refresh strategy + self._background_task: Optional[asyncio.Task] = None + + async def initialize(self, context: EvaluationContext): + """Initialize the provider. + + Fetches initial config and experiments, starts refresh strategy. + + Args: + context: Global evaluation context. + """ + try: + logger.info("Initializing LocalResolutionProvider...") + self.status = ProviderStatus.NOT_READY + self.global_context = context + + # Create FFI cache + self.ffi_cache = ProviderCache() + + # Fetch initial config (required) + await self._fetch_and_cache_config(init=True) + + # Fetch initial experiments (best-effort) + await self._fetch_and_cache_experiments(init=True) + + # Start refresh strategy + await self._start_refresh_strategy() + + self.status = ProviderStatus.READY + logger.info("LocalResolutionProvider initialized successfully") + except Exception as e: + logger.error(f"Failed to initialize LocalResolutionProvider: {e}") + self.status = ProviderStatus.FATAL + raise + + async def shutdown(self): + """Shutdown the provider and stop all background tasks.""" + logger.info("Shutting down LocalResolutionProvider...") + + # Cancel background tasks + if self._background_task: + self._background_task.cancel() + try: + await self._background_task + except asyncio.CancelledError: + pass + + # Close data sources + try: + await self.primary_source.close() + except Exception as e: + logger.warning(f"Error closing primary data source: {e}") + + if self.fallback_source: + try: + await self.fallback_source.close() + except Exception as e: + logger.warning(f"Error closing fallback data source: {e}") + + # Clear caches + self.cached_config = None + self.cached_experiments = None + self.ffi_cache = None + + self.status = ProviderStatus.NOT_READY + logger.info("LocalResolutionProvider shutdown completed") + + def get_metadata(self) -> Metadata: + """Get provider metadata.""" + return self.metadata + + def get_status(self) -> ProviderStatus: + """Get provider status.""" + return self.status + + # --- AllFeatureProvider implementation --- + def resolve_all_features_with_filter( + self, + context: Optional[EvaluationContext], + prefix_filter: Optional[List[str]] = None, + ) -> Dict[str, Any]: + """Resolve all features with optional filtering. + + Args: + context: Evaluation context. + prefix_filter: Optional list of key prefixes. + + Returns: + Dictionary of filtered resolved flags. + """ + match self.refresh_strategy: + case OnDemandStrategy(): + logger.debug("ON_DEMAND strategy: data might be stale, use async resolve to ensure freshness") + case _: () + + if not self.ffi_cache or not self.cached_config: + raise RuntimeError("Provider not properly initialized") + + # Merge contexts + targeting_key, query_data = self._merge_contexts(context) + + try: + # Use FFI for local evaluation + result = self.ffi_cache.eval_config( + query_data, + MergeStrategy.MERGE, + prefix_filter, + targeting_key, + ) + + # Convert from JSON strings to Python values + return { k: json.loads(v) for k, v in result.items() } + except Exception as e: + logger.error(f"Error resolving features: {e}") + raise + + async def resolve_all_features_with_filter_async( + self, + context: Optional[EvaluationContext], + prefix_filter: Optional[List[str]] = None, + ) -> Dict[str, Any]: + """Resolve all features with optional filtering. + + Args: + context: Evaluation context. + prefix_filter: Optional list of key prefixes. + + Returns: + Dictionary of filtered resolved flags. + """ + # Ensure fresh data (for ON_DEMAND strategy) + await self._ensure_fresh_data() + + if not self.ffi_cache or not self.cached_config: + raise RuntimeError("Provider not properly initialized") + + # Merge contexts + targeting_key, query_data = self._merge_contexts(context) + + try: + # Use FFI for local evaluation + result = self.ffi_cache.eval_config( + query_data, + MergeStrategy.MERGE, + prefix_filter, + targeting_key, + ) + + # Convert from JSON strings to Python values + return { k: json.loads(v) for k, v in result.items() } + except Exception as e: + logger.error(f"Error resolving features: {e}") + raise + + # --- FeatureExperimentMeta implementation --- + + async def get_applicable_variants( + self, + context: Optional[EvaluationContext], + prefix_filter: Optional[List[str]] = None, + ) -> List[str]: + """Get applicable experiment variants. + + Args: + context: Evaluation context with targeting key. + prefix_filter: Optional list of variant ID prefixes. + + Returns: + List of applicable variant IDs. + """ + await self._ensure_fresh_data() + + if not self.cached_experiments or not self.ffi_cache: + raise RuntimeError("Provider not properly initialized") + + # Merge contexts + targeting_key, query_data = self._merge_contexts(context) + + if targeting_key is None: + return [] + + try: + # Use FFI for local evaluation + return self.ffi_cache.get_applicable_variants( + query_data, + prefix_filter, + targeting_key, + ) + + except Exception as e: + logger.error(f"Error getting applicable variants: {e}") + raise + + # --- OpenFeature FeatureProvider methods --- + + def resolve_boolean_details( + self, + flag_key: str, + default_value: bool, + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[bool]: + """Resolve a boolean flag.""" + return self.resolve_bool(flag_key, default_value, evaluation_context) + + def resolve_string_details( + self, + flag_key: str, + default_value: str, + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[str]: + """Resolve a string flag.""" + return self.resolve_string(flag_key, default_value, evaluation_context) + + def resolve_integer_details( + self, + flag_key: str, + default_value: int, + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[int]: + """Resolve an integer flag.""" + return self.resolve_int(flag_key, default_value, evaluation_context) + + def resolve_float_details( + self, + flag_key: str, + default_value: float, + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[float]: + """Resolve a float flag.""" + return self.resolve_float(flag_key, default_value, evaluation_context) + + def resolve_object_details( + self, + flag_key: str, + default_value: Union[Mapping, Sequence], + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[Union[Mapping, Sequence]]: + """Resolve an object/struct flag.""" + return self.resolve_object(flag_key, default_value, evaluation_context) + + async def resolve_boolean_details_async( + self, + flag_key: str, + default_value: bool, + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[bool]: + """Resolve a boolean flag.""" + return await self.resolve_bool_async(flag_key, default_value, evaluation_context) + + async def resolve_string_details_async( + self, + flag_key: str, + default_value: str, + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[str]: + """Resolve a string flag.""" + return await self.resolve_string_async(flag_key, default_value, evaluation_context) + + async def resolve_integer_details_async( + self, + flag_key: str, + default_value: int, + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[int]: + """Resolve an integer flag.""" + return await self.resolve_int_async(flag_key, default_value, evaluation_context) + + async def resolve_float_details_async( + self, + flag_key: str, + default_value: float, + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[float]: + """Resolve a float flag.""" + return await self.resolve_float_async(flag_key, default_value, evaluation_context) + + async def resolve_object_details_async( + self, + flag_key: str, + default_value: Union[Mapping, Sequence], + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[Union[Mapping, Sequence]]: + """Resolve an object/struct flag.""" + return await self.resolve_object_async(flag_key, default_value, evaluation_context) + + # --- Public refresh methods --- + + async def refresh(self) -> None: + """Manually refresh both config and experiments. + + Useful for MANUAL refresh strategy. + """ + await asyncio.gather(self._fetch_and_cache_config(), self._fetch_and_cache_experiments()) + + # --- Private helpers --- + + async def _handle_fetch_config_from_fallback(self): + """Fetch config from fallback source, if available.""" + try: + logger.info("Attempting to fetch config from fallback source...") + if self.fallback_source: + logger.info("Fetching config from fallback source...") + response = await self.fallback_source.fetch_config(None) + if response.get_data(): + self.cached_config = response.get_data() + self._update_config_ffi_cache() + logger.info("Config updated from fallback source") + except Exception as e: + logger.error(f"Error fetching fallback config: {e}") + raise e + + async def _fetch_and_cache_config(self, init: bool = False) -> None: + """Fetch and cache configuration from primary/fallback sources.""" + try: + logger.info(f"Fetching config (init={init})...") + # Try primary source + if_modified_since = None if init else self.cached_config.fetched_at + response = await self.primary_source.fetch_config(if_modified_since) + logger.debug(f"Primary source fetch_config response: {response}") + if response.get_data(): + self.cached_config = response.get_data() + self._update_config_ffi_cache() + logger.info("Config updated from primary source") + elif init: + # need to counter the hack present in HttpDataSource + await self._handle_fetch_config_from_fallback() + + if not self.cached_config: + raise RuntimeError("Failed to fetch config from both primary and fallback sources") + except Exception as e: + logger.error(f"Error fetching config from primary source: {e}, init={init}") + # Try fallback source if available + if init: + logger.info("Attempting to fetch config from fallback source due to primary source error during initialization") + await self._handle_fetch_config_from_fallback() + else: + logger.error(f"Error fetching fallback config: {e}") + + if not self.cached_config: + raise e + + async def _handle_fetch_experiments_from_fallback(self): + """Fetch experiments from fallback source, if available.""" + if not self.fallback_source or not self.fallback_source.supports_experiments(): + return + try: + response = await self.fallback_source.fetch_active_experiments(None) + if response.get_data(): + self.cached_experiments = response.get_data() + self._update_exp_ffi_cache() + logger.info("Experiments updated from fallback source") + except Exception as e: + logger.error(f"Error fetching fallback experiments: {e}") + raise e + + async def _fetch_and_cache_experiments(self, init: bool = False) -> None: + """Fetch and cache experiments from primary/fallback sources.""" + if not self.primary_source.supports_experiments(): + return + try: + # Try primary source + if_modified_since = ( + None if init or self.cached_experiments is None + else self.cached_experiments.fetched_at + ) + response = await self.primary_source.fetch_active_experiments(if_modified_since) + if response.get_data(): + self.cached_experiments = response.get_data() + self._update_exp_ffi_cache() + logger.info("Experiments updated from primary source") + elif init: + # need to counter the hack present in HttpDataSource + await self._handle_fetch_experiments_from_fallback() + + if (not self.fallback_source or self.fallback_source.supports_experiments()) and not self.cached_experiments: + raise RuntimeError("Failed to fetch experiments from both primary and fallback sources") + except Exception as e: + logger.error(f"Error fetching experiments from primary source: {e}") + + # Try fallback source if available and no 304 + if init: + await self._handle_fetch_experiments_from_fallback() + else: + logger.error(f"Error fetching fallback experiments: {e}") + + if init and self.fallback_source and self.fallback_source.supports_experiments() and not self.cached_experiments: + raise e + + async def _ensure_fresh_data(self) -> None: + """Check if data needs refresh (for ON_DEMAND strategy).""" + match self.refresh_strategy: + case OnDemandStrategy(): + ttl = self.refresh_strategy.ttl + use_stale_on_error = self.refresh_strategy.use_stale_on_error + + def is_elapsed(cached_at: datetime) -> bool: + return (datetime.now(timezone.utc) - cached_at).total_seconds() > ttl + + should_refresh_config = self.cached_config.fetched_at is None or is_elapsed(self.cached_config.fetched_at) + should_refresh_experiments = ( + self.cached_experiments is None + or self.cached_experiments.fetched_at is None + or is_elapsed(self.cached_experiments.fetched_at) + ) + + if should_refresh_config or should_refresh_experiments: + try: + await self.refresh() + except Exception as e: + if not use_stale_on_error: + raise e + logger.error(f"Error refreshing: {e}") + + case _: + logger.debug("Do nothing - fresh data check not required") + + async def _start_refresh_strategy(self) -> None: + """Start the configured refresh strategy.""" + match self.refresh_strategy: + case WatchStrategy(): + self._background_task = asyncio.create_task(self._watch_loop()) + case PollingStrategy(): + self._background_task = asyncio.create_task(self._polling_loop()) + case ManualStrategy(): + logger.debug("MANUAL strategy: caller must invoke refresh()") + case OnDemandStrategy(): + logger.debug("ON_DEMAND strategy: refresh on first stale access") + + async def _polling_loop(self) -> None: + """Polling refresh loop.""" + logger.info(f"Starting polling with interval {self.refresh_strategy.interval}s") + try: + while True: + await asyncio.sleep(self.refresh_strategy.interval) + await self.refresh() + except asyncio.CancelledError: + logger.info("Polling loop cancelled") + + async def _watch_loop(self) -> None: + """File watching refresh loop.""" + logger.info("Starting watch-based refresh") + try: + # use self.refresh_strategy.debounce for debounce interval + debounce_interval = self.refresh_strategy.debounce_ms / 1000 + # watch() is an async generator, iterate directly with async for + async for _ in self.primary_source.watch(): + logger.debug("File change detected, refreshing...") + await asyncio.sleep(debounce_interval) + await self.refresh() + except asyncio.CancelledError: + logger.info("Watch loop cancelled") + + def _merge_contexts(self, context: Optional[EvaluationContext]) -> Tuple[Optional[str], dict[str, str]]: + """Merge global and evaluation contexts.""" + eval_ctx = self.global_context.merge(context) if context else self.global_context + query_data = { k: json.dumps(v) for k, v in eval_ctx.attributes.items() } + return eval_ctx.targeting_key, query_data + + def _update_config_ffi_cache(self) -> None: + """Update ffi config cache with new values.""" + config = self.cached_config.data + self.ffi_cache.init_config(config.default_configs, config.contexts, config.overrides, config.dimensions) + + def _update_exp_ffi_cache(self) -> None: + """Update ffi exp config cache with new values.""" + exp = self.cached_experiments.data + self.ffi_cache.init_experiments(exp.experiments, exp.experiment_groups) diff --git a/clients/python/provider/superposition_provider/remote_provider.py b/clients/python/provider/superposition_provider/remote_provider.py new file mode 100644 index 000000000..179a506ce --- /dev/null +++ b/clients/python/provider/superposition_provider/remote_provider.py @@ -0,0 +1,313 @@ +""" +SuperpositionAPIProvider - Direct remote evaluation without caching. + +Uses the Superposition SDK directly for every flag evaluation, +with no local state or caching. +""" + +import logging +from typing import Dict, List, Optional, Any, Tuple, Union, Sequence, Mapping + +from openfeature.provider import ( + AbstractProvider, + Metadata, + ProviderStatus, +) +from openfeature.evaluation_context import EvaluationContext +from openfeature.flag_evaluation import FlagResolutionDetails + +from smithy_core.documents import Document + +from superposition_sdk.client import Superposition +from superposition_sdk.config import Config as SdkConfig +from superposition_sdk.auth_helpers import bearer_auth_config +from superposition_sdk.models import ApplicableVariantsInput, GetResolvedConfigWithIdentifierInput + +from .conversions import document_to_python_value +from .interfaces import AllFeatureProvider, FeatureExperimentMeta +from .types import SuperpositionOptions + +logger = logging.getLogger(__name__) + + +class SuperpositionAPIProvider(AbstractProvider, AllFeatureProvider, FeatureExperimentMeta): + """Direct remote OpenFeature provider with no local caching. + + Every flag evaluation goes directly to the Superposition API. + This provider is suitable for serverless and stateless deployments. + """ + + def __init__(self, options: SuperpositionOptions): + """Initialize the remote provider. + + Args: + options: Superposition configuration options. + """ + self.options = options + self.metadata = Metadata(name="SuperpositionAPIProvider") + self.status = ProviderStatus.NOT_READY + self.global_context = EvaluationContext() + self.client = self._create_client() + + def _create_client(self) -> Superposition: + """Create and configure the SDK client.""" + (resolver, schemes) = bearer_auth_config( + token=self.options.token + ) + sdk_config = SdkConfig( + endpoint_uri=self.options.endpoint, + http_auth_scheme_resolver=resolver, + http_auth_schemes=schemes + ) + + # Create Superposition client + return Superposition(config=sdk_config) + + def initialize(self, evaluation_context: EvaluationContext): + """Initialize the provider. + + Args: + evaluation_context: Global evaluation context to apply to all resolutions. + """ + try: + logger.info("Initializing SuperpositionAPIProvider...") + self.global_context = evaluation_context + self.status = ProviderStatus.READY + logger.info("SuperpositionAPIProvider initialized successfully") + except Exception as e: + logger.error(f"Failed to initialize SuperpositionAPIProvider: {e}") + self.status = ProviderStatus.FATAL + raise + + def shutdown(self): + """Shutdown the provider.""" + logger.info("Shutting down SuperpositionAPIProvider...") + if self.client: + self.client = None + self.status = ProviderStatus.NOT_READY + logger.info("SuperpositionAPIProvider shutdown completed") + + def get_metadata(self) -> Metadata: + """Get provider metadata.""" + return self.metadata + + def get_status(self) -> ProviderStatus: + """Get provider status.""" + return self.status + + def resolve_all_features_with_filter( + self, + context: Optional[EvaluationContext] = None, + prefix_filter: Optional[List[str]] = None, + ) -> Dict[str, Any]: + """Resolve all features with optional prefix filtering. + + Args: + context: Evaluation context (optional, uses global context if not provided). + prefix_filter: Optional list of key prefixes to include. + + Returns: + Dictionary of filtered resolved flags. + """ + raise NotImplementedError("Synchronous resolution is not implemented in SuperpositionAPIProvider. Use async functions instead.") + + # --- AllFeatureProvider implementation --- + async def resolve_all_features_with_filter_async( + self, + context: Optional[EvaluationContext] = None, + prefix_filter: Optional[List[str]] = None, + ) -> Dict[str, Any]: + """Resolve all features with optional prefix filtering. + + Args: + context: Evaluation context (optional, uses global context if not provided). + prefix_filter: Optional list of key prefixes to include. + + Returns: + Dictionary of filtered resolved flags. + """ + return await self._resolve_remote(context, prefix_filter) + + # --- FeatureExperimentMeta implementation --- + + async def get_applicable_variants( + self, + context: Optional[EvaluationContext] = None, + prefix_filter: Optional[List[str]] = None, + ) -> List[str]: + """Get applicable experiment variants via remote API. + + Args: + context: Evaluation context with targeting key (optional). + prefix_filter: Optional list of variant ID prefixes. + + Returns: + List of applicable variant IDs. + """ + targeting_key, merged_context = self._get_merged_context(context) + + if not targeting_key: + logger.warning("Missing targeting key for variant resolution") + return [] + + try: + response = await self.client.applicable_variants( + input=ApplicableVariantsInput( + workspace_id=self.options.workspace_id, + org_id=self.options.org_id, + identifier=targeting_key, + context=merged_context, + prefix=prefix_filter, + ) + ) + + # Extract variant IDs from response + return [v.id for v in response.data] if response.data else [] + except Exception as e: + logger.error(f"Failed to get applicable variants: {e}") + return [] + + # --- OpenFeature FeatureProvider methods --- + + def resolve_boolean_details( + self, + flag_key: str, + default_value: bool, + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[bool]: + """Resolve a boolean flag.""" + return self.resolve_bool(flag_key, default_value, evaluation_context) + + def resolve_string_details( + self, + flag_key: str, + default_value: str, + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[str]: + """Resolve a string flag.""" + return self.resolve_string(flag_key, default_value, evaluation_context) + + def resolve_integer_details( + self, + flag_key: str, + default_value: int, + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[int]: + """Resolve an integer flag.""" + return self.resolve_int(flag_key, default_value, evaluation_context) + + def resolve_float_details( + self, + flag_key: str, + default_value: float, + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[float]: + """Resolve a float flag.""" + return self.resolve_float(flag_key, default_value, evaluation_context) + + def resolve_object_details( + self, + flag_key: str, + default_value: Union[Mapping, Sequence], + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[Union[Mapping, Sequence]]: + """Resolve an object/struct flag.""" + return self.resolve_object(flag_key, default_value, evaluation_context) + + async def resolve_boolean_details_async( + self, + flag_key: str, + default_value: bool, + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[bool]: + """Resolve a boolean flag.""" + return await self.resolve_bool_async(flag_key, default_value, evaluation_context) + + async def resolve_string_details_async( + self, + flag_key: str, + default_value: str, + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[str]: + """Resolve a string flag.""" + return await self.resolve_string_async(flag_key, default_value, evaluation_context) + + async def resolve_integer_details_async( + self, + flag_key: str, + default_value: int, + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[int]: + """Resolve an integer flag.""" + return await self.resolve_int_async(flag_key, default_value, evaluation_context) + + async def resolve_float_details_async( + self, + flag_key: str, + default_value: float, + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[float]: + """Resolve a float flag.""" + return await self.resolve_float_async(flag_key, default_value, evaluation_context) + + async def resolve_object_details_async( + self, + flag_key: str, + default_value: Union[Mapping, Sequence], + evaluation_context: Optional[EvaluationContext] = None, + ) -> FlagResolutionDetails[Union[Mapping, Sequence]]: + """Resolve an object/struct flag.""" + return await self.resolve_object_async(flag_key, default_value, evaluation_context) + + # --- Private helpers --- + + async def _resolve_remote( + self, + context: Optional[EvaluationContext] = None, + prefix_filter: Optional[List[str]] = None, + ) -> Dict[str, Any]: + """Resolve configuration via remote API. + + Args: + context: Merged context dictionary with string values. + prefix_filter: Optional list of key prefixes. + + Returns: + Dictionary of resolved flags. + """ + try: + targeting_key, merged_context = self._get_merged_context(context) + response = await self.client.get_resolved_config_with_identifier( + input=GetResolvedConfigWithIdentifierInput( + workspace_id=self.options.workspace_id, + org_id=self.options.org_id, + context=merged_context, + prefix=prefix_filter, + identifier=targeting_key, + ) + ) + + # Convert response to plain dict + config_value = document_to_python_value(response.config) + + if isinstance(config_value, dict): + return config_value + else: + # Wrap non-dict responses + return {"_value": config_value} + except Exception as e: + logger.error(f"Failed to resolve config: {e}") + raise + + def _get_merged_context( + self, + context: Optional[EvaluationContext], + ) -> Tuple[Optional[str], dict[str, Document]]: + """Merge global and evaluation contexts. + + Returns: + Tuple of (merged_query_data, targeting_key) + """ + eval_ctx = self.global_context.merge(context) if context else self.global_context + query_data = { k: Document(v) for k, v in eval_ctx.attributes.items() } + return eval_ctx.targeting_key, query_data diff --git a/clients/python/provider/superposition_provider/types.py b/clients/python/provider/superposition_provider/types.py index 925ee0e6d..6f89399dc 100644 --- a/clients/python/provider/superposition_provider/types.py +++ b/clients/python/provider/superposition_provider/types.py @@ -1,57 +1,128 @@ -from dataclasses import dataclass -from typing import Optional, Dict, Any +""" +Type definitions for Superposition provider configuration. +""" +from dataclasses import dataclass, field +from typing import Optional, Dict, Any, Union +from .data_source import SuperpositionDataSource + + +# ============================================================================ +# Basic Configuration Types +# ============================================================================ @dataclass -class SuperpositionConfig: +class SuperpositionOptions: + """Core Superposition API configuration.""" endpoint: str token: str org_id: str workspace_id: str +# ============================================================================ +# Cache Configuration +# ============================================================================ @dataclass class EvaluationCacheOptions: + """Options for evaluation result caching.""" ttl: Optional[int] = None size: Optional[int] = None + +# ============================================================================ +# Refresh Strategy Types +# ============================================================================ + @dataclass class PollingStrategy: - interval: int - timeout: int = None + """Polling-based refresh strategy. + + Fetches configuration at regular intervals. + """ + interval: int # seconds + timeout: Optional[int] = None + +def default_polling_strategy(): + return PollingStrategy(60, 30) @dataclass class OnDemandStrategy: - ttl: int + """On-demand refresh strategy. + + Refreshes only when data becomes stale. + """ + ttl: int # time-to-live in seconds use_stale_on_error: bool = False - timeout: int = None + timeout: Optional[int] = None + +def default_on_demand_strategy(): + return OnDemandStrategy(300, True, 30) + +@dataclass +class WatchStrategy: + """File watch-based refresh strategy. + + Refreshes when local files change. + """ + debounce_ms: int = 500 + +def default_watch_strategy(): + return WatchStrategy(500) + +@dataclass +class ManualStrategy: + """Manual refresh strategy. + + Caller explicitly triggers refresh via refresh() method. + """ + pass + -RefreshStrategy = PollingStrategy | OnDemandStrategy +# Union type for all refresh strategies +RefreshStrategy = Union[PollingStrategy, OnDemandStrategy, WatchStrategy, ManualStrategy] + + +# ============================================================================ +# Provider-Specific Options +# ============================================================================ @dataclass class ExperimentationOptions: + """Configuration for experimentation client.""" refresh_strategy: RefreshStrategy evaluation_cache_options: Optional[EvaluationCacheOptions] = None default_toss: int = -1 - -@dataclass -class SuperpositionOptions: - endpoint: str - token: str - org_id: str - workspace_id: str - @dataclass class ConfigurationOptions: + """Configuration for config/CAC client.""" refresh_strategy: RefreshStrategy fallback_config: Optional[Dict[str, Any]] = None evaluation_cache_options: Optional[EvaluationCacheOptions] = None +# ============================================================================ +# Provider Initialization Options +# ============================================================================ + +@dataclass +class LocalProviderOptions: + """Options for LocalResolutionProvider.""" + primary_source: SuperpositionDataSource + fallback_source: Optional[SuperpositionDataSource] = None + refresh_strategy: RefreshStrategy = field(default_factory=default_polling_strategy) + + +@dataclass +class RemoteProviderOptions: + """Options for SuperpositionAPIProvider.""" + superposition_options: SuperpositionOptions + @dataclass class SuperpositionProviderOptions: + """Universal provider options (backward compatibility).""" refresh_strategy: RefreshStrategy endpoint: str token: str @@ -61,5 +132,3 @@ class SuperpositionProviderOptions: fallback_config: Optional[Dict[str, Any]] = None evaluation_cache_options: Optional[EvaluationCacheOptions] = None experimentation_options: Optional[ExperimentationOptions] = None - - diff --git a/crates/frontend/src/components/change_summary.rs b/crates/frontend/src/components/change_summary.rs index 16f414051..06c9bf0b4 100644 --- a/crates/frontend/src/components/change_summary.rs +++ b/crates/frontend/src/components/change_summary.rs @@ -59,7 +59,9 @@ impl DiffSide { fn highlight(&self) -> (ChangeTag, &'static str) { match self { DiffSide::Old => (ChangeTag::Delete, "bg-red-200 text-red-900 rounded-sm"), - DiffSide::New => (ChangeTag::Insert, "bg-green-200 text-green-900 rounded-sm"), + DiffSide::New => { + (ChangeTag::Insert, "bg-green-200 text-green-900 rounded-sm") + } } } } @@ -87,9 +89,7 @@ fn render_inline_diff(old_text: &str, new_text: &str, side: DiffSide) -> View { view! { {spans} }.into_view() } -fn make_diff_formatter( - side: DiffSide, -) -> impl Fn(&str, &Map) -> View { +fn make_diff_formatter(side: DiffSide) -> impl Fn(&str, &Map) -> View { move |value: &str, row: &Map| { if let Some(v) = render_sentinel(value) { return v; @@ -103,7 +103,7 @@ fn make_diff_formatter( render_inline_diff(old, new, side) } None => view! { {value.to_string()} } - .into_view(), + .into_view(), } } } diff --git a/crates/frontend/src/components/monaco_editor.rs b/crates/frontend/src/components/monaco_editor.rs index d1857a8e6..ec5b02022 100644 --- a/crates/frontend/src/components/monaco_editor.rs +++ b/crates/frontend/src/components/monaco_editor.rs @@ -167,13 +167,10 @@ pub fn MonacoDiffEditor( let editor_state = StoredValue::new(None::<(js_sys::Object, ITextModel, ITextModel)>); on_cleanup(move || { - if let Some((editor, original_model, modified_model)) = editor_state.get_value() - { + if let Some((editor, original_model, modified_model)) = editor_state.get_value() { original_model.dispose(); modified_model.dispose(); - if let Ok(editor) = editor - .dyn_into::() - { + if let Ok(editor) = editor.dyn_into::() { editor.dispose(); } } diff --git a/crates/superposition_core/src/ffi.rs b/crates/superposition_core/src/ffi.rs index e525d1659..0dc88c6dc 100644 --- a/crates/superposition_core/src/ffi.rs +++ b/crates/superposition_core/src/ffi.rs @@ -219,6 +219,35 @@ fn ffi_parse_json_config(json_content: String) -> Result .map_err(|e| OperationError::Unexpected(e.to_string())) } +#[uniffi::export] +fn ffi_parse_config_file_with_filters( + file_content: String, + format: String, + dimension_data: Option>, + prefix: Option>, +) -> Result { + let dimension_data = dimension_data + .map(json_from_map) + .transpose() + .map_err(|err| OperationError::Unexpected(err.to_string()))?; + let prefix_list = prefix.map(HashSet::from_iter); + + let config = match format.to_lowercase().as_str() { + "json" => JsonFormat::parse_config(&file_content) + .map_err(|e| OperationError::Unexpected(e.to_string()))?, + "toml" => TomlFormat::parse_config(&file_content) + .map_err(|e| OperationError::Unexpected(e.to_string()))?, + _ => { + return Err(OperationError::Unexpected(format!( + "Unsupported format: {}. Supported formats are 'json' and 'toml'.", + format + ))); + } + }; + + Ok(config.filter(dimension_data.as_ref(), prefix_list.as_ref())) +} + #[derive(Default)] pub struct CacheData { pub config: Config, @@ -395,4 +424,49 @@ impl ProviderCache { ), }) } + + fn get_applicable_variants( + &self, + dimension_data: Option>, + prefix: Option>, + targeting_key: String, + ) -> Result, OperationError> { + let dimension_data = dimension_data + .map(json_from_map) + .transpose() + .map_err(|err| OperationError::Unexpected(err.to_string()))? + .unwrap_or_default(); + + let (exps, exp_grps, dimensions_info) = { + let cache_data = self.data.lock().map_err(|err| { + OperationError::Unexpected(format!( + "Failed to acquire cache lock: {}", + err + )) + })?; + + let exp_config = cache_data.experiment.as_ref().ok_or_else(|| { + OperationError::Unexpected( + "Experiment configuration not initialized".to_string(), + ) + })?; + + ( + exp_config.experiments.clone(), + exp_config.experiment_groups.clone(), + cache_data.config.dimensions.clone(), + ) + }; + + let variants = get_applicable_variants( + &dimensions_info, + exps, + &exp_grps, + &dimension_data, + &targeting_key, + prefix, + ); + + Ok(variants) + } } diff --git a/crates/superposition_provider/examples/local_file_example.rs b/crates/superposition_provider/examples/local_file_example.rs index a5133bacf..be48b560c 100644 --- a/crates/superposition_provider/examples/local_file_example.rs +++ b/crates/superposition_provider/examples/local_file_example.rs @@ -11,7 +11,8 @@ async fn main() { env_logger::init(); let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let file_source = FileDataSource::new(manifest_dir.join("examples/config.toml")); + let file_source = + FileDataSource::new(manifest_dir.join("examples/config.toml")).unwrap(); let provider = LocalResolutionProvider::new( Box::new(file_source), diff --git a/crates/superposition_provider/examples/local_file_watch_example.rs b/crates/superposition_provider/examples/local_file_watch_example.rs index 985dfefeb..444bcf4f4 100644 --- a/crates/superposition_provider/examples/local_file_watch_example.rs +++ b/crates/superposition_provider/examples/local_file_watch_example.rs @@ -16,7 +16,7 @@ async fn main() { println!("Watching config file: {:?}", config_path); println!("Edit the file in another terminal to see changes.\n"); - let file_source = FileDataSource::new(config_path); + let file_source = FileDataSource::new(config_path).unwrap(); let provider = LocalResolutionProvider::new( Box::new(file_source), diff --git a/crates/superposition_provider/examples/local_with_fallback_example.rs b/crates/superposition_provider/examples/local_with_fallback_example.rs index d63a1d085..74684571d 100644 --- a/crates/superposition_provider/examples/local_with_fallback_example.rs +++ b/crates/superposition_provider/examples/local_with_fallback_example.rs @@ -20,7 +20,8 @@ async fn main() { )); let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let file_source = FileDataSource::new(manifest_dir.join("examples/config.toml")); + let file_source = + FileDataSource::new(manifest_dir.join("examples/config.toml")).unwrap(); let provider = LocalResolutionProvider::new( Box::new(http_source), diff --git a/crates/superposition_provider/src/data_source.rs b/crates/superposition_provider/src/data_source.rs index 382d6f6a5..3a6d8cf37 100644 --- a/crates/superposition_provider/src/data_source.rs +++ b/crates/superposition_provider/src/data_source.rs @@ -55,7 +55,7 @@ impl Display for FetchResponse { /// Holds a resolved configuration along with the time it was fetched. #[derive(Debug, Clone)] pub struct ConfigData { - pub config: Config, + pub data: Config, pub fetched_at: DateTime, } @@ -63,12 +63,12 @@ impl Display for ConfigData { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, - "ConfigData(fetched_at: {}, config.contexts: {}, config.overrides: {}, config.default_configs: {}, config.dimensions: {})", + "ConfigData(fetched_at: {}, data.contexts: {}, data.overrides: {}, data.default_configs: {}, data.dimensions: {})", self.fetched_at, - self.config.contexts.len(), - self.config.overrides.len(), - self.config.default_configs.len(), - self.config.dimensions.len() + self.data.contexts.len(), + self.data.overrides.len(), + self.data.default_configs.len(), + self.data.dimensions.len() ) } } diff --git a/crates/superposition_provider/src/data_source/file.rs b/crates/superposition_provider/src/data_source/file.rs index 5ad760b41..6aa350492 100644 --- a/crates/superposition_provider/src/data_source/file.rs +++ b/crates/superposition_provider/src/data_source/file.rs @@ -5,7 +5,7 @@ use async_trait::async_trait; use chrono::{DateTime, Utc}; use notify::{Event, RecommendedWatcher, Watcher}; use serde_json::{Map, Value}; -use superposition_core::{ConfigFormat, TomlFormat}; +use superposition_core::{ConfigFormat, JsonFormat, TomlFormat}; use tokio::sync::broadcast; use crate::data_source::FetchResponse; @@ -20,15 +20,32 @@ struct WatcherInner { pub struct FileDataSource { file_path: PathBuf, + file_format: &'static str, watcher: Mutex>, } impl FileDataSource { - pub fn new(file_path: PathBuf) -> Self { - Self { + pub fn new(file_path: PathBuf) -> std::result::Result { + let file_format = match file_path + .extension() + .and_then(|ext| ext.to_str()) + .map(|s| s.to_lowercase()) + { + Some(ref ext) if ext == "json" => "json", + Some(ref ext) if ext == "toml" => "toml", + Some(ext) => return Err(format!("Unsupported file extension '{}'.", ext)), + None => { + return Err( + "File path must have an extension to determine format.".into() + ); + } + }; + + Ok(Self { file_path, + file_format, watcher: Mutex::new(None), - } + }) } } @@ -51,12 +68,20 @@ impl SuperpositionDataSource for FileDataSource { )) })?; - let config = TomlFormat::parse_config(&content).map_err(|e| { - SuperpositionError::ConfigError(format!("Failed to parse TOML config: {}", e)) + let parser = match self.file_format.to_lowercase().as_str() { + "json" => JsonFormat::parse_config, + _ => TomlFormat::parse_config, + }; + let config = parser(&content).map_err(|e| { + SuperpositionError::ConfigError(format!( + "Failed to parse {} config: {}", + self.file_format.to_uppercase(), + e + )) })?; Ok(FetchResponse::Data(ConfigData { - config, + data: config, fetched_at: now, })) } @@ -71,7 +96,7 @@ impl SuperpositionDataSource for FileDataSource { .fetch_config(if_modified_since) .await? .map_data(|mut data| { - data.config = data.config.filter( + data.data = data.data.filter( context.as_ref(), prefix_filter.map(|p| p.into_iter().collect()).as_ref(), ); diff --git a/crates/superposition_provider/src/data_source/http.rs b/crates/superposition_provider/src/data_source/http.rs index 227f13ff4..fa24a191c 100644 --- a/crates/superposition_provider/src/data_source/http.rs +++ b/crates/superposition_provider/src/data_source/http.rs @@ -140,7 +140,7 @@ impl HttpDataSource { Utc.timestamp_nanos(res.last_modified.as_nanos() as i64); ConversionUtils::convert_get_config_response(res) .map(|d| ConfigData { - config: d, + data: d, fetched_at: modified_at, }) .map(FetchResponse::Data) diff --git a/crates/superposition_provider/src/local_provider.rs b/crates/superposition_provider/src/local_provider.rs index 7c5c52736..4d0ff8896 100644 --- a/crates/superposition_provider/src/local_provider.rs +++ b/crates/superposition_provider/src/local_provider.rs @@ -268,62 +268,72 @@ impl LocalResolutionProvider { .map(|data| data.fetched_at) }; - let config_result = self.primary.fetch_config(last_fetched_at).await; - let mut resp = match config_result { - Ok(FetchResponse::Data(data)) => { - let mut cached = self.cached_config.write().await; - *cached = Some(data); - log::debug!("LocalResolutionProvider: config refreshed from primary"); - Ok(()) - } - Ok(FetchResponse::NotModified) => { - log::debug!("LocalResolutionProvider: config not modified"); - Ok(()) - } - Err(e) => { - log::warn!( + let config_resp = async { + match self.primary.fetch_config(last_fetched_at).await { + Ok(FetchResponse::Data(data)) => { + let mut cached = self.cached_config.write().await; + *cached = Some(data); + log::debug!("LocalResolutionProvider: config refreshed from primary"); + Ok(()) + } + Ok(FetchResponse::NotModified) => { + log::debug!("LocalResolutionProvider: config not modified"); + Ok(()) + } + Err(e) => { + log::warn!( "LocalResolutionProvider: config refresh failed, keeping last known good: {}", e ); - Err(e) + Err(e) + } } }; - if self.primary.supports_experiments() { - let exp_last_fetched_at = { - self.cached_experiments - .read() + let exp_resp = async { + if self.primary.supports_experiments() { + let exp_last_fetched_at = { + self.cached_experiments + .read() + .await + .as_ref() + .map(|d| d.fetched_at) + }; + match self + .primary + .fetch_active_experiments(exp_last_fetched_at) .await - .as_ref() - .map(|d| d.fetched_at) - }; - match self - .primary - .fetch_active_experiments(exp_last_fetched_at) - .await - { - Ok(exp_resp) => { - let mut cached = self.cached_experiments.write().await; - if let Some(data) = exp_resp.into_data() { - *cached = Some(data); + { + Ok(exp_resp) => { + let mut cached = self.cached_experiments.write().await; + if let Some(data) = exp_resp.into_data() { + *cached = Some(data); + } + log::debug!( + "LocalResolutionProvider: experiments refreshed from primary" + ); + Ok(()) } - log::debug!( - "LocalResolutionProvider: experiments refreshed from primary" - ); - } - Err(e) => { - log::warn!( - "LocalResolutionProvider: experiment refresh failed, keeping last known good: {}", - e - ); - if resp.is_ok() { - resp = Err(e); + Err(e) => { + log::warn!( + "LocalResolutionProvider: experiment refresh failed, keeping last known good: {}", + e + ); + Err(e) } } + } else { + Ok(()) } - } + }; + + let (config_resp, exp_resp) = tokio::join!(config_resp, exp_resp); - resp + if config_resp.is_ok() { + exp_resp + } else { + config_resp + } } async fn start_polling(&self, interval: u64) -> JoinHandle<()> { @@ -407,7 +417,7 @@ impl LocalResolutionProvider { async fn get_dimensions_info(&self) -> HashMap { let cached = self.cached_config.read().await; match cached.as_ref() { - Some(data) => data.config.dimensions.clone(), + Some(data) => data.data.dimensions.clone(), None => HashMap::new(), } } @@ -456,10 +466,10 @@ impl LocalResolutionProvider { let cached = self.cached_config.read().await; match cached.as_ref() { Some(config_data) => eval_config( - (*config_data.config.default_configs).clone(), - &config_data.config.contexts, - &config_data.config.overrides, - &config_data.config.dimensions, + (*config_data.data.default_configs).clone(), + &config_data.data.contexts, + &config_data.data.overrides, + &config_data.data.dimensions, &query_data, MergeStrategy::MERGE, prefix_filter, @@ -479,13 +489,6 @@ impl LocalResolutionProvider { #[async_trait] impl AllFeatureProvider for LocalResolutionProvider { - async fn resolve_all_features( - &self, - context: EvaluationContext, - ) -> Result> { - self.eval_with_context(context, None).await - } - async fn resolve_all_features_with_filter( &self, context: EvaluationContext, @@ -631,7 +634,7 @@ impl SuperpositionDataSource for LocalResolutionProvider { .await? .map_data(|mut c| { let prefix = prefix_filter.map(HashSet::from_iter); - c.config = c.config.filter(context.as_ref(), prefix.as_ref()); + c.data = c.data.filter(context.as_ref(), prefix.as_ref()); c }); diff --git a/crates/superposition_provider/src/remote_provider.rs b/crates/superposition_provider/src/remote_provider.rs index eff315724..ba4ead7f0 100644 --- a/crates/superposition_provider/src/remote_provider.rs +++ b/crates/superposition_provider/src/remote_provider.rs @@ -101,13 +101,6 @@ impl SuperpositionAPIProvider { #[async_trait] impl AllFeatureProvider for SuperpositionAPIProvider { - async fn resolve_all_features( - &self, - context: EvaluationContext, - ) -> Result> { - self.resolve_remote(context, None).await - } - async fn resolve_all_features_with_filter( &self, context: EvaluationContext, diff --git a/crates/superposition_provider/src/traits.rs b/crates/superposition_provider/src/traits.rs index f2abcfeb1..2879e6b9e 100644 --- a/crates/superposition_provider/src/traits.rs +++ b/crates/superposition_provider/src/traits.rs @@ -31,7 +31,9 @@ pub trait AllFeatureProvider: Send + Sync { async fn resolve_all_features( &self, context: EvaluationContext, - ) -> Result>; + ) -> Result> { + self.resolve_all_features_with_filter(context, None).await + } /// Resolve all features for the given evaluation context, optionally /// filtered to only include keys matching the provided prefixes. diff --git a/crates/superposition_provider/tests/config.toml b/crates/superposition_provider/tests/config.toml new file mode 100644 index 000000000..ea596515c --- /dev/null +++ b/crates/superposition_provider/tests/config.toml @@ -0,0 +1,59 @@ +[default-configs] +currency = { value = "Rupee", schema = { enum = [ + "Rupee", + "Dollar", + "Euro", +], type = "string" } } +price = { value = 10000, schema = { minimum = 0, type = "number" } } + + +[dimensions] +city = { position = 3, schema = { type = "string" }, type = "REGULAR" } +customers = { position = 1, schema = { definitions = { gold = { in = [ + { var = "name" }, + [ + "Angit", + "Bhrey", + ], +] }, platinum = { in = [ + { var = "name" }, + [ + "Agush", + "Sauyav", + ], +] } }, enum = [ + "platinum", + "gold", + "otherwise", +], type = "string" }, type = "LOCAL_COHORT:name" } +name = { position = 2, schema = { type = "string" }, type = "REGULAR" } +variantIds = { position = 0, schema = { pattern = ".*", type = "string" }, type = "REGULAR" } + + +[[overrides]] +_context_ = { customers = "platinum" } +price = 5000 + +[[overrides]] +_context_ = { customers = "gold" } +price = 8000 + +[[overrides]] +_context_ = { name = "karbik" } +price = 1 + +[[overrides]] +_context_ = { city = "Boston" } +currency = "Dollar" + +[[overrides]] +_context_ = { city = "Berlin" } +currency = "Euro" + +[[overrides]] +_context_ = { city = "Kolkata", variantIds = "7445772794710855680-test-control" } +price = 8000 + +[[overrides]] +_context_ = { city = "Kolkata", variantIds = "7445772794710855680-test-experimental" } +price = 7000 diff --git a/crates/superposition_provider/tests/integration_test.rs b/crates/superposition_provider/tests/integration_test.rs index 9e0f9f811..778a91aba 100644 --- a/crates/superposition_provider/tests/integration_test.rs +++ b/crates/superposition_provider/tests/integration_test.rs @@ -1,8 +1,9 @@ use open_feature::{provider::FeatureProvider, EvaluationContext, OpenFeature}; use serde_json::Value; use superposition_provider::{ - ExperimentationOptions, OnDemandStrategy, RefreshStrategy, SuperpositionProvider, - SuperpositionProviderOptions, + data_source::{file::FileDataSource, http::HttpDataSource}, + AllFeatureProvider, LocalResolutionProvider, PollingStrategy, RefreshStrategy, + SuperpositionAPIProvider, SuperpositionOptions, }; use superposition_sdk::{ types::{ContextPut, DimensionType, Variant, WorkspaceStatus}, @@ -377,229 +378,624 @@ async fn setup_with_sdk(org_id: &str, workspace_id: &str) { async fn run_provider_tests(org_id: &str, workspace_id: &str) { println!("\n=== Starting OpenFeature provider tests ===\n"); - // Create provider with on-demand refresh strategy - let provider_options = SuperpositionProviderOptions { + let refresh_strategy = RefreshStrategy::Polling(PollingStrategy::default()); + let http_options = SuperpositionOptions { endpoint: ENDPOINT.to_string(), token: TOKEN.to_string(), org_id: org_id.to_string(), workspace_id: workspace_id.to_string(), - refresh_strategy: RefreshStrategy::OnDemand(OnDemandStrategy::default()), - evaluation_cache: None, - fallback_config: None, - experimentation_options: Some(ExperimentationOptions { - refresh_strategy: RefreshStrategy::OnDemand(OnDemandStrategy::default()), - evaluation_cache: None, - default_toss: None, - }), }; + let wrong_http_options = SuperpositionOptions { + endpoint: ENDPOINT.to_string(), + token: "12345678".to_string(), + org_id: org_id.to_string(), + workspace_id: "workspace_id".to_string(), + }; + let primary_source = HttpDataSource::new(http_options.clone()); + let fallback_source = FileDataSource::new("tests/config.toml".into()).unwrap(); - let provider = SuperpositionProvider::new(provider_options); - // Test 0: Verify provider clone works (sanity check) - println!("Test 0: Verify provider clone works (sanity check)"); - { - let mut provider_clone = provider.clone(); - provider_clone - .initialize(&EvaluationContext::default()) - .await; - let ctx = EvaluationContext::default().with_custom_field("name", "karbik"); - let all_fields = provider_clone.resolve_full_config(&ctx).await.unwrap(); - - assert_eq!( - all_fields.get("price").unwrap(), - &Value::from(1), - "Price should be 1 for karbik" - ); - assert_eq!( - all_fields.get("currency").unwrap(), - &Value::from("Rupee"), - "Currency should be default Rupee" - ); - println!(" ✓ Test passed\n"); - } - - // Set provider as the global provider - let mut api = OpenFeature::singleton_mut().await; - api.set_provider(provider).await; - - let client = api.create_client(); - - // Test 1: Default values (no context) - println!("Test 1: Default values (no context)"); - { - let ctx = EvaluationContext::default(); - let price = client - .get_float_value("price", Some(&ctx), None) - .await - .unwrap(); - let currency = client - .get_string_value("currency", Some(&ctx), None) - .await - .unwrap(); - - assert_eq!(price, 10000.0, "Default price should be 10000"); - assert_eq!(currency, "Rupee", "Default currency should be Rupee"); - println!(" ✓ Test passed\n"); - } - - // Test 2: Platinum customer - Agush, no city - println!("Test 2: Platinum customer - Agush (no city)"); - { - let ctx = EvaluationContext::default().with_custom_field("name", "Agush"); - let price = client - .get_float_value("price", Some(&ctx), None) - .await - .unwrap(); - let currency = client - .get_string_value("currency", Some(&ctx), None) - .await - .unwrap(); - - assert_eq!(price, 5000.0, "Price should be 5000 for platinum customer"); - assert_eq!(currency, "Rupee", "Currency should be default Rupee"); - println!(" ✓ Test passed\n"); - } - - // Test 3: Platinum customer - Sauyav, with city Boston - println!("Test 3: Platinum customer - Sauyav with city Boston"); - { - let ctx = EvaluationContext::default() - .with_custom_field("name", "Sauyav") - .with_custom_field("city", "Boston"); - let price = client - .get_float_value("price", Some(&ctx), None) - .await - .unwrap(); - let currency = client - .get_string_value("currency", Some(&ctx), None) - .await - .unwrap(); - - assert_eq!(price, 5000.0, "Price should be 5000"); - assert_eq!(currency, "Dollar", "Currency should be Dollar"); - println!(" ✓ Test passed\n"); - } - - // Test 4: Regular customer - John (no city) - println!("Test 4: Regular customer - John (no city)"); { - let ctx = EvaluationContext::default().with_custom_field("name", "John"); - let price = client - .get_float_value("price", Some(&ctx), None) - .await - .unwrap(); - let currency = client - .get_string_value("currency", Some(&ctx), None) - .await - .unwrap(); - - assert_eq!(price, 10000.0, "Price should be default 10000"); - assert_eq!(currency, "Rupee", "Currency should be default Rupee"); - println!(" ✓ Test passed\n"); - } + println!("Testing LocalResolutionProvider with HTTP data source (no fallback)"); - // Test 5: Platinum customer - Sauyav with city Berlin - println!("Test 5: Platinum customer - Sauyav with city Berlin"); - { - let ctx = EvaluationContext::default() - .with_custom_field("name", "Sauyav") - .with_custom_field("city", "Berlin"); - let price = client - .get_float_value("price", Some(&ctx), None) - .await - .unwrap(); - let currency = client - .get_string_value("currency", Some(&ctx), None) - .await - .unwrap(); - - assert_eq!(price, 5000.0, "Price should be 5000"); - assert_eq!(currency, "Euro", "Currency should be Euro in Berlin"); - println!(" ✓ Test passed\n"); - } + let provider = LocalResolutionProvider::new( + Box::new(primary_source), + None, + refresh_strategy.clone(), + ); - // Test 6: Regular customer - John with city Boston - println!("Test 6: Regular customer - John with city Boston"); - { - let ctx = EvaluationContext::default() - .with_custom_field("name", "John") - .with_custom_field("city", "Boston"); - let price = client - .get_float_value("price", Some(&ctx), None) - .await - .unwrap(); - let currency = client - .get_string_value("currency", Some(&ctx), None) - .await - .unwrap(); - - assert_eq!(price, 10000.0, "Price should be default 10000"); - assert_eq!(currency, "Dollar", "Currency should be Dollar in Boston"); - println!(" ✓ Test passed\n"); + // Test 0: Verify provider clone works (sanity check) + println!("Test 0: Verify provider clone works (sanity check)"); + { + let mut provider_clone = provider.clone(); + provider_clone + .initialize(&EvaluationContext::default()) + .await; + let ctx = EvaluationContext::default().with_custom_field("name", "karbik"); + let all_fields = provider_clone.resolve_all_features(ctx).await.unwrap(); + + assert_eq!( + all_fields.get("price").unwrap(), + &Value::from(1), + "Price should be 1 for karbik" + ); + assert_eq!( + all_fields.get("currency").unwrap(), + &Value::from("Rupee"), + "Currency should be default Rupee" + ); + println!(" ✓ Test passed\n"); + } + + // Set provider as the global provider + let mut api = OpenFeature::singleton_mut().await; + api.set_provider(provider).await; + + let client = api.create_client(); + + // Test 1: Default values (no context) + println!("Test 1: Default values (no context)"); + { + let ctx = EvaluationContext::default(); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 10000.0, "Default price should be 10000"); + assert_eq!(currency, "Rupee", "Default currency should be Rupee"); + println!(" ✓ Test passed\n"); + } + + // Test 2: Platinum customer - Agush, no city + println!("Test 2: Platinum customer - Agush (no city)"); + { + let ctx = EvaluationContext::default().with_custom_field("name", "Agush"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 5000.0, "Price should be 5000 for platinum customer"); + assert_eq!(currency, "Rupee", "Currency should be default Rupee"); + println!(" ✓ Test passed\n"); + } + + // Test 3: Platinum customer - Sauyav, with city Boston + println!("Test 3: Platinum customer - Sauyav with city Boston"); + { + let ctx = EvaluationContext::default() + .with_custom_field("name", "Sauyav") + .with_custom_field("city", "Boston"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 5000.0, "Price should be 5000"); + assert_eq!(currency, "Dollar", "Currency should be Dollar"); + println!(" ✓ Test passed\n"); + } + + // Test 4: Regular customer - John (no city) + println!("Test 4: Regular customer - John (no city)"); + { + let ctx = EvaluationContext::default().with_custom_field("name", "John"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 10000.0, "Price should be default 10000"); + assert_eq!(currency, "Rupee", "Currency should be default Rupee"); + println!(" ✓ Test passed\n"); + } + + // Test 5: Platinum customer - Sauyav with city Berlin + println!("Test 5: Platinum customer - Sauyav with city Berlin"); + { + let ctx = EvaluationContext::default() + .with_custom_field("name", "Sauyav") + .with_custom_field("city", "Berlin"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 5000.0, "Price should be 5000"); + assert_eq!(currency, "Euro", "Currency should be Euro in Berlin"); + println!(" ✓ Test passed\n"); + } + + // Test 6: Regular customer - John with city Boston + println!("Test 6: Regular customer - John with city Boston"); + { + let ctx = EvaluationContext::default() + .with_custom_field("name", "John") + .with_custom_field("city", "Boston"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 10000.0, "Price should be default 10000"); + assert_eq!(currency, "Dollar", "Currency should be Dollar in Boston"); + println!(" ✓ Test passed\n"); + } + + // Test 7: Edge case customer - karbik (specific override) + println!("Test 7: Edge case customer - karbik (specific override)"); + { + let ctx = EvaluationContext::default().with_custom_field("name", "karbik"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 1.0, "Price should be 1 for karbik"); + assert_eq!(currency, "Rupee", "Currency should be default Rupee"); + println!(" ✓ Test passed\n"); + } + + // Test 8: Edge case customer - karbik with city Boston + println!("Test 8: Edge case customer - karbik with city Boston"); + { + let ctx = EvaluationContext::default() + .with_custom_field("name", "karbik") + .with_custom_field("city", "Boston"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 1.0, "Price should be 1 for karbik"); + assert_eq!(currency, "Dollar", "Currency should be Dollar in Boston"); + println!(" ✓ Test passed\n"); + } + + // Test 9: Experiment case - Kolkata pricing + println!("Test 9: Experiment case: Kolkata pricing"); + { + let ctx = EvaluationContext::default() + .with_custom_field("city", "Kolkata") + .with_targeting_key("test"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + println!(" Retrieved price: {}, currency: {}", price, currency); + + assert!( + price == 8000.0 || price == 7000.0, + "Price should be either 8000 (control) or 7000 (experiment) " + ); + assert_eq!(currency, "Rupee", "Currency should be default Rupee"); + println!(" ✓ Experiment test passed "); + } + + api.shutdown().await; + + println!("\n=== All tests passed! ===\n"); } - - // Test 7: Edge case customer - karbik (specific override) - println!("Test 7: Edge case customer - karbik (specific override)"); { - let ctx = EvaluationContext::default().with_custom_field("name", "karbik"); - let price = client - .get_float_value("price", Some(&ctx), None) - .await - .unwrap(); - let currency = client - .get_string_value("currency", Some(&ctx), None) - .await - .unwrap(); - - assert_eq!(price, 1.0, "Price should be 1 for karbik"); - assert_eq!(currency, "Rupee", "Currency should be default Rupee"); - println!(" ✓ Test passed\n"); + println!("Testing SuperpositionAPIProvider"); + let provider = SuperpositionAPIProvider::new(http_options); + // Set provider as the global provider + let mut api = OpenFeature::singleton_mut().await; + api.set_provider(provider).await; + + let client = api.create_client(); + + // Test 1: Default values (no context) + println!("Test 1: Default values (no context)"); + { + let ctx = EvaluationContext::default(); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 10000.0, "Default price should be 10000"); + assert_eq!(currency, "Rupee", "Default currency should be Rupee"); + println!(" ✓ Test passed\n"); + } + + // Test 2: Platinum customer - Agush, no city + println!("Test 2: Platinum customer - Agush (no city)"); + { + let ctx = EvaluationContext::default().with_custom_field("name", "Agush"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 5000.0, "Price should be 5000 for platinum customer"); + assert_eq!(currency, "Rupee", "Currency should be default Rupee"); + println!(" ✓ Test passed\n"); + } + + // Test 3: Platinum customer - Sauyav, with city Boston + println!("Test 3: Platinum customer - Sauyav with city Boston"); + { + let ctx = EvaluationContext::default() + .with_custom_field("name", "Sauyav") + .with_custom_field("city", "Boston"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 5000.0, "Price should be 5000"); + assert_eq!(currency, "Dollar", "Currency should be Dollar"); + println!(" ✓ Test passed\n"); + } + + // Test 4: Regular customer - John (no city) + println!("Test 4: Regular customer - John (no city)"); + { + let ctx = EvaluationContext::default().with_custom_field("name", "John"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 10000.0, "Price should be default 10000"); + assert_eq!(currency, "Rupee", "Currency should be default Rupee"); + println!(" ✓ Test passed\n"); + } + + // Test 5: Platinum customer - Sauyav with city Berlin + println!("Test 5: Platinum customer - Sauyav with city Berlin"); + { + let ctx = EvaluationContext::default() + .with_custom_field("name", "Sauyav") + .with_custom_field("city", "Berlin"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 5000.0, "Price should be 5000"); + assert_eq!(currency, "Euro", "Currency should be Euro in Berlin"); + println!(" ✓ Test passed\n"); + } + + // Test 6: Regular customer - John with city Boston + println!("Test 6: Regular customer - John with city Boston"); + { + let ctx = EvaluationContext::default() + .with_custom_field("name", "John") + .with_custom_field("city", "Boston"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 10000.0, "Price should be default 10000"); + assert_eq!(currency, "Dollar", "Currency should be Dollar in Boston"); + println!(" ✓ Test passed\n"); + } + + // Test 7: Edge case customer - karbik (specific override) + println!("Test 7: Edge case customer - karbik (specific override)"); + { + let ctx = EvaluationContext::default().with_custom_field("name", "karbik"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 1.0, "Price should be 1 for karbik"); + assert_eq!(currency, "Rupee", "Currency should be default Rupee"); + println!(" ✓ Test passed\n"); + } + + // Test 8: Edge case customer - karbik with city Boston + println!("Test 8: Edge case customer - karbik with city Boston"); + { + let ctx = EvaluationContext::default() + .with_custom_field("name", "karbik") + .with_custom_field("city", "Boston"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 1.0, "Price should be 1 for karbik"); + assert_eq!(currency, "Dollar", "Currency should be Dollar in Boston"); + println!(" ✓ Test passed\n"); + } + + // Test 9: Experiment case - Kolkata pricing + println!("Test 9: Experiment case: Kolkata pricing"); + { + let ctx = EvaluationContext::default() + .with_custom_field("city", "Kolkata") + .with_targeting_key("test"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + println!(" Retrieved price: {}, currency: {}", price, currency); + + assert!( + price == 8000.0 || price == 7000.0, + "Price should be either 8000 (control) or 7000 (experiment) " + ); + assert_eq!(currency, "Rupee", "Currency should be default Rupee"); + println!(" ✓ Experiment test passed "); + } + + api.shutdown().await; + println!("\n=== All tests passed! ===\n"); } - // Test 8: Edge case customer - karbik with city Boston - println!("Test 8: Edge case customer - karbik with city Boston"); { - let ctx = EvaluationContext::default() - .with_custom_field("name", "karbik") - .with_custom_field("city", "Boston"); - let price = client - .get_float_value("price", Some(&ctx), None) - .await - .unwrap(); - let currency = client - .get_string_value("currency", Some(&ctx), None) - .await - .unwrap(); - - assert_eq!(price, 1.0, "Price should be 1 for karbik"); - assert_eq!(currency, "Dollar", "Currency should be Dollar in Boston"); - println!(" ✓ Test passed\n"); - } + println!("Testing LocalResolutionProvider with wrong HTTP data source but valid file fallback"); + let provider = LocalResolutionProvider::new( + Box::new(HttpDataSource::new(wrong_http_options)), + Some(Box::new(fallback_source)), + refresh_strategy, + ); - // Test 9: Experiment case - Kolkata pricing - println!("Test 9: Experiment case: Kolkata pricing"); - { - let ctx = EvaluationContext::default() - .with_custom_field("city", "Kolkata") - .with_targeting_key("test"); - let price = client - .get_float_value("price", Some(&ctx), None) - .await - .unwrap(); - let currency = client - .get_string_value("currency", Some(&ctx), None) - .await - .unwrap(); - println!(" Retrieved price: {}, currency: {}", price, currency); - - assert!( - price == 8000.0 || price == 7000.0, - "Price should be either 8000 (control) or 7000 (experiment) " + // Set provider as the global provider + let mut api = OpenFeature::singleton_mut().await; + api.set_provider(provider).await; + + let client = api.create_client(); + + // Test 1: Default values (no context) + println!("Test 1: Default values (no context)"); + { + let ctx = EvaluationContext::default(); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 10000.0, "Default price should be 10000"); + assert_eq!(currency, "Rupee", "Default currency should be Rupee"); + println!(" ✓ Test passed\n"); + } + + // Test 2: Platinum customer - Agush, no city + println!("Test 2: Platinum customer - Agush (no city)"); + { + let ctx = EvaluationContext::default().with_custom_field("name", "Agush"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 5000.0, "Price should be 5000 for platinum customer"); + assert_eq!(currency, "Rupee", "Currency should be default Rupee"); + println!(" ✓ Test passed\n"); + } + + // Test 3: Platinum customer - Sauyav, with city Boston + println!("Test 3: Platinum customer - Sauyav with city Boston"); + { + let ctx = EvaluationContext::default() + .with_custom_field("name", "Sauyav") + .with_custom_field("city", "Boston"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 5000.0, "Price should be 5000"); + assert_eq!(currency, "Dollar", "Currency should be Dollar"); + println!(" ✓ Test passed\n"); + } + + // Test 4: Regular customer - John (no city) + println!("Test 4: Regular customer - John (no city)"); + { + let ctx = EvaluationContext::default().with_custom_field("name", "John"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 10000.0, "Price should be default 10000"); + assert_eq!(currency, "Rupee", "Currency should be default Rupee"); + println!(" ✓ Test passed\n"); + } + + // Test 5: Platinum customer - Sauyav with city Berlin + println!("Test 5: Platinum customer - Sauyav with city Berlin"); + { + let ctx = EvaluationContext::default() + .with_custom_field("name", "Sauyav") + .with_custom_field("city", "Berlin"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 5000.0, "Price should be 5000"); + assert_eq!(currency, "Euro", "Currency should be Euro in Berlin"); + println!(" ✓ Test passed\n"); + } + + // Test 6: Regular customer - John with city Boston + println!("Test 6: Regular customer - John with city Boston"); + { + let ctx = EvaluationContext::default() + .with_custom_field("name", "John") + .with_custom_field("city", "Boston"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 10000.0, "Price should be default 10000"); + assert_eq!(currency, "Dollar", "Currency should be Dollar in Boston"); + println!(" ✓ Test passed\n"); + } + + // Test 7: Edge case customer - karbik (specific override) + println!("Test 7: Edge case customer - karbik (specific override)"); + { + let ctx = EvaluationContext::default().with_custom_field("name", "karbik"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 1.0, "Price should be 1 for karbik"); + assert_eq!(currency, "Rupee", "Currency should be default Rupee"); + println!(" ✓ Test passed\n"); + } + + // Test 8: Edge case customer - karbik with city Boston + println!("Test 8: Edge case customer - karbik with city Boston"); + { + let ctx = EvaluationContext::default() + .with_custom_field("name", "karbik") + .with_custom_field("city", "Boston"); + let price = client + .get_float_value("price", Some(&ctx), None) + .await + .unwrap(); + let currency = client + .get_string_value("currency", Some(&ctx), None) + .await + .unwrap(); + + assert_eq!(price, 1.0, "Price should be 1 for karbik"); + assert_eq!(currency, "Dollar", "Currency should be Dollar in Boston"); + println!(" ✓ Test passed\n"); + } + + println!( + "Experiment not supported in file data source, skipping experiment test" ); - assert_eq!(currency, "Rupee", "Currency should be default Rupee"); - println!(" ✓ Experiment test passed "); + // // Test 9: Experiment case - Kolkata pricing + // println!("Test 9: Experiment case: Kolkata pricing"); + // { + // let ctx = EvaluationContext::default() + // .with_custom_field("city", "Kolkata") + // .with_targeting_key("test"); + // let price = client + // .get_float_value("price", Some(&ctx), None) + // .await + // .unwrap(); + // let currency = client + // .get_string_value("currency", Some(&ctx), None) + // .await + // .unwrap(); + // println!(" Retrieved price: {}, currency: {}", price, currency); + + // assert!( + // price == 8000.0 || price == 7000.0, + // "Price should be either 8000 (control) or 7000 (experiment) " + // ); + // assert_eq!(currency, "Rupee", "Currency should be default Rupee"); + // println!(" ✓ Experiment test passed "); + // } + + println!("\n=== All tests passed! ===\n"); } - - println!("\n=== All tests passed! ===\n"); } #[tokio::test]