validkit-py 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ *.pyc
2
+ .benchmarks/
3
+ .idea/
4
+ dist/
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.4
2
+ Name: validkit-py
3
+ Version: 1.0.0
4
+ Summary: A simple and powerful validation library for Python.
5
+ Project-URL: Homepage, https://github.com/disnana/ValidKit
6
+ Author: disnana
7
+ License: MIT
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+
15
+ # ValidKit
16
+
17
+ ValidKit は、辞書ベースで簡潔に定義でき、日本語キーや複雑なネストに対応した Python バリデーションライブラリです。 Pydantic よりも軽量かつ、Discord ボットの設定管理などの複雑なデータ構造を直感的に扱うことができます。
18
+
19
+ ## 特徴
20
+
21
+ - 📝 **辞書ベースのスキーマ**: クラス定義不要。データ構造をそのままスキーマとして定義可能
22
+ - 🌏 **日本語キー対応**: 日本語をキーにした設定データもそのまま検証
23
+ - 🔗 **チェイン可能なバリデータ**: `v.int().range(1, 10)` のように直感的に記述
24
+ - 🔄 **マイグレーション機能**: 古い形式のデータを検証と同時に新形式へ自動変換
25
+ - 🛠️ **部分更新・デフォルト値**: 足りないキーの補完や部分的な検証をサポート
26
+ - 🔍 **詳細なエラー収集**: 全エラーを一括で取得し、エラー箇所のパスを明確に表示
27
+
28
+ ## インストール
29
+
30
+ ```bash
31
+ pip install validkit
32
+ ```
33
+ *(注: 現時点ではプレリリース版です)*
34
+
35
+ ## クイックスタート
36
+
37
+ ```python
38
+ from validkit import v, validate
39
+
40
+ # スキーマ定義
41
+ SCHEMA = {
42
+ "ユーザー名": v.str().regex(r"^\w{3,15}$"),
43
+ "レベル": v.int().range(1, 100),
44
+ "スキル": v.list(v.oneof(["火", "水", "風"])),
45
+ "設定": {
46
+ "通知": v.bool(),
47
+ "言語": v.oneof(["日本語", "English"])
48
+ }
49
+ }
50
+
51
+ # 検証データ
52
+ data = {
53
+ "ユーザー名": "nana_kit",
54
+ "レベル": 50,
55
+ "スキル": ["火", "風"],
56
+ "設定": {
57
+ "通知": True,
58
+ "言語": "日本語"
59
+ }
60
+ }
61
+
62
+ try:
63
+ validated = validate(data, SCHEMA)
64
+ print("検証成功!")
65
+ except ValidationError as e:
66
+ print(f"エラー: {e.path} - {e.message}")
67
+ ```
68
+
69
+ ## 便利な高度な機能
70
+
71
+ ### 部分更新とデフォルト値のマージ
72
+ ```python
73
+ DEFAULT_CONFIG = {"言語": "English", "音量": 50}
74
+ partial_input = {"音量": 80}
75
+
76
+ updated = validate(
77
+ partial_input,
78
+ SCHEMA,
79
+ partial=True, # 不足キーを許可
80
+ base=DEFAULT_CONFIG # デフォルト値とマージ
81
+ )
82
+ ```
83
+
84
+ ### マイグレーション
85
+ ```python
86
+ old_data = {"旧キー": "値", "timeout": 30}
87
+ migrated = validate(old_data, SCHEMA, migrate={
88
+ "旧キー": "新キー",
89
+ "timeout": lambda v: f"{v}s" # 値の変換
90
+ })
91
+ ```
92
+
93
+ ## ライセンス
94
+
95
+ MIT
@@ -0,0 +1,81 @@
1
+ # ValidKit
2
+
3
+ ValidKit は、辞書ベースで簡潔に定義でき、日本語キーや複雑なネストに対応した Python バリデーションライブラリです。 Pydantic よりも軽量かつ、Discord ボットの設定管理などの複雑なデータ構造を直感的に扱うことができます。
4
+
5
+ ## 特徴
6
+
7
+ - 📝 **辞書ベースのスキーマ**: クラス定義不要。データ構造をそのままスキーマとして定義可能
8
+ - 🌏 **日本語キー対応**: 日本語をキーにした設定データもそのまま検証
9
+ - 🔗 **チェイン可能なバリデータ**: `v.int().range(1, 10)` のように直感的に記述
10
+ - 🔄 **マイグレーション機能**: 古い形式のデータを検証と同時に新形式へ自動変換
11
+ - 🛠️ **部分更新・デフォルト値**: 足りないキーの補完や部分的な検証をサポート
12
+ - 🔍 **詳細なエラー収集**: 全エラーを一括で取得し、エラー箇所のパスを明確に表示
13
+
14
+ ## インストール
15
+
16
+ ```bash
17
+ pip install validkit
18
+ ```
19
+ *(注: 現時点ではプレリリース版です)*
20
+
21
+ ## クイックスタート
22
+
23
+ ```python
24
+ from validkit import v, validate
25
+
26
+ # スキーマ定義
27
+ SCHEMA = {
28
+ "ユーザー名": v.str().regex(r"^\w{3,15}$"),
29
+ "レベル": v.int().range(1, 100),
30
+ "スキル": v.list(v.oneof(["火", "水", "風"])),
31
+ "設定": {
32
+ "通知": v.bool(),
33
+ "言語": v.oneof(["日本語", "English"])
34
+ }
35
+ }
36
+
37
+ # 検証データ
38
+ data = {
39
+ "ユーザー名": "nana_kit",
40
+ "レベル": 50,
41
+ "スキル": ["火", "風"],
42
+ "設定": {
43
+ "通知": True,
44
+ "言語": "日本語"
45
+ }
46
+ }
47
+
48
+ try:
49
+ validated = validate(data, SCHEMA)
50
+ print("検証成功!")
51
+ except ValidationError as e:
52
+ print(f"エラー: {e.path} - {e.message}")
53
+ ```
54
+
55
+ ## 便利な高度な機能
56
+
57
+ ### 部分更新とデフォルト値のマージ
58
+ ```python
59
+ DEFAULT_CONFIG = {"言語": "English", "音量": 50}
60
+ partial_input = {"音量": 80}
61
+
62
+ updated = validate(
63
+ partial_input,
64
+ SCHEMA,
65
+ partial=True, # 不足キーを許可
66
+ base=DEFAULT_CONFIG # デフォルト値とマージ
67
+ )
68
+ ```
69
+
70
+ ### マイグレーション
71
+ ```python
72
+ old_data = {"旧キー": "値", "timeout": 30}
73
+ migrated = validate(old_data, SCHEMA, migrate={
74
+ "旧キー": "新キー",
75
+ "timeout": lambda v: f"{v}s" # 値の変換
76
+ })
77
+ ```
78
+
79
+ ## ライセンス
80
+
81
+ MIT
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "validkit-py"
7
+ dynamic = ["version"]
8
+ description = "A simple and powerful validation library for Python."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "disnana" },
14
+ ]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Operating System :: OS Independent",
19
+ "Topic :: Software Development :: Libraries :: Python Modules",
20
+ ]
21
+ dependencies = []
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/disnana/ValidKit"
25
+
26
+ [tool.hatch.version]
27
+ path = "src/validkit/__init__.py"
28
+
29
+ [tool.hatch.build.targets.sdist]
30
+ include = ["/src", "/README.md", "/LICENSE*"]
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["src/validkit"]
@@ -0,0 +1,5 @@
1
+ from .v import v
2
+ from .validator import validate, ValidationError
3
+
4
+ __version__ = "1.0.0"
5
+ __all__ = ["v", "validate", "ValidationError"]
@@ -0,0 +1,156 @@
1
+ import re
2
+ import builtins
3
+ from typing import Any, Callable, Dict, List, Union, Type, Optional, cast
4
+
5
+ class Validator:
6
+ def __init__(self) -> None:
7
+ self._optional = False
8
+ self._custom_checks: List[Callable[[Any], Any]] = []
9
+ self._when_condition: Optional[Callable[[Dict[str, Any]], bool]] = None
10
+
11
+ def optional(self) -> "Validator":
12
+ self._optional = True
13
+ return self
14
+
15
+ def custom(self, func: Callable[[Any], Any]) -> "Validator":
16
+ self._custom_checks.append(func)
17
+ return self
18
+
19
+ def when(self, condition: Callable[[Dict[str, Any]], bool]) -> "Validator":
20
+ self._when_condition = condition
21
+ return self
22
+
23
+ def validate(self, value: Any, data: Optional[Dict[str, Any]] = None) -> Any:
24
+ """Base validate method. Subclasses should override this."""
25
+ return self._validate_base(value, data)
26
+
27
+ def _validate_base(self, value: Any, data: Optional[Dict[str, Any]] = None) -> Any:
28
+ # if condition is not met, this validator might be skipped or handled by caller
29
+ for check in self._custom_checks:
30
+ value = check(value)
31
+ return value
32
+
33
+ class StringValidator(Validator):
34
+ def __init__(self) -> None:
35
+ super().__init__()
36
+ self._regex: Optional[re.Pattern[str]] = None
37
+
38
+ def regex(self, pattern: str) -> "StringValidator":
39
+ self._regex = re.compile(pattern)
40
+ return self
41
+
42
+ def validate(self, value: Any, data: Optional[Dict[str, Any]] = None) -> str:
43
+ if not isinstance(value, str):
44
+ raise TypeError(f"Expected str, got {type(value).__name__}")
45
+ if self._regex and not self._regex.match(value):
46
+ raise ValueError(f"Value '{value}' does not match regex '{self._regex.pattern}'")
47
+ return cast(str, self._validate_base(value, data))
48
+
49
+ class NumberValidator(Validator):
50
+ def __init__(self, type_cls: Union[Type[int], Type[float]]) -> None:
51
+ super().__init__()
52
+ self._type_cls = type_cls
53
+ self._min: Optional[float] = None
54
+ self._max: Optional[float] = None
55
+
56
+ def range(self, min_val: float, max_val: float) -> "NumberValidator":
57
+ self._min = min_val
58
+ self._max = max_val
59
+ return self
60
+
61
+ def min(self, min_val: float) -> "NumberValidator":
62
+ self._min = min_val
63
+ return self
64
+
65
+ def max(self, max_val: float) -> "NumberValidator":
66
+ self._max = max_val
67
+ return self
68
+
69
+ def validate(self, value: Any, data: Optional[Dict[str, Any]] = None) -> Union[int, float]:
70
+ if not isinstance(value, self._type_cls):
71
+ raise TypeError(f"Expected {self._type_cls.__name__}, got {type(value).__name__}")
72
+ if self._min is not None and value < self._min:
73
+ raise ValueError(f"Value {value} is less than minimum {self._min}")
74
+ if self._max is not None and value > self._max:
75
+ raise ValueError(f"Value {value} is greater than maximum {self._max}")
76
+ return cast(Union[int, float], self._validate_base(value, data))
77
+
78
+ class BoolValidator(Validator):
79
+ def validate(self, value: Any, data: Optional[Dict[str, Any]] = None) -> bool:
80
+ if not isinstance(value, bool):
81
+ raise TypeError(f"Expected bool, got {type(value).__name__}")
82
+ return cast(bool, self._validate_base(value, data))
83
+
84
+ class ListValidator(Validator):
85
+ def __init__(self, item_validator: Union[Validator, Dict[builtins.str, Any], Type[Any]]) -> None:
86
+ super().__init__()
87
+ self._item_validator = item_validator
88
+
89
+ def validate(self, value: Any, data: Optional[Dict[str, Any]] = None) -> List[Any]:
90
+ if not isinstance(value, (list, tuple)):
91
+ raise TypeError(f"Expected list, got {type(value).__name__}")
92
+ from .validator import validate_internal
93
+ result = []
94
+ root_data = data if data is not None else {}
95
+ for i, item in enumerate(value):
96
+ try:
97
+ result.append(validate_internal(item, self._item_validator, root_data, path_prefix=f"[{i}]"))
98
+ except Exception:
99
+ # Re-raise or collect will be handled by validate_internal/caller
100
+ raise
101
+ return cast(List[Any], self._validate_base(result, data))
102
+
103
+ class DictValidator(Validator):
104
+ def __init__(self, key_type: Type[Any], value_validator: Union[Validator, Dict[str, Any], Type[Any]]) -> None:
105
+ super().__init__()
106
+ self._key_type = key_type
107
+ self._value_validator = value_validator
108
+
109
+ def validate(self, value: Any, data: Optional[Dict[str, Any]] = None) -> Dict[Any, Any]:
110
+ if not isinstance(value, dict):
111
+ raise TypeError(f"Expected dict, got {type(value).__name__}")
112
+ from .validator import validate_internal
113
+ result = {}
114
+ root_data = data if data is not None else {}
115
+ for k, v in value.items():
116
+ if not isinstance(k, self._key_type):
117
+ raise TypeError(f"Expected key type {self._key_type.__name__}, got {type(k).__name__}")
118
+ try:
119
+ result[k] = validate_internal(v, self._value_validator, root_data, path_prefix=f"{k}")
120
+ except Exception:
121
+ raise
122
+ return cast(Dict[Any, Any], self._validate_base(result, data))
123
+
124
+ class OneOfValidator(Validator):
125
+ def __init__(self, choices: List[Any]) -> None:
126
+ super().__init__()
127
+ self._choices = choices
128
+
129
+ def validate(self, value: Any, data: Optional[Dict[str, Any]] = None) -> Any:
130
+ if value not in self._choices:
131
+ raise ValueError(f"Value '{value}' is not one of {self._choices}")
132
+ return self._validate_base(value, data)
133
+
134
+ class VBuilder:
135
+ def str(self) -> StringValidator:
136
+ return StringValidator()
137
+
138
+ def int(self) -> NumberValidator:
139
+ return NumberValidator(int)
140
+
141
+ def float(self) -> NumberValidator:
142
+ return NumberValidator(float)
143
+
144
+ def bool(self) -> BoolValidator:
145
+ return BoolValidator()
146
+
147
+ def list(self, item_validator: Union[Validator, Dict[builtins.str, Any], Type[Any]]) -> ListValidator:
148
+ return ListValidator(item_validator)
149
+
150
+ def dict(self, key_type: Type[Any], value_validator: Union[Validator, Dict[builtins.str, Any], Type[Any]]) -> DictValidator:
151
+ return DictValidator(key_type, value_validator)
152
+
153
+ def oneof(self, choices: List[Any]) -> OneOfValidator:
154
+ return OneOfValidator(choices)
155
+
156
+ v = VBuilder()
@@ -0,0 +1,178 @@
1
+ from typing import Any, Dict, List, Union, Optional
2
+ from .v import Validator, v
3
+
4
+ class ValidationError(Exception):
5
+ def __init__(self, message: str, path: str = "", value: Any = None) -> None:
6
+ self.message = message
7
+ self.path = path
8
+ self.value = value
9
+ super().__init__(f"{path}: {message}" if path else message)
10
+
11
+ class ErrorDetail:
12
+ def __init__(self, path: str, message: str, value: Any) -> None:
13
+ self.path = path
14
+ self.message = message
15
+ self.value = value
16
+
17
+ def __str__(self) -> str:
18
+ return f"{self.path}: {self.message} (value: {self.value})"
19
+
20
+ class ValidationResult:
21
+ def __init__(self, data: Any, errors: Optional[List[ErrorDetail]] = None) -> None:
22
+ self.data = data
23
+ self.errors = errors or []
24
+
25
+ def validate_internal(
26
+ value: Any,
27
+ schema: Any,
28
+ root_data: Dict[str, Any],
29
+ path_prefix: str = "",
30
+ partial: bool = False,
31
+ base: Any = None,
32
+ collect_errors: bool = False,
33
+ errors: Optional[List[ErrorDetail]] = None
34
+ ) -> Any:
35
+ # 1. Shorthand types
36
+ if isinstance(schema, type) and schema in (str, int, float, bool):
37
+ if schema is str:
38
+ schema = v.str()
39
+ elif schema is int:
40
+ schema = v.int()
41
+ elif schema is float:
42
+ schema = v.float()
43
+ elif schema is bool:
44
+ schema = v.bool()
45
+
46
+ # 2. Validator objects
47
+ if isinstance(schema, Validator):
48
+ # Allow None if optional
49
+ if value is None and schema._optional:
50
+ return base if base is not None else None
51
+
52
+ # Check condition if any
53
+ if schema._when_condition and not schema._when_condition(root_data):
54
+ # If condition not met and we have a base value, use it, else return None (or skip)
55
+ return base
56
+
57
+ try:
58
+ return schema.validate(value, root_data)
59
+ except (TypeError, ValueError) as e:
60
+ err_msg = str(e)
61
+ if collect_errors and errors is not None:
62
+ errors.append(ErrorDetail(path_prefix, err_msg, value))
63
+ return value
64
+ raise ValidationError(err_msg, path_prefix, value)
65
+
66
+ # 3. Dict schemas
67
+ if isinstance(schema, dict):
68
+ if value is not None and not isinstance(value, dict):
69
+ err_msg = f"Expected dict, got {type(value).__name__}"
70
+ if collect_errors and errors is not None:
71
+ errors.append(ErrorDetail(path_prefix, err_msg, value))
72
+ return value
73
+ raise ValidationError(err_msg, path_prefix, value)
74
+
75
+ result = {}
76
+ input_dict = value if value is not None else {}
77
+ base_dict = base if isinstance(base, dict) else {}
78
+
79
+ # All keys in schema
80
+ if not partial:
81
+ # Check for missing keys
82
+ pass # We'll check individually
83
+
84
+ for key, sub_schema in schema.items():
85
+ current_path = f"{path_prefix}.{key}" if path_prefix else key
86
+ val = input_dict.get(key)
87
+ sub_base = base_dict.get(key)
88
+
89
+ is_optional = False
90
+ if isinstance(sub_schema, Validator) and sub_schema._optional:
91
+ is_optional = True
92
+
93
+ if key not in input_dict:
94
+ if sub_base is not None:
95
+ # Use base value
96
+ result[key] = sub_base
97
+ continue
98
+
99
+ # Check condition for requirement
100
+ if isinstance(sub_schema, Validator) and sub_schema._when_condition:
101
+ if not sub_schema._when_condition(root_data):
102
+ # Condition not met, not required
103
+ continue
104
+
105
+ if is_optional or partial:
106
+ # Skip or keep as None
107
+ if is_optional and sub_base is not None:
108
+ result[key] = sub_base
109
+ continue
110
+ else:
111
+ err_msg = "Missing required key"
112
+ if collect_errors and errors is not None:
113
+ errors.append(ErrorDetail(current_path, err_msg, None))
114
+ continue
115
+ raise ValidationError(err_msg, current_path, None)
116
+
117
+ # Key exists in input
118
+ try:
119
+ result[key] = validate_internal(
120
+ val, sub_schema, root_data, current_path,
121
+ partial, sub_base, collect_errors, errors
122
+ )
123
+ except ValidationError:
124
+ if collect_errors:
125
+ continue
126
+ raise
127
+
128
+ # Check for unknown keys? (Optional feature, not requested but good for strictness)
129
+ # For now, we only process keys in schema.
130
+ return result
131
+
132
+ # 4. Literal / Pre-validated?
133
+ return value
134
+
135
+ def validate(
136
+ data: Any,
137
+ schema: Any,
138
+ partial: bool = False,
139
+ base: Any = None,
140
+ migrate: Optional[Dict[str, Any]] = None,
141
+ collect_errors: bool = False
142
+ ) -> Union[Any, ValidationResult]:
143
+
144
+ # Apply migration if any
145
+ if migrate and isinstance(data, dict):
146
+ data = data.copy()
147
+ for old_key, action in migrate.items():
148
+ if old_key in data:
149
+ val = data.pop(old_key)
150
+ if isinstance(action, str):
151
+ data[action] = val
152
+ elif callable(action):
153
+ # We need to decide where the result goes.
154
+ # If migrate says {"timeout": lambda v: f"{v}s"}, it modifies the value but keeps the key?
155
+ # Actual request: "timeout": lambda v: f"{v}s" -> value transformation.
156
+ # "旧キー名": "新キー名" -> key rename.
157
+ # Let's assume if it's a string, it's a rename. If it's a callable, it's a value transform of the SAME key (or should it be rename + transform?)
158
+ # Usually migration means "old format to new format".
159
+ # Let's support both.
160
+ data[old_key] = action(val)
161
+ # Note: if it's a rename, we might want to transform too.
162
+ # But the prompt example shows them separately.
163
+
164
+ errors: List[ErrorDetail] = []
165
+ try:
166
+ validated_data = validate_internal(
167
+ data, schema, root_data=data,
168
+ partial=partial, base=base,
169
+ collect_errors=collect_errors, errors=errors
170
+ )
171
+ except ValidationError:
172
+ if not collect_errors:
173
+ raise
174
+ validated_data = data # fallback
175
+
176
+ if collect_errors:
177
+ return ValidationResult(validated_data, errors)
178
+ return validated_data