diff --git a/packages/mp/pyproject.toml b/packages/mp/pyproject.toml index ab6632a3e..06bede5fa 100644 --- a/packages/mp/pyproject.toml +++ b/packages/mp/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "mp" -version = "1.25.6" +version = "1.25.7" description = "General Purpose CLI tool for Google SecOps Marketplace" readme = "README.md" authors = [ diff --git a/packages/mp/src/mp/core/constants.py b/packages/mp/src/mp/core/constants.py index c5580824c..1486b118f 100644 --- a/packages/mp/src/mp/core/constants.py +++ b/packages/mp/src/mp/core/constants.py @@ -185,13 +185,14 @@ "Git Verify SSL", "Siemplify Verify SSL", } -LONG_DESCRIPTION_MAX_LENGTH: int = 2_200 +LONG_DESCRIPTION_MAX_LENGTH: int = 2050 SHORT_DESCRIPTION_MAX_LENGTH: int = 2050 DISPLAY_NAME_MAX_LENGTH: int = 150 MAX_PARAMETERS_LENGTH: int = 50 PARAM_NAME_MAX_LENGTH: int = 150 PARAM_NAME_MAX_WORDS: int = 13 MINIMUM_SCRIPT_VERSION: float = 1.0 +MAX_SCRIPT_RESULT_NAME_LENGTH: int = 100 # ------------------ Playbooks ------------------ diff --git a/packages/mp/src/mp/core/data_models/integrations/action/metadata.py b/packages/mp/src/mp/core/data_models/integrations/action/metadata.py index 1570c7ea2..d99d7dab2 100644 --- a/packages/mp/src/mp/core/data_models/integrations/action/metadata.py +++ b/packages/mp/src/mp/core/data_models/integrations/action/metadata.py @@ -107,7 +107,9 @@ class ActionMetadata(ComponentMetadata[BuiltActionMetadata, NonBuiltActionMetada ] default_result_value: str | None creator: str - script_result_name: str + script_result_name: Annotated[ + str, pydantic.Field(max_length=mp.core.constants.MAX_SCRIPT_RESULT_NAME_LENGTH) + ] simulation_data_json: str version: Annotated[ pydantic.PositiveFloat, @@ -185,7 +187,7 @@ def _from_built(cls, file_name: str, built: BuiltActionMetadata) -> Self: for drm in built.get("DynamicResultsMetadata", []) or [] ], integration_identifier=built["IntegrationIdentifier"], - is_async=built.get("IsAsync", False), + is_async=v if (v := built.get("IsAsync")) is not None else False, is_custom=built.get("IsCustom", False), is_enabled=built.get("IsEnabled", True), name=built["Name"], diff --git a/packages/mp/src/mp/core/data_models/integrations/action/parameter.py b/packages/mp/src/mp/core/data_models/integrations/action/parameter.py index 5714f6969..2936d7186 100644 --- a/packages/mp/src/mp/core/data_models/integrations/action/parameter.py +++ b/packages/mp/src/mp/core/data_models/integrations/action/parameter.py @@ -21,7 +21,6 @@ import mp.core.constants import mp.core.utils import mp.core.validators -from mp.core import exclusions from mp.core.data_models.abc import Buildable, RepresentableEnum @@ -85,7 +84,6 @@ class ActionParameter( str, pydantic.Field( max_length=mp.core.constants.PARAM_NAME_MAX_LENGTH, - pattern=exclusions.get_param_display_name_regex(), ), pydantic.AfterValidator(mp.core.validators.validate_param_name), ] diff --git a/packages/mp/src/mp/core/data_models/integrations/connector/parameter.py b/packages/mp/src/mp/core/data_models/integrations/connector/parameter.py index 5d65c1eff..4ce1da209 100644 --- a/packages/mp/src/mp/core/data_models/integrations/connector/parameter.py +++ b/packages/mp/src/mp/core/data_models/integrations/connector/parameter.py @@ -21,7 +21,6 @@ import mp.core.constants import mp.core.utils import mp.core.validators -from mp.core import exclusions from mp.core.data_models.abc import Buildable, RepresentableEnum from mp.core.data_models.integrations.script.parameter import ScriptParamType @@ -58,7 +57,6 @@ class ConnectorParameter( str, pydantic.Field( max_length=mp.core.constants.PARAM_NAME_MAX_LENGTH, - pattern=exclusions.get_param_display_name_regex(), ), pydantic.AfterValidator(mp.core.validators.validate_param_name), ] diff --git a/packages/mp/src/mp/core/data_models/integrations/integration_meta/metadata.py b/packages/mp/src/mp/core/data_models/integrations/integration_meta/metadata.py index 7ccbb27c4..273e93c7e 100644 --- a/packages/mp/src/mp/core/data_models/integrations/integration_meta/metadata.py +++ b/packages/mp/src/mp/core/data_models/integrations/integration_meta/metadata.py @@ -137,7 +137,6 @@ class IntegrationMetadata( str, pydantic.Field( max_length=mp.core.constants.DISPLAY_NAME_MAX_LENGTH, - pattern=exclusions.get_script_display_name_regex(), ), ] identifier: Annotated[ diff --git a/packages/mp/src/mp/core/data_models/integrations/integration_meta/parameter.py b/packages/mp/src/mp/core/data_models/integrations/integration_meta/parameter.py index d3cfaf023..d5eb58628 100644 --- a/packages/mp/src/mp/core/data_models/integrations/integration_meta/parameter.py +++ b/packages/mp/src/mp/core/data_models/integrations/integration_meta/parameter.py @@ -50,7 +50,6 @@ class IntegrationParameter(Buildable[BuiltIntegrationParameter, NonBuiltIntegrat str, pydantic.Field( max_length=mp.core.constants.PARAM_NAME_MAX_LENGTH, - pattern=exclusions.get_param_display_name_regex(), ), pydantic.AfterValidator(mp.core.validators.validate_param_name), ] @@ -74,7 +73,7 @@ def _from_built(cls, built: BuiltIntegrationParameter) -> Self: return cls( name=built["PropertyName"], default_value=built["Value"], - description=built.get("PropertyDescription", ""), + description=v if (v := built.get("PropertyDescription")) is not None else "", is_mandatory=built.get("IsMandatory", False), type_=ScriptParamType(int(built["PropertyType"])), integration_identifier=built["IntegrationIdentifier"], diff --git a/packages/mp/src/mp/core/data_models/integrations/job/metadata.py b/packages/mp/src/mp/core/data_models/integrations/job/metadata.py index 02fda4012..d3c1ad213 100644 --- a/packages/mp/src/mp/core/data_models/integrations/job/metadata.py +++ b/packages/mp/src/mp/core/data_models/integrations/job/metadata.py @@ -74,7 +74,6 @@ class JobMetadata(ComponentMetadata[BuiltJobMetadata, NonBuiltJobMetadata]): str, pydantic.Field( max_length=mp.core.constants.DISPLAY_NAME_MAX_LENGTH, - pattern=exclusions.get_script_display_name_regex(), ), ] parameters: Annotated[ diff --git a/packages/mp/src/mp/core/data_models/integrations/job/parameter.py b/packages/mp/src/mp/core/data_models/integrations/job/parameter.py index 432fcfa4a..74497e37a 100644 --- a/packages/mp/src/mp/core/data_models/integrations/job/parameter.py +++ b/packages/mp/src/mp/core/data_models/integrations/job/parameter.py @@ -22,7 +22,6 @@ import mp.core.constants import mp.core.utils import mp.core.validators -from mp.core import exclusions from mp.core.data_models.abc import Buildable from mp.core.data_models.integrations.script.parameter import ScriptParamType @@ -48,7 +47,6 @@ class JobParameter(Buildable[BuiltJobParameter, NonBuiltJobParameter]): str, pydantic.Field( max_length=mp.core.constants.PARAM_NAME_MAX_LENGTH, - pattern=exclusions.get_param_display_name_regex(), ), pydantic.AfterValidator(mp.core.validators.validate_param_name), ] @@ -64,7 +62,7 @@ class JobParameter(Buildable[BuiltJobParameter, NonBuiltJobParameter]): def _from_built(cls, built: BuiltJobParameter) -> Self: return cls( name=built["Name"], - description=built.get("Description", ""), + description=v if (v := built.get("Description")) is not None else "", is_mandatory=built["IsMandatory"], type_=ScriptParamType(int(built["Type"])), default_value=built.get("DefaultValue"), diff --git a/packages/mp/src/mp/core/exclusions.py b/packages/mp/src/mp/core/exclusions.py index cc4f9b333..641b0caef 100644 --- a/packages/mp/src/mp/core/exclusions.py +++ b/packages/mp/src/mp/core/exclusions.py @@ -62,6 +62,18 @@ def _build_regex_from_list(excluded_names: list[str], base_regex: str) -> str: def get_script_display_name_regex() -> str: """Build and return the script display name regex. + Returns: + The script display name regex. + + """ + data: dict[str, Any] = _load_exclusions_data() + excluded_names: list[str] = data.get("excluded_script_display_names_for_regex", []) + return _build_regex_from_list(excluded_names, r"^[A-Za-z0-9-_,\s]+$") + + +def get_strict_script_display_name_regex() -> str: + """Build and return the strict (validation only) script display name regex. + Returns: The script display name regex. diff --git a/packages/mp/src/mp/validate/pre_build_validation/integrations/__init__.py b/packages/mp/src/mp/validate/pre_build_validation/integrations/__init__.py index 3b707129d..51472a3a7 100644 --- a/packages/mp/src/mp/validate/pre_build_validation/integrations/__init__.py +++ b/packages/mp/src/mp/validate/pre_build_validation/integrations/__init__.py @@ -22,6 +22,7 @@ from .dependency_provider_validation import DependencyProviderValidation from .disabled_validation import NoDisabledComponentsInIntegrationValidation from .documentation_link_validation import IntegrationHasDocumentationLinkValidation +from .fields_validation import FieldsValidation from .integration_ssl_validation import SslParameterExistsInIntegrationValidation from .mapping_rules_validation import IntegrationHasMappingRulesIfHasConnectorValidation from .ping_validation import IntegrationHasPingActionValidation @@ -59,6 +60,7 @@ def _get_non_priority_validations() -> list[Validator]: IntegrationHasDocumentationLinkValidation(), ConnectorsHasDocumentationLinkValidation(), PythonVersionValidation(), + FieldsValidation(), ] diff --git a/packages/mp/src/mp/validate/pre_build_validation/integrations/fields_validation.py b/packages/mp/src/mp/validate/pre_build_validation/integrations/fields_validation.py new file mode 100644 index 000000000..38839c5cd --- /dev/null +++ b/packages/mp/src/mp/validate/pre_build_validation/integrations/fields_validation.py @@ -0,0 +1,147 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +import dataclasses +import re +from typing import TYPE_CHECKING + +from mp.core.data_models.integrations.integration import Integration +from mp.core.exceptions import NonFatalValidationError +from mp.core.exclusions import get_param_display_name_regex, get_strict_script_display_name_regex + +if TYPE_CHECKING: + from pathlib import Path + + from mp.core.data_models.integrations.action.metadata import ActionMetadata + from mp.core.data_models.integrations.action.parameter import ActionParameter + from mp.core.data_models.integrations.connector.metadata import ConnectorMetadata + from mp.core.data_models.integrations.connector.parameter import ConnectorParameter + from mp.core.data_models.integrations.integration_meta.metadata import IntegrationMetadata + from mp.core.data_models.integrations.integration_meta.parameter import IntegrationParameter + from mp.core.data_models.integrations.job.metadata import JobMetadata + from mp.core.data_models.integrations.job.parameter import JobParameter + +METADATA_NAME_REGEX: str = get_strict_script_display_name_regex() +PARAM_NAME_REGEX: str = get_param_display_name_regex() + + +@dataclasses.dataclass(slots=True, frozen=True) +class FieldsValidation: + name: str = "Fields Validation" + + @staticmethod + def run(integration_path: Path) -> None: + """Strict integration fields names. + + Args: + integration_path: The path of the integration to validate. + + Raises: + NonFatalValidationError: If the integration doesn't have a documentation link. + + """ + integration: Integration = Integration.from_non_built_path(integration_path) + + result: list[str] = [] + result.extend(_validate_action_metadata(list(integration.actions_metadata.values()))) + result.extend(_validate_connector_metadata(list(integration.connectors_metadata.values()))) + result.extend(_validate_job_metadata(list(integration.jobs_metadata.values()))) + result.extend(_integration_metadata(integration.metadata)) + + if result: + raise NonFatalValidationError("\n".join(result)) + + +def _validate_action_metadata(all_actions_metadata: list[ActionMetadata]) -> list[str]: + result: list[str] = [] + for metadata in all_actions_metadata: + if not re.match(METADATA_NAME_REGEX, metadata.name): + result.append( + f"Action name: {metadata.name} does not match the regex: {METADATA_NAME_REGEX}" + ) + result.extend(_validate_action_parameters(metadata.parameters)) + + return result + + +def _validate_action_parameters(action_parameters: list[ActionParameter]) -> list[str]: + result: list[str] = [ + f"Action Parameter name: {parameter.name} does not match the regex: {PARAM_NAME_REGEX}" + for parameter in action_parameters + if not re.match(PARAM_NAME_REGEX, parameter.name) + ] + return result + + +def _validate_connector_metadata(all_connectors_metadata: list[ConnectorMetadata]) -> list[str]: + result: list[str] = [] + for metadata in all_connectors_metadata: + if not re.match(METADATA_NAME_REGEX, metadata.name): + result.append( + f"Connector name: {metadata.name} does not match the regex: {METADATA_NAME_REGEX}" + ) + result.extend(_validate_connector_parameters(metadata.parameters)) + return result + + +def _validate_connector_parameters(connector_parameters: list[ConnectorParameter]) -> list[str]: + result: list[str] = [ + f"Connector Parameter name: {parameter.name} does not match the regex: {PARAM_NAME_REGEX}" + for parameter in connector_parameters + if not re.match(PARAM_NAME_REGEX, parameter.name) + ] + return result + + +def _validate_job_metadata(all_jobs_metadata: list[JobMetadata]) -> list[str]: + result: list[str] = [] + for metadata in all_jobs_metadata: + if not re.match(METADATA_NAME_REGEX, metadata.name): + result.append( + f"Job name: {metadata.name} does not match the regex: {METADATA_NAME_REGEX}" + ) + result.extend(_validate_job_parameters(metadata.parameters)) + return result + + +def _validate_job_parameters(job_parameters: list[JobParameter]) -> list[str]: + result: list[str] = [ + f"Job Parameter name: {parameter.name} does not match the regex: {PARAM_NAME_REGEX}" + for parameter in job_parameters + if not re.match(PARAM_NAME_REGEX, parameter.name) + ] + return result + + +def _integration_metadata(integration_metadata: IntegrationMetadata) -> list[str]: + result: list[str] = [] + if not re.match(METADATA_NAME_REGEX, integration_metadata.name): + result.append( + f"Integration name: {integration_metadata.name} " + f"does not match the regex: {METADATA_NAME_REGEX}\n" + ) + result.extend(_integration_parameters(integration_metadata.parameters)) + + return result + + +def _integration_parameters(integration_parameters: list[IntegrationParameter]) -> list[str]: + result: list[str] = [ + f"Integration Parameter name: {parameter.name} does not match the regex: {PARAM_NAME_REGEX}" + for parameter in integration_parameters + if not re.match(PARAM_NAME_REGEX, parameter.name) + ] + return result diff --git a/packages/mp/tests/test_mp/test_validate/test_pre_build_validations/test_integrations/test_fields_validation.py b/packages/mp/tests/test_mp/test_validate/test_pre_build_validations/test_integrations/test_fields_validation.py new file mode 100644 index 000000000..fad135019 --- /dev/null +++ b/packages/mp/tests/test_mp/test_validate/test_pre_build_validations/test_integrations/test_fields_validation.py @@ -0,0 +1,193 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from pathlib import Path + +import pytest +import yaml + +from mp.core.exceptions import NonFatalValidationError +from mp.validate.pre_build_validation.integrations.fields_validation import ( + FieldsValidation, +) + + +def test_fields_validation_run_valid(temp_integration: Path) -> None: + FieldsValidation.run(temp_integration) + + +def test_fields_validation_invalid_integration_name(temp_integration: Path) -> None: + definition_path = temp_integration / "definition.yaml" + with Path(definition_path).open(encoding="utf-8") as f: + definition = yaml.safe_load(f) + + invalid_name = "Invalid Name!" + definition["name"] = invalid_name + + with Path(definition_path).open("w", encoding="utf-8") as f: + yaml.dump(definition, f) + + with pytest.raises(NonFatalValidationError) as excinfo: + FieldsValidation.run(temp_integration) + + assert "Integration name" in str(excinfo.value) + assert invalid_name in str(excinfo.value) + + +def test_fields_validation_invalid_action_name(temp_integration: Path) -> None: + action_path = temp_integration / "actions" / "ping.yaml" + with Path(action_path).open(encoding="utf-8") as f: + action = yaml.safe_load(f) + + invalid_name = "Ping_" + action["name"] = invalid_name + + with Path(action_path).open("w", encoding="utf-8") as f: + yaml.dump(action, f) + + with pytest.raises(NonFatalValidationError) as excinfo: + FieldsValidation.run(temp_integration) + + assert "Action name" in str(excinfo.value) + assert invalid_name in str(excinfo.value) + + +def test_fields_validation_invalid_action_parameter_name(temp_integration: Path) -> None: + action_path = temp_integration / "actions" / "ping.yaml" + with Path(action_path).open(encoding="utf-8") as f: + action = yaml.safe_load(f) + + invalid_name = "Host!" + action["parameters"][0]["name"] = invalid_name + + with Path(action_path).open("w", encoding="utf-8") as f: + yaml.dump(action, f) + + with pytest.raises(NonFatalValidationError) as excinfo: + FieldsValidation.run(temp_integration) + + assert "Action Parameter name" in str(excinfo.value) + assert invalid_name in str(excinfo.value) + + +def test_fields_validation_invalid_connector_name(temp_integration: Path) -> None: + connector_path = temp_integration / "connectors" / "connector.yaml" + with Path(connector_path).open(encoding="utf-8") as f: + connector = yaml.safe_load(f) + + invalid_name = "Mock_Connector" + connector["name"] = invalid_name + + with Path(connector_path).open("w", encoding="utf-8") as f: + yaml.dump(connector, f) + + with pytest.raises(NonFatalValidationError) as excinfo: + FieldsValidation.run(temp_integration) + + assert "Connector name" in str(excinfo.value) + assert invalid_name in str(excinfo.value) + + +def test_fields_validation_invalid_connector_parameter_name(temp_integration: Path) -> None: + connector_path = temp_integration / "connectors" / "connector.yaml" + with Path(connector_path).open(encoding="utf-8") as f: + connector = yaml.safe_load(f) + + invalid_name = "Param!" + connector["parameters"] = [ + { + "name": invalid_name, + "description": "A parameter with an invalid name", + "type": "string", + "is_mandatory": False, + "is_advanced": False, + "mode": "script", + "default_value": "", + } + ] + + with Path(connector_path).open("w", encoding="utf-8") as f: + yaml.dump(connector, f) + + with pytest.raises(NonFatalValidationError) as excinfo: + FieldsValidation.run(temp_integration) + + assert "Connector Parameter name" in str(excinfo.value) + assert invalid_name in str(excinfo.value) + + +def test_fields_validation_invalid_job_name(temp_integration: Path) -> None: + job_path = temp_integration / "jobs" / "job.yaml" + with Path(job_path).open(encoding="utf-8") as f: + job = yaml.safe_load(f) + + invalid_name = "Mock Job!" + job["name"] = invalid_name + + with Path(job_path).open("w", encoding="utf-8") as f: + yaml.dump(job, f) + + with pytest.raises(NonFatalValidationError) as excinfo: + FieldsValidation.run(temp_integration) + + assert "Job name" in str(excinfo.value) + assert invalid_name in str(excinfo.value) + + +def test_fields_validation_invalid_job_parameter_name(temp_integration: Path) -> None: + job_path = temp_integration / "jobs" / "job.yaml" + with Path(job_path).open(encoding="utf-8") as f: + job = yaml.safe_load(f) + + invalid_name = "Mock Job Parameter!" + job["parameters"][0]["name"] = invalid_name + + with Path(job_path).open("w", encoding="utf-8") as f: + yaml.dump(job, f) + + with pytest.raises(NonFatalValidationError) as excinfo: + FieldsValidation.run(temp_integration) + + assert "Job Parameter name" in str(excinfo.value) + assert invalid_name in str(excinfo.value) + + +def test_fields_validation_invalid_integration_parameter_name(temp_integration: Path) -> None: + definition_path = temp_integration / "definition.yaml" + with Path(definition_path).open(encoding="utf-8") as f: + definition = yaml.safe_load(f) + + invalid_name = "Invalid Param Name!" + if "parameters" not in definition: + definition["parameters"] = [] + + definition["parameters"].append({ + "name": invalid_name, + "description": "A parameter with an invalid name", + "type": "string", + "is_mandatory": False, + "default_value": "", + "integration_identifier": definition["identifier"], + }) + + with Path(definition_path).open("w", encoding="utf-8") as f: + yaml.dump(definition, f) + + with pytest.raises(NonFatalValidationError) as excinfo: + FieldsValidation.run(temp_integration) + + assert "Integration Parameter name" in str(excinfo.value) + assert invalid_name in str(excinfo.value) diff --git a/packages/mp/uv.lock b/packages/mp/uv.lock index fb359db54..80d6454c5 100644 --- a/packages/mp/uv.lock +++ b/packages/mp/uv.lock @@ -226,7 +226,7 @@ wheels = [ [[package]] name = "mp" -version = "1.25.6" +version = "1.25.7" source = { editable = "." } dependencies = [ { name = "deepdiff" }, @@ -608,27 +608,26 @@ wheels = [ [[package]] name = "ty" -version = "0.0.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/78/ba1a4ad403c748fbba8be63b7e774a90e80b67192f6443d624c64fe4aaab/ty-0.0.12.tar.gz", hash = "sha256:cd01810e106c3b652a01b8f784dd21741de9fdc47bd595d02c122a7d5cefeee7", size = 4981303, upload-time = "2026-01-14T22:30:48.537Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/8f/c21314d074dda5fb13d3300fa6733fd0d8ff23ea83a721818740665b6314/ty-0.0.12-py3-none-linux_armv6l.whl", hash = "sha256:eb9da1e2c68bd754e090eab39ed65edf95168d36cbeb43ff2bd9f86b4edd56d1", size = 9614164, upload-time = "2026-01-14T22:30:44.016Z" }, - { url = "https://files.pythonhosted.org/packages/09/28/f8a4d944d13519d70c486e8f96d6fa95647ac2aa94432e97d5cfec1f42f6/ty-0.0.12-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c181f42aa19b0ed7f1b0c2d559980b1f1d77cc09419f51c8321c7ddf67758853", size = 9542337, upload-time = "2026-01-14T22:30:05.687Z" }, - { url = "https://files.pythonhosted.org/packages/e1/9c/f576e360441de7a8201daa6dc4ebc362853bc5305e059cceeb02ebdd9a48/ty-0.0.12-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1f829e1eecd39c3e1b032149db7ae6a3284f72fc36b42436e65243a9ed1173db", size = 8909582, upload-time = "2026-01-14T22:30:46.089Z" }, - { url = "https://files.pythonhosted.org/packages/d6/13/0898e494032a5d8af3060733d12929e3e7716db6c75eac63fa125730a3e7/ty-0.0.12-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f45162e7826e1789cf3374627883cdeb0d56b82473a0771923e4572928e90be3", size = 9384932, upload-time = "2026-01-14T22:30:13.769Z" }, - { url = "https://files.pythonhosted.org/packages/e4/1a/b35b6c697008a11d4cedfd34d9672db2f0a0621ec80ece109e13fca4dfef/ty-0.0.12-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d11fec40b269bec01e751b2337d1c7ffa959a2c2090a950d7e21c2792442cccd", size = 9453140, upload-time = "2026-01-14T22:30:11.131Z" }, - { url = "https://files.pythonhosted.org/packages/dd/1e/71c9edbc79a3c88a0711324458f29c7dbf6c23452c6e760dc25725483064/ty-0.0.12-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09d99e37e761a4d2651ad9d5a610d11235fbcbf35dc6d4bc04abf54e7cf894f1", size = 9960680, upload-time = "2026-01-14T22:30:33.621Z" }, - { url = "https://files.pythonhosted.org/packages/0e/75/39375129f62dd22f6ad5a99cd2a42fd27d8b91b235ce2db86875cdad397d/ty-0.0.12-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d9ca0cdb17bd37397da7b16a7cd23423fc65c3f9691e453ad46c723d121225a1", size = 10904518, upload-time = "2026-01-14T22:30:08.464Z" }, - { url = "https://files.pythonhosted.org/packages/32/5e/26c6d88fafa11a9d31ca9f4d12989f57782ec61e7291d4802d685b5be118/ty-0.0.12-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcf2757b905e7eddb7e456140066335b18eb68b634a9f72d6f54a427ab042c64", size = 10525001, upload-time = "2026-01-14T22:30:16.454Z" }, - { url = "https://files.pythonhosted.org/packages/c2/a5/2f0b91894af13187110f9ad7ee926d86e4e6efa755c9c88a820ed7f84c85/ty-0.0.12-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00cf34c1ebe1147efeda3021a1064baa222c18cdac114b7b050bbe42deb4ca80", size = 10307103, upload-time = "2026-01-14T22:30:41.221Z" }, - { url = "https://files.pythonhosted.org/packages/4b/77/13d0410827e4bc713ebb7fdaf6b3590b37dcb1b82e0a81717b65548f2442/ty-0.0.12-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb3a655bd869352e9a22938d707631ac9fbca1016242b1f6d132d78f347c851", size = 10072737, upload-time = "2026-01-14T22:30:51.783Z" }, - { url = "https://files.pythonhosted.org/packages/e1/dd/fc36d8bac806c74cf04b4ca735bca14d19967ca84d88f31e121767880df1/ty-0.0.12-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4658e282c7cb82be304052f8f64f9925f23c3c4f90eeeb32663c74c4b095d7ba", size = 9368726, upload-time = "2026-01-14T22:30:18.683Z" }, - { url = "https://files.pythonhosted.org/packages/54/70/9e8e461647550f83e2fe54bc632ccbdc17a4909644783cdbdd17f7296059/ty-0.0.12-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c167d838eaaa06e03bb66a517f75296b643d950fbd93c1d1686a187e5a8dbd1f", size = 9454704, upload-time = "2026-01-14T22:30:22.759Z" }, - { url = "https://files.pythonhosted.org/packages/04/9b/6292cf7c14a0efeca0539cf7d78f453beff0475cb039fbea0eb5d07d343d/ty-0.0.12-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2956e0c9ab7023533b461d8a0e6b2ea7b78e01a8dde0688e8234d0fce10c4c1c", size = 9649829, upload-time = "2026-01-14T22:30:31.234Z" }, - { url = "https://files.pythonhosted.org/packages/49/bd/472a5d2013371e4870886cff791c94abdf0b92d43d305dd0f8e06b6ff719/ty-0.0.12-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5c6a3fd7479580009f21002f3828320621d8a82d53b7ba36993234e3ccad58c8", size = 10162814, upload-time = "2026-01-14T22:30:36.174Z" }, - { url = "https://files.pythonhosted.org/packages/31/e9/2ecbe56826759845a7c21d80aa28187865ea62bc9757b056f6cbc06f78ed/ty-0.0.12-py3-none-win32.whl", hash = "sha256:a91c24fd75c0f1796d8ede9083e2c0ec96f106dbda73a09fe3135e075d31f742", size = 9140115, upload-time = "2026-01-14T22:30:38.903Z" }, - { url = "https://files.pythonhosted.org/packages/5d/6d/d9531eff35a5c0ec9dbc10231fac21f9dd6504814048e81d6ce1c84dc566/ty-0.0.12-py3-none-win_amd64.whl", hash = "sha256:df151894be55c22d47068b0f3b484aff9e638761e2267e115d515fcc9c5b4a4b", size = 9884532, upload-time = "2026-01-14T22:30:25.112Z" }, - { url = "https://files.pythonhosted.org/packages/e9/f3/20b49e75967023b123a221134548ad7000f9429f13fdcdda115b4c26305f/ty-0.0.12-py3-none-win_arm64.whl", hash = "sha256:cea99d334b05629de937ce52f43278acf155d3a316ad6a35356635f886be20ea", size = 9313974, upload-time = "2026-01-14T22:30:27.44Z" }, +version = "0.0.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/dc/b607f00916f5a7c52860b84a66dc17bc6988e8445e96b1d6e175a3837397/ty-0.0.13.tar.gz", hash = "sha256:7a1d135a400ca076407ea30012d1f75419634160ed3b9cad96607bf2956b23b3", size = 4999183, upload-time = "2026-01-21T13:21:16.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/df/3632f1918f4c0a33184f107efc5d436ab6da147fd3d3b94b3af6461efbf4/ty-0.0.13-py3-none-linux_armv6l.whl", hash = "sha256:1b2b8e02697c3a94c722957d712a0615bcc317c9b9497be116ef746615d892f2", size = 9993501, upload-time = "2026-01-21T13:21:26.628Z" }, + { url = "https://files.pythonhosted.org/packages/92/87/6a473ced5ac280c6ce5b1627c71a8a695c64481b99aabc798718376a441e/ty-0.0.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f15cdb8e233e2b5adfce673bb21f4c5e8eaf3334842f7eea3c70ac6fda8c1de5", size = 9860986, upload-time = "2026-01-21T13:21:24.425Z" }, + { url = "https://files.pythonhosted.org/packages/5d/9b/d89ae375cf0a7cd9360e1164ce017f8c753759be63b6a11ed4c944abe8c6/ty-0.0.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:0819e89ac9f0d8af7a062837ce197f0461fee2fc14fd07e2c368780d3a397b73", size = 9350748, upload-time = "2026-01-21T13:21:28.502Z" }, + { url = "https://files.pythonhosted.org/packages/a8/a6/9ad58518056fab344b20c0bb2c1911936ebe195318e8acc3bc45ac1c6b6b/ty-0.0.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1de79f481084b7cc7a202ba0d7a75e10970d10ffa4f025b23f2e6b7324b74886", size = 9849884, upload-time = "2026-01-21T13:21:21.886Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c3/8add69095fa179f523d9e9afcc15a00818af0a37f2b237a9b59bc0046c34/ty-0.0.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4fb2154cff7c6e95d46bfaba283c60642616f20d73e5f96d0c89c269f3e1bcec", size = 9822975, upload-time = "2026-01-21T13:21:14.292Z" }, + { url = "https://files.pythonhosted.org/packages/a4/05/4c0927c68a0a6d43fb02f3f0b6c19c64e3461dc8ed6c404dde0efb8058f7/ty-0.0.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00be58d89337c27968a20d58ca553458608c5b634170e2bec82824c2e4cf4d96", size = 10294045, upload-time = "2026-01-21T13:21:30.505Z" }, + { url = "https://files.pythonhosted.org/packages/b4/86/6dc190838aba967557fe0bfd494c595d00b5081315a98aaf60c0e632aaeb/ty-0.0.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72435eade1fa58c6218abb4340f43a6c3ff856ae2dc5722a247d3a6dd32e9737", size = 10916460, upload-time = "2026-01-21T13:21:07.788Z" }, + { url = "https://files.pythonhosted.org/packages/04/40/9ead96b7c122e1109dfcd11671184c3506996bf6a649306ec427e81d9544/ty-0.0.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:77a548742ee8f621d718159e7027c3b555051d096a49bb580249a6c5fc86c271", size = 10597154, upload-time = "2026-01-21T13:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7d/e832a2c081d2be845dc6972d0c7998914d168ccbc0b9c86794419ab7376e/ty-0.0.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da067c57c289b7cf914669704b552b6207c2cc7f50da4118c3e12388642e6b3f", size = 10410710, upload-time = "2026-01-21T13:21:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/31/e3/898be3a96237a32f05c4c29b43594dc3b46e0eedfe8243058e46153b324f/ty-0.0.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d1b50a01fffa140417fca5a24b658fbe0734074a095d5b6f0552484724474343", size = 9826299, upload-time = "2026-01-21T13:21:00.845Z" }, + { url = "https://files.pythonhosted.org/packages/bb/eb/db2d852ce0ed742505ff18ee10d7d252f3acfd6fc60eca7e9c7a0288a6d8/ty-0.0.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0f33c46f52e5e9378378eca0d8059f026f3c8073ace02f7f2e8d079ddfe5207e", size = 9831610, upload-time = "2026-01-21T13:21:05.842Z" }, + { url = "https://files.pythonhosted.org/packages/9e/61/149f59c8abaddcbcbb0bd13b89c7741ae1c637823c5cf92ed2c644fcadef/ty-0.0.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:168eda24d9a0b202cf3758c2962cc295878842042b7eca9ed2965259f59ce9f2", size = 9978885, upload-time = "2026-01-21T13:21:10.306Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/026d4e4af60a80918a8d73d2c42b8262dd43ab2fa7b28d9743004cb88d57/ty-0.0.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d4917678b95dc8cb399cc459fab568ba8d5f0f33b7a94bf840d9733043c43f29", size = 10506453, upload-time = "2026-01-21T13:20:56.633Z" }, + { url = "https://files.pythonhosted.org/packages/63/06/8932833a4eca2df49c997a29afb26721612de8078ae79074c8fe87e17516/ty-0.0.13-py3-none-win32.whl", hash = "sha256:c1f2ec40daa405508b053e5b8e440fbae5fdb85c69c9ab0ee078f8bc00eeec3d", size = 9433482, upload-time = "2026-01-21T13:20:58.717Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fd/e8d972d1a69df25c2cecb20ea50e49ad5f27a06f55f1f5f399a563e71645/ty-0.0.13-py3-none-win_amd64.whl", hash = "sha256:8b7b1ab9f187affbceff89d51076038363b14113be29bda2ddfa17116de1d476", size = 10319156, upload-time = "2026-01-21T13:21:03.266Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c2/05fdd64ac003a560d4fbd1faa7d9a31d75df8f901675e5bed1ee2ceeff87/ty-0.0.13-py3-none-win_arm64.whl", hash = "sha256:1c9630333497c77bb9bcabba42971b96ee1f36c601dd3dcac66b4134f9fa38f0", size = 9808316, upload-time = "2026-01-21T13:20:54.053Z" }, ] [[package]]