kiarina-utils-common 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.
- kiarina_utils_common-1.0.0/.gitignore +39 -0
- kiarina_utils_common-1.0.0/CHANGELOG.md +39 -0
- kiarina_utils_common-1.0.0/PKG-INFO +144 -0
- kiarina_utils_common-1.0.0/README.md +118 -0
- kiarina_utils_common-1.0.0/pyproject.toml +41 -0
- kiarina_utils_common-1.0.0/src/kiarina/utils/common/__init__.py +10 -0
- kiarina_utils_common-1.0.0/src/kiarina/utils/common/_parse_config_string.py +216 -0
- kiarina_utils_common-1.0.0/src/kiarina/utils/common/py.typed +0 -0
- kiarina_utils_common-1.0.0/tests/test_parse_config_string.py +296 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.so
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.ruff_cache/
|
|
9
|
+
.mypy_cache/
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.coverage
|
|
12
|
+
coverage.xml
|
|
13
|
+
htmlcov/
|
|
14
|
+
|
|
15
|
+
# uv
|
|
16
|
+
.uv_cache/
|
|
17
|
+
|
|
18
|
+
# Virtual environments & config
|
|
19
|
+
.venv/
|
|
20
|
+
.env
|
|
21
|
+
|
|
22
|
+
# IDE
|
|
23
|
+
.vscode/
|
|
24
|
+
*.code-workspace
|
|
25
|
+
|
|
26
|
+
# OS
|
|
27
|
+
.DS_Store
|
|
28
|
+
|
|
29
|
+
# Project specific
|
|
30
|
+
*.log
|
|
31
|
+
tmp/
|
|
32
|
+
ai.yaml
|
|
33
|
+
|
|
34
|
+
# Test data
|
|
35
|
+
tests/data/large/
|
|
36
|
+
|
|
37
|
+
# mise tasks (always include)
|
|
38
|
+
!mise-tasks/
|
|
39
|
+
!mise-tasks/**
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the kiarina-utils-common package will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.0.0] - 2025-09-09
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Comprehensive README.md with usage examples and API documentation
|
|
14
|
+
- Enhanced pyproject.toml with proper metadata, classifiers, and project URLs
|
|
15
|
+
- CHANGELOG.md for tracking version changes
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Improved package documentation and metadata
|
|
19
|
+
|
|
20
|
+
## [0.1.0] - 2025-01-09
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- Initial release of kiarina-utils-common
|
|
24
|
+
- `parse_config_string` function for parsing configuration strings
|
|
25
|
+
- Support for nested keys using dot notation
|
|
26
|
+
- Support for array indices in configuration strings
|
|
27
|
+
- Automatic type conversion (bool, int, float, str)
|
|
28
|
+
- Flag support (keys without values)
|
|
29
|
+
- Customizable separators for different parsing needs
|
|
30
|
+
- Comprehensive test suite with pytest
|
|
31
|
+
- Type hints and py.typed marker for full typing support
|
|
32
|
+
|
|
33
|
+
### Features
|
|
34
|
+
- Parse configuration strings like `"cache.enabled:true,db.port:5432"`
|
|
35
|
+
- Support for nested structures: `{"cache": {"enabled": True}, "db": {"port": 5432}}`
|
|
36
|
+
- Array index support: `"items.0:first,items.1:second"` → `{"items": ["first", "second"]}`
|
|
37
|
+
- Flag functionality: `"debug,verbose"` → `{"debug": None, "verbose": None}`
|
|
38
|
+
- Custom separators: configurable item, key-value, and nested separators
|
|
39
|
+
- Automatic type detection and conversion for common data types
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kiarina-utils-common
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Common utility functions for the kiarina namespace packages
|
|
5
|
+
Project-URL: Homepage, https://github.com/kiarina/kiarina-python
|
|
6
|
+
Project-URL: Repository, https://github.com/kiarina/kiarina-python
|
|
7
|
+
Project-URL: Issues, https://github.com/kiarina/kiarina-python/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/kiarina/kiarina-python/blob/main/packages/kiarina-utils-common/CHANGELOG.md
|
|
9
|
+
Project-URL: Documentation, https://github.com/kiarina/kiarina-python/tree/main/packages/kiarina-utils-common#readme
|
|
10
|
+
Author-email: kiarina <kiarinadawa@gmail.com>
|
|
11
|
+
Maintainer-email: kiarina <kiarinadawa@gmail.com>
|
|
12
|
+
License: MIT
|
|
13
|
+
Keywords: common,config,parser,utilities
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: >=3.12
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# kiarina-utils-common
|
|
28
|
+
|
|
29
|
+
[](https://badge.fury.io/py/kiarina-utils-common)
|
|
30
|
+
[](https://pypi.org/project/kiarina-utils-common/)
|
|
31
|
+
[](https://opensource.org/licenses/MIT)
|
|
32
|
+
|
|
33
|
+
Common utility functions for the kiarina namespace packages.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install kiarina-utils-common
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
### Configuration String Parser
|
|
44
|
+
|
|
45
|
+
Parse configuration strings into nested dictionaries with automatic type conversion.
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from kiarina.utils.common import parse_config_string
|
|
49
|
+
|
|
50
|
+
# Basic usage
|
|
51
|
+
config = parse_config_string("cache.enabled:true,db.port:5432")
|
|
52
|
+
# Result: {"cache": {"enabled": True}, "db": {"port": 5432}}
|
|
53
|
+
|
|
54
|
+
# Flag support (no value)
|
|
55
|
+
config = parse_config_string("debug,verbose,cache.enabled:true")
|
|
56
|
+
# Result: {"debug": None, "verbose": None, "cache": {"enabled": True}}
|
|
57
|
+
|
|
58
|
+
# Array indices support
|
|
59
|
+
config = parse_config_string("items.0:first,items.1:second")
|
|
60
|
+
# Result: {"items": ["first", "second"]}
|
|
61
|
+
|
|
62
|
+
# Custom separators
|
|
63
|
+
config = parse_config_string(
|
|
64
|
+
"key1=val1;key2.sub=42",
|
|
65
|
+
separator=";",
|
|
66
|
+
key_value_separator="="
|
|
67
|
+
)
|
|
68
|
+
# Result: {"key1": "val1", "key2": {"sub": 42}}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
#### Type Conversion
|
|
72
|
+
|
|
73
|
+
Values are automatically converted to appropriate types:
|
|
74
|
+
|
|
75
|
+
- `"true"`, `"True"` → `bool(True)`
|
|
76
|
+
- `"false"`, `"False"` → `bool(False)`
|
|
77
|
+
- Numeric strings (`"1"`, `"0"`, `"-5"`, `"3.14"`) → `int` or `float`
|
|
78
|
+
- Other strings → `str`
|
|
79
|
+
|
|
80
|
+
#### Nested Keys
|
|
81
|
+
|
|
82
|
+
Use dot notation for nested structures:
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
config = parse_config_string("database.host:localhost,database.port:5432")
|
|
86
|
+
# Result: {"database": {"host": "localhost", "port": 5432}}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### Array Indices
|
|
90
|
+
|
|
91
|
+
Use numeric keys for array structures:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
config = parse_config_string("users.0.name:Alice,users.0.age:30,users.1.name:Bob")
|
|
95
|
+
# Result: {"users": [{"name": "Alice", "age": 30}, {"name": "Bob"}]}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## API Reference
|
|
99
|
+
|
|
100
|
+
### `parse_config_string(config_str, *, separator=",", key_value_separator=":", nested_separator=".")`
|
|
101
|
+
|
|
102
|
+
Parse configuration string into nested dictionary.
|
|
103
|
+
|
|
104
|
+
**Parameters:**
|
|
105
|
+
- `config_str` (str): Configuration string to parse
|
|
106
|
+
- `separator` (str, optional): Item separator. Default: `","`
|
|
107
|
+
- `key_value_separator` (str, optional): Key-value separator. Default: `":"`
|
|
108
|
+
- `nested_separator` (str, optional): Nested key separator. Default: `"."`
|
|
109
|
+
|
|
110
|
+
**Returns:**
|
|
111
|
+
- `dict[str, Any]`: Parsed configuration dictionary
|
|
112
|
+
|
|
113
|
+
**Examples:**
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
# Basic usage
|
|
117
|
+
parse_config_string("key1:value1,key2:value2")
|
|
118
|
+
# {"key1": "value1", "key2": "value2"}
|
|
119
|
+
|
|
120
|
+
# Nested keys
|
|
121
|
+
parse_config_string("cache.enabled:true,db.port:5432")
|
|
122
|
+
# {"cache": {"enabled": True}, "db": {"port": 5432}}
|
|
123
|
+
|
|
124
|
+
# Flags (no value)
|
|
125
|
+
parse_config_string("debug,verbose")
|
|
126
|
+
# {"debug": None, "verbose": None}
|
|
127
|
+
|
|
128
|
+
# Custom separators
|
|
129
|
+
parse_config_string("a=1;b=2", separator=";", key_value_separator="=")
|
|
130
|
+
# {"a": 1, "b": 2}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
This project is licensed under the MIT License - see the [LICENSE](../../LICENSE) file for details.
|
|
136
|
+
|
|
137
|
+
## Contributing
|
|
138
|
+
|
|
139
|
+
This is a personal project by kiarina. While issues and pull requests are welcome, please note that this is primarily developed for personal use.
|
|
140
|
+
|
|
141
|
+
## Related Packages
|
|
142
|
+
|
|
143
|
+
- [kiarina-utils-file](../kiarina-utils-file/): File operation utilities
|
|
144
|
+
- [kiarina-llm](../kiarina-llm/): LLM-related utilities
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# kiarina-utils-common
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/py/kiarina-utils-common)
|
|
4
|
+
[](https://pypi.org/project/kiarina-utils-common/)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
|
|
7
|
+
Common utility functions for the kiarina namespace packages.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install kiarina-utils-common
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
### Configuration String Parser
|
|
18
|
+
|
|
19
|
+
Parse configuration strings into nested dictionaries with automatic type conversion.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from kiarina.utils.common import parse_config_string
|
|
23
|
+
|
|
24
|
+
# Basic usage
|
|
25
|
+
config = parse_config_string("cache.enabled:true,db.port:5432")
|
|
26
|
+
# Result: {"cache": {"enabled": True}, "db": {"port": 5432}}
|
|
27
|
+
|
|
28
|
+
# Flag support (no value)
|
|
29
|
+
config = parse_config_string("debug,verbose,cache.enabled:true")
|
|
30
|
+
# Result: {"debug": None, "verbose": None, "cache": {"enabled": True}}
|
|
31
|
+
|
|
32
|
+
# Array indices support
|
|
33
|
+
config = parse_config_string("items.0:first,items.1:second")
|
|
34
|
+
# Result: {"items": ["first", "second"]}
|
|
35
|
+
|
|
36
|
+
# Custom separators
|
|
37
|
+
config = parse_config_string(
|
|
38
|
+
"key1=val1;key2.sub=42",
|
|
39
|
+
separator=";",
|
|
40
|
+
key_value_separator="="
|
|
41
|
+
)
|
|
42
|
+
# Result: {"key1": "val1", "key2": {"sub": 42}}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
#### Type Conversion
|
|
46
|
+
|
|
47
|
+
Values are automatically converted to appropriate types:
|
|
48
|
+
|
|
49
|
+
- `"true"`, `"True"` → `bool(True)`
|
|
50
|
+
- `"false"`, `"False"` → `bool(False)`
|
|
51
|
+
- Numeric strings (`"1"`, `"0"`, `"-5"`, `"3.14"`) → `int` or `float`
|
|
52
|
+
- Other strings → `str`
|
|
53
|
+
|
|
54
|
+
#### Nested Keys
|
|
55
|
+
|
|
56
|
+
Use dot notation for nested structures:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
config = parse_config_string("database.host:localhost,database.port:5432")
|
|
60
|
+
# Result: {"database": {"host": "localhost", "port": 5432}}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
#### Array Indices
|
|
64
|
+
|
|
65
|
+
Use numeric keys for array structures:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
config = parse_config_string("users.0.name:Alice,users.0.age:30,users.1.name:Bob")
|
|
69
|
+
# Result: {"users": [{"name": "Alice", "age": 30}, {"name": "Bob"}]}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## API Reference
|
|
73
|
+
|
|
74
|
+
### `parse_config_string(config_str, *, separator=",", key_value_separator=":", nested_separator=".")`
|
|
75
|
+
|
|
76
|
+
Parse configuration string into nested dictionary.
|
|
77
|
+
|
|
78
|
+
**Parameters:**
|
|
79
|
+
- `config_str` (str): Configuration string to parse
|
|
80
|
+
- `separator` (str, optional): Item separator. Default: `","`
|
|
81
|
+
- `key_value_separator` (str, optional): Key-value separator. Default: `":"`
|
|
82
|
+
- `nested_separator` (str, optional): Nested key separator. Default: `"."`
|
|
83
|
+
|
|
84
|
+
**Returns:**
|
|
85
|
+
- `dict[str, Any]`: Parsed configuration dictionary
|
|
86
|
+
|
|
87
|
+
**Examples:**
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
# Basic usage
|
|
91
|
+
parse_config_string("key1:value1,key2:value2")
|
|
92
|
+
# {"key1": "value1", "key2": "value2"}
|
|
93
|
+
|
|
94
|
+
# Nested keys
|
|
95
|
+
parse_config_string("cache.enabled:true,db.port:5432")
|
|
96
|
+
# {"cache": {"enabled": True}, "db": {"port": 5432}}
|
|
97
|
+
|
|
98
|
+
# Flags (no value)
|
|
99
|
+
parse_config_string("debug,verbose")
|
|
100
|
+
# {"debug": None, "verbose": None}
|
|
101
|
+
|
|
102
|
+
# Custom separators
|
|
103
|
+
parse_config_string("a=1;b=2", separator=";", key_value_separator="=")
|
|
104
|
+
# {"a": 1, "b": 2}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
This project is licensed under the MIT License - see the [LICENSE](../../LICENSE) file for details.
|
|
110
|
+
|
|
111
|
+
## Contributing
|
|
112
|
+
|
|
113
|
+
This is a personal project by kiarina. While issues and pull requests are welcome, please note that this is primarily developed for personal use.
|
|
114
|
+
|
|
115
|
+
## Related Packages
|
|
116
|
+
|
|
117
|
+
- [kiarina-utils-file](../kiarina-utils-file/): File operation utilities
|
|
118
|
+
- [kiarina-llm](../kiarina-llm/): LLM-related utilities
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "kiarina-utils-common"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Common utility functions for the kiarina namespace packages"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "kiarina", email = "kiarinadawa@gmail.com" }
|
|
9
|
+
]
|
|
10
|
+
maintainers = [
|
|
11
|
+
{ name = "kiarina", email = "kiarinadawa@gmail.com" }
|
|
12
|
+
]
|
|
13
|
+
keywords = ["utilities", "config", "parser", "common"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
|
+
"Topic :: Utilities",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
requires-python = ">=3.12"
|
|
27
|
+
dependencies = []
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/kiarina/kiarina-python"
|
|
31
|
+
Repository = "https://github.com/kiarina/kiarina-python"
|
|
32
|
+
Issues = "https://github.com/kiarina/kiarina-python/issues"
|
|
33
|
+
Changelog = "https://github.com/kiarina/kiarina-python/blob/main/packages/kiarina-utils-common/CHANGELOG.md"
|
|
34
|
+
Documentation = "https://github.com/kiarina/kiarina-python/tree/main/packages/kiarina-utils-common#readme"
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["hatchling"]
|
|
38
|
+
build-backend = "hatchling.build"
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/kiarina"]
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def parse_config_string(
|
|
5
|
+
config_str: str,
|
|
6
|
+
*,
|
|
7
|
+
separator: str = ",",
|
|
8
|
+
key_value_separator: str = ":",
|
|
9
|
+
nested_separator: str = ".",
|
|
10
|
+
) -> dict[str, Any]:
|
|
11
|
+
"""
|
|
12
|
+
Parse configuration string into nested dictionary
|
|
13
|
+
|
|
14
|
+
When key contains nested_separator, it will be treated as nested keys:
|
|
15
|
+
- hoge.fuga:30 → {"hoge": {"fuga": 30}}
|
|
16
|
+
|
|
17
|
+
When key_value_separator is not found, the key is treated as a flag with None value:
|
|
18
|
+
- debug,enabled:true → {"debug": None, "enabled": True}
|
|
19
|
+
|
|
20
|
+
Values are automatically converted to appropriate types:
|
|
21
|
+
- "true", "True" → bool(True)
|
|
22
|
+
- "false", "False" → bool(False)
|
|
23
|
+
- Numeric strings ("1", "0", "-5", etc.) → int/float
|
|
24
|
+
- Others → str
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
config_str: Configuration string to parse
|
|
28
|
+
separator: Item separator (default: ",")
|
|
29
|
+
key_value_separator: Key-value separator (default: ":")
|
|
30
|
+
nested_separator: Nested key separator (default: ".")
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Parsed configuration dictionary
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
>>> parse_config_string("cache.enabled:true,db.port:5432")
|
|
37
|
+
{"cache": {"enabled": True}, "db": {"port": 5432}}
|
|
38
|
+
|
|
39
|
+
>>> parse_config_string("debug,verbose,cache.enabled:true")
|
|
40
|
+
{"debug": None, "verbose": None, "cache": {"enabled": True}}
|
|
41
|
+
|
|
42
|
+
>>> parse_config_string("key1=val1;key2.sub=42", separator=";", key_value_separator="=")
|
|
43
|
+
{"key1": "val1", "key2": {"sub": 42}}
|
|
44
|
+
"""
|
|
45
|
+
if not config_str:
|
|
46
|
+
return {}
|
|
47
|
+
|
|
48
|
+
result: dict[str, Any] = {}
|
|
49
|
+
|
|
50
|
+
for option in config_str.split(separator):
|
|
51
|
+
option = option.strip()
|
|
52
|
+
|
|
53
|
+
if not option:
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
if key_value_separator in option:
|
|
57
|
+
key, value = option.split(key_value_separator, 1)
|
|
58
|
+
key = key.strip()
|
|
59
|
+
value = value.strip()
|
|
60
|
+
|
|
61
|
+
converted_value = _convert_value(value)
|
|
62
|
+
|
|
63
|
+
# Handle nested keys
|
|
64
|
+
_set_nested_value(result, key, converted_value, nested_separator)
|
|
65
|
+
else:
|
|
66
|
+
# No key_value_separator found, treat as flag with None value
|
|
67
|
+
_set_nested_value(result, option, None, nested_separator)
|
|
68
|
+
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _convert_value(value: str) -> Any:
|
|
73
|
+
"""
|
|
74
|
+
Convert string value to appropriate type
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
value: String value to convert
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Converted value
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
# Boolean values (only true and false, 1 and 0 are treated as numbers)
|
|
84
|
+
if value.lower() == "true":
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
elif value.lower() == "false":
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
# Integer case
|
|
91
|
+
elif value.lstrip("-").isdigit():
|
|
92
|
+
return int(value)
|
|
93
|
+
|
|
94
|
+
# Floating point case
|
|
95
|
+
else:
|
|
96
|
+
try:
|
|
97
|
+
float_value = float(value)
|
|
98
|
+
|
|
99
|
+
# Don't convert to integer if it can be represented as integer (respect original value)
|
|
100
|
+
if "." in value:
|
|
101
|
+
return float_value
|
|
102
|
+
else:
|
|
103
|
+
# If integer format but not caught above, treat as string
|
|
104
|
+
return value
|
|
105
|
+
|
|
106
|
+
except ValueError:
|
|
107
|
+
# String case
|
|
108
|
+
return value
|
|
109
|
+
|
|
110
|
+
except (ValueError, TypeError):
|
|
111
|
+
# If conversion fails, save as string
|
|
112
|
+
return value
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _set_nested_value(
|
|
116
|
+
target: dict[str, Any], key: str, value: Any, separator: str = "."
|
|
117
|
+
) -> None:
|
|
118
|
+
"""
|
|
119
|
+
Set value with nested keys (supports array indices)
|
|
120
|
+
|
|
121
|
+
When a key is a numeric string, it's treated as an array index:
|
|
122
|
+
- items.0:foo → {"items": ["foo"]}
|
|
123
|
+
- users.0.name:Alice → {"users": [{"name": "Alice"}]}
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
target: Target dictionary to set the value
|
|
127
|
+
key: Key (supports dot notation and array indices)
|
|
128
|
+
value: Value to set
|
|
129
|
+
separator: Nested key separator (default: ".")
|
|
130
|
+
"""
|
|
131
|
+
keys = key.split(separator)
|
|
132
|
+
|
|
133
|
+
current: dict[str, Any] | list[Any] = target
|
|
134
|
+
|
|
135
|
+
# Navigate/create nested structure up to the last key
|
|
136
|
+
for i, k in enumerate(keys[:-1]):
|
|
137
|
+
next_key = keys[i + 1]
|
|
138
|
+
|
|
139
|
+
if _is_array_index(k):
|
|
140
|
+
index = int(k)
|
|
141
|
+
|
|
142
|
+
if not isinstance(current, list):
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f"Cannot access array index {index} on non-array value"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
while len(current) <= index:
|
|
148
|
+
current.append(None)
|
|
149
|
+
|
|
150
|
+
if _is_array_index(next_key):
|
|
151
|
+
if current[index] is None:
|
|
152
|
+
current[index] = []
|
|
153
|
+
elif not isinstance(current[index], list):
|
|
154
|
+
current[index] = []
|
|
155
|
+
|
|
156
|
+
else:
|
|
157
|
+
if current[index] is None:
|
|
158
|
+
current[index] = {}
|
|
159
|
+
elif not isinstance(current[index], dict):
|
|
160
|
+
current[index] = {}
|
|
161
|
+
|
|
162
|
+
current = current[index]
|
|
163
|
+
|
|
164
|
+
else:
|
|
165
|
+
if not isinstance(current, dict):
|
|
166
|
+
raise ValueError(f"Cannot access key '{k}' on non-dict value")
|
|
167
|
+
|
|
168
|
+
if _is_array_index(next_key):
|
|
169
|
+
if k not in current:
|
|
170
|
+
current[k] = []
|
|
171
|
+
elif not isinstance(current[k], list):
|
|
172
|
+
current[k] = []
|
|
173
|
+
|
|
174
|
+
current = current[k]
|
|
175
|
+
|
|
176
|
+
else:
|
|
177
|
+
if k not in current:
|
|
178
|
+
current[k] = {}
|
|
179
|
+
elif not isinstance(current[k], dict):
|
|
180
|
+
current[k] = {}
|
|
181
|
+
|
|
182
|
+
current = current[k]
|
|
183
|
+
|
|
184
|
+
# Set the value for the last key
|
|
185
|
+
last_key = keys[-1]
|
|
186
|
+
|
|
187
|
+
if _is_array_index(last_key):
|
|
188
|
+
index = int(last_key)
|
|
189
|
+
|
|
190
|
+
if not isinstance(current, list):
|
|
191
|
+
raise ValueError(f"Cannot set array index {index} on non-array value")
|
|
192
|
+
|
|
193
|
+
# Extend list if necessary (fill with None)
|
|
194
|
+
while len(current) <= index:
|
|
195
|
+
current.append(None)
|
|
196
|
+
|
|
197
|
+
current[index] = value
|
|
198
|
+
|
|
199
|
+
else:
|
|
200
|
+
if not isinstance(current, dict):
|
|
201
|
+
raise ValueError(f"Cannot set key '{last_key}' on non-dict value")
|
|
202
|
+
|
|
203
|
+
current[last_key] = value
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _is_array_index(key: str) -> bool:
|
|
207
|
+
"""
|
|
208
|
+
Check if a key represents an array index
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
key: Key to check
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
True if key is a non-negative integer string
|
|
215
|
+
"""
|
|
216
|
+
return key.isdigit()
|
|
File without changes
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from kiarina.utils.common import parse_config_string
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_parse_config_string_empty():
|
|
7
|
+
"""Test empty configuration string"""
|
|
8
|
+
result = parse_config_string("")
|
|
9
|
+
assert result == {}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.parametrize(
|
|
13
|
+
"config_str,expected",
|
|
14
|
+
[
|
|
15
|
+
# Basic key-value pairs
|
|
16
|
+
("key1:value1,key2:value2", {"key1": "value1", "key2": "value2"}),
|
|
17
|
+
("single:option", {"single": "option"}),
|
|
18
|
+
# Flag functionality (no key_value_separator)
|
|
19
|
+
("debug", {"debug": None}),
|
|
20
|
+
("debug,verbose", {"debug": None, "verbose": None}),
|
|
21
|
+
("debug,enabled:true", {"debug": None, "enabled": True}),
|
|
22
|
+
(
|
|
23
|
+
"debug,verbose,cache.enabled:true",
|
|
24
|
+
{"debug": None, "verbose": None, "cache": {"enabled": True}},
|
|
25
|
+
),
|
|
26
|
+
# Type conversion
|
|
27
|
+
("bool_true:true", {"bool_true": True}),
|
|
28
|
+
("bool_false:false", {"bool_false": False}),
|
|
29
|
+
("bool_True:True", {"bool_True": True}),
|
|
30
|
+
("bool_False:False", {"bool_False": False}),
|
|
31
|
+
("int_pos:123", {"int_pos": 123}),
|
|
32
|
+
("int_neg:-456", {"int_neg": -456}),
|
|
33
|
+
("int_zero:0", {"int_zero": 0}),
|
|
34
|
+
("float_pos:3.14", {"float_pos": 3.14}),
|
|
35
|
+
("float_neg:-2.5", {"float_neg": -2.5}),
|
|
36
|
+
("str_val:hello", {"str_val": "hello"}),
|
|
37
|
+
# Mixed types
|
|
38
|
+
(
|
|
39
|
+
"bool:true,int:42,float:3.14,str:hello",
|
|
40
|
+
{"bool": True, "int": 42, "float": 3.14, "str": "hello"},
|
|
41
|
+
),
|
|
42
|
+
],
|
|
43
|
+
)
|
|
44
|
+
def test_parse_config_string_basic(config_str, expected):
|
|
45
|
+
"""Test basic configuration string parsing and type conversion"""
|
|
46
|
+
result = parse_config_string(config_str)
|
|
47
|
+
assert result == expected
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.mark.parametrize(
|
|
51
|
+
"config_str,expected",
|
|
52
|
+
[
|
|
53
|
+
# Simple nesting
|
|
54
|
+
("cache.enabled:true", {"cache": {"enabled": True}}),
|
|
55
|
+
("db.port:5432", {"db": {"port": 5432}}),
|
|
56
|
+
# Nested flags
|
|
57
|
+
("app.debug", {"app": {"debug": None}}),
|
|
58
|
+
("app.debug,app.verbose", {"app": {"debug": None, "verbose": None}}),
|
|
59
|
+
("app.debug,app.port:8080", {"app": {"debug": None, "port": 8080}}),
|
|
60
|
+
# Multiple nested keys
|
|
61
|
+
(
|
|
62
|
+
"cache.enabled:true,cache.ttl:3600",
|
|
63
|
+
{"cache": {"enabled": True, "ttl": 3600}},
|
|
64
|
+
),
|
|
65
|
+
# Different sections
|
|
66
|
+
(
|
|
67
|
+
"cache.enabled:true,db.host:localhost,db.port:5432",
|
|
68
|
+
{"cache": {"enabled": True}, "db": {"host": "localhost", "port": 5432}},
|
|
69
|
+
),
|
|
70
|
+
# Deep nesting
|
|
71
|
+
("a.b.c.d:value1", {"a": {"b": {"c": {"d": "value1"}}}}),
|
|
72
|
+
# Mixed depth nesting with flags
|
|
73
|
+
(
|
|
74
|
+
"a.b.c.d:value1,a.b.e:value2,a.f:value3,a.b.flag",
|
|
75
|
+
{
|
|
76
|
+
"a": {
|
|
77
|
+
"b": {"c": {"d": "value1"}, "e": "value2", "flag": None},
|
|
78
|
+
"f": "value3",
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
),
|
|
82
|
+
],
|
|
83
|
+
)
|
|
84
|
+
def test_parse_config_string_nested(config_str, expected):
|
|
85
|
+
"""Test nested key parsing with dot notation"""
|
|
86
|
+
result = parse_config_string(config_str)
|
|
87
|
+
assert result == expected
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@pytest.mark.parametrize(
|
|
91
|
+
"config_str,expected",
|
|
92
|
+
[
|
|
93
|
+
# Whitespace around options
|
|
94
|
+
(" key1:value1 , key2:value2 ", {"key1": "value1", "key2": "value2"}),
|
|
95
|
+
# Whitespace around key-value pairs
|
|
96
|
+
("key1 : value1", {"key1": "value1"}),
|
|
97
|
+
# Whitespace with nested keys
|
|
98
|
+
(" key1.nested : 42 ", {"key1": {"nested": 42}}),
|
|
99
|
+
# Mixed whitespace
|
|
100
|
+
(
|
|
101
|
+
" cache.enabled : true , db.port : 5432 ",
|
|
102
|
+
{"cache": {"enabled": True}, "db": {"port": 5432}},
|
|
103
|
+
),
|
|
104
|
+
],
|
|
105
|
+
)
|
|
106
|
+
def test_parse_config_string_whitespace(config_str, expected):
|
|
107
|
+
"""Test whitespace handling in keys and values"""
|
|
108
|
+
result = parse_config_string(config_str)
|
|
109
|
+
assert result == expected
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@pytest.mark.parametrize(
|
|
113
|
+
"config_str,separator,key_value_separator,nested_separator,expected",
|
|
114
|
+
[
|
|
115
|
+
# Custom item separator
|
|
116
|
+
(
|
|
117
|
+
"key1=value1;key2=value2",
|
|
118
|
+
";",
|
|
119
|
+
"=",
|
|
120
|
+
".",
|
|
121
|
+
{"key1": "value1", "key2": "value2"},
|
|
122
|
+
),
|
|
123
|
+
# Custom key-value separator with nesting
|
|
124
|
+
(
|
|
125
|
+
"key1=value1;key2.sub=42",
|
|
126
|
+
";",
|
|
127
|
+
"=",
|
|
128
|
+
".",
|
|
129
|
+
{"key1": "value1", "key2": {"sub": 42}},
|
|
130
|
+
),
|
|
131
|
+
# Custom nested separator
|
|
132
|
+
(
|
|
133
|
+
"key1/sub:value1,key2/nested/deep:42",
|
|
134
|
+
",",
|
|
135
|
+
":",
|
|
136
|
+
"/",
|
|
137
|
+
{"key1": {"sub": "value1"}, "key2": {"nested": {"deep": 42}}},
|
|
138
|
+
),
|
|
139
|
+
# All custom separators
|
|
140
|
+
(
|
|
141
|
+
"app|debug=true;app|port=8080",
|
|
142
|
+
";",
|
|
143
|
+
"=",
|
|
144
|
+
"|",
|
|
145
|
+
{"app": {"debug": True, "port": 8080}},
|
|
146
|
+
),
|
|
147
|
+
],
|
|
148
|
+
)
|
|
149
|
+
def test_parse_config_string_custom_separators(
|
|
150
|
+
config_str, separator, key_value_separator, nested_separator, expected
|
|
151
|
+
):
|
|
152
|
+
"""Test custom separators"""
|
|
153
|
+
result = parse_config_string(
|
|
154
|
+
config_str,
|
|
155
|
+
separator=separator,
|
|
156
|
+
key_value_separator=key_value_separator,
|
|
157
|
+
nested_separator=nested_separator,
|
|
158
|
+
)
|
|
159
|
+
assert result == expected
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@pytest.mark.parametrize(
|
|
163
|
+
"config_str,expected",
|
|
164
|
+
[
|
|
165
|
+
# Options without key-value separator are now treated as flags
|
|
166
|
+
("debug,valid:value", {"debug": None, "valid": "value"}),
|
|
167
|
+
("flag1,key1:value1,flag2", {"flag1": None, "key1": "value1", "flag2": None}),
|
|
168
|
+
# Empty options (should be ignored)
|
|
169
|
+
("key1:value1,,key2:value2", {"key1": "value1", "key2": "value2"}),
|
|
170
|
+
# Mixed flags and values with empty options
|
|
171
|
+
("debug,,verbose,port:8080,", {"debug": None, "verbose": None, "port": 8080}),
|
|
172
|
+
# Whitespace handling with flags
|
|
173
|
+
(
|
|
174
|
+
" debug , verbose , port : 8080 ",
|
|
175
|
+
{"debug": None, "verbose": None, "port": 8080},
|
|
176
|
+
),
|
|
177
|
+
],
|
|
178
|
+
)
|
|
179
|
+
def test_parse_config_string_edge_cases(config_str, expected):
|
|
180
|
+
"""Test edge cases and error handling"""
|
|
181
|
+
result = parse_config_string(config_str)
|
|
182
|
+
assert result == expected
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@pytest.mark.parametrize(
|
|
186
|
+
"config_str,expected",
|
|
187
|
+
[
|
|
188
|
+
# Simple array
|
|
189
|
+
("items.0:foo,items.1:bar,items.2:baz", {"items": ["foo", "bar", "baz"]}),
|
|
190
|
+
("tags.0:python,tags.1:rust", {"tags": ["python", "rust"]}),
|
|
191
|
+
# Array with type conversion
|
|
192
|
+
("numbers.0:1,numbers.1:2,numbers.2:3", {"numbers": [1, 2, 3]}),
|
|
193
|
+
("mixed.0:1,mixed.1:true,mixed.2:hello", {"mixed": [1, True, "hello"]}),
|
|
194
|
+
# Sparse array (with None gaps)
|
|
195
|
+
("sparse.0:first,sparse.2:third", {"sparse": ["first", None, "third"]}),
|
|
196
|
+
# Nested structure with arrays
|
|
197
|
+
(
|
|
198
|
+
"users.0.name:Alice,users.0.age:30,users.1.name:Bob,users.1.age:25",
|
|
199
|
+
{"users": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]},
|
|
200
|
+
),
|
|
201
|
+
# Mixed arrays and regular keys
|
|
202
|
+
(
|
|
203
|
+
"app.name:myapp,app.tags.0:web,app.tags.1:api,app.debug:true",
|
|
204
|
+
{"app": {"name": "myapp", "tags": ["web", "api"], "debug": True}},
|
|
205
|
+
),
|
|
206
|
+
# Array in nested structure
|
|
207
|
+
(
|
|
208
|
+
"db.servers.0:localhost,db.servers.1:192.168.1.1,db.port:5432",
|
|
209
|
+
{"db": {"servers": ["localhost", "192.168.1.1"], "port": 5432}},
|
|
210
|
+
),
|
|
211
|
+
# Deep nesting with arrays
|
|
212
|
+
(
|
|
213
|
+
"config.db.hosts.0:primary,config.db.hosts.1:secondary",
|
|
214
|
+
{"config": {"db": {"hosts": ["primary", "secondary"]}}},
|
|
215
|
+
),
|
|
216
|
+
],
|
|
217
|
+
)
|
|
218
|
+
def test_parse_config_string_arrays(config_str, expected):
|
|
219
|
+
"""Test array support with index notation"""
|
|
220
|
+
result = parse_config_string(config_str)
|
|
221
|
+
assert result == expected
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@pytest.mark.parametrize(
|
|
225
|
+
"config_str,expected",
|
|
226
|
+
[
|
|
227
|
+
# Array flags (no values)
|
|
228
|
+
("items.0,items.1:value", {"items": [None, "value"]}),
|
|
229
|
+
("flags.0,flags.1,flags.2", {"flags": [None, None, None]}),
|
|
230
|
+
# Mixed flags and arrays
|
|
231
|
+
(
|
|
232
|
+
"debug,items.0:first,verbose,items.1:second",
|
|
233
|
+
{"debug": None, "items": ["first", "second"], "verbose": None},
|
|
234
|
+
),
|
|
235
|
+
],
|
|
236
|
+
)
|
|
237
|
+
def test_parse_config_string_array_flags(config_str, expected):
|
|
238
|
+
"""Test array support with flag notation"""
|
|
239
|
+
result = parse_config_string(config_str)
|
|
240
|
+
assert result == expected
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_parse_config_string_array_edge_cases():
|
|
244
|
+
"""Test edge cases for array functionality"""
|
|
245
|
+
# Single element array
|
|
246
|
+
result = parse_config_string("items.0:single")
|
|
247
|
+
assert result == {"items": ["single"]}
|
|
248
|
+
|
|
249
|
+
# Large index (creates sparse array)
|
|
250
|
+
result = parse_config_string("items.10:value")
|
|
251
|
+
expected_items = [None] * 10 + ["value"]
|
|
252
|
+
assert result == {"items": expected_items}
|
|
253
|
+
|
|
254
|
+
# Zero index
|
|
255
|
+
result = parse_config_string("items.0:zero")
|
|
256
|
+
assert result == {"items": ["zero"]}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def test_parse_config_string_array_type_conflicts():
|
|
260
|
+
"""Test handling of type conflicts with arrays"""
|
|
261
|
+
# When dict key comes first, then array index - dict gets converted to array
|
|
262
|
+
result = parse_config_string("items.key:value,items.0:first")
|
|
263
|
+
# The dict gets converted to array, losing the original key
|
|
264
|
+
assert result == {"items": ["first"]}
|
|
265
|
+
|
|
266
|
+
# When array index comes first, then dict key - array gets converted to dict
|
|
267
|
+
result = parse_config_string("items.0:first,items.key:value")
|
|
268
|
+
# The array gets converted to dict, preserving both values
|
|
269
|
+
assert result == {"items": {"key": "value"}}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def test_parse_config_string_real_world_example():
|
|
273
|
+
"""Test real-world application configuration example"""
|
|
274
|
+
config_str = "app.debug:true,app.port:8080,app.name:myapp,db.host:localhost,db.port:5432,db.timeout:30.5,cache.enabled:true,cache.ttl:3600"
|
|
275
|
+
|
|
276
|
+
expected = {
|
|
277
|
+
"app": {"debug": True, "port": 8080, "name": "myapp"},
|
|
278
|
+
"db": {"host": "localhost", "port": 5432, "timeout": 30.5},
|
|
279
|
+
"cache": {"enabled": True, "ttl": 3600},
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
result = parse_config_string(config_str)
|
|
283
|
+
assert result == expected
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def test_parse_config_string_real_world_with_arrays():
|
|
287
|
+
"""Test real-world configuration with arrays"""
|
|
288
|
+
config_str = "app.name:myapp,app.tags.0:web,app.tags.1:api,db.servers.0:primary,db.servers.1:secondary,db.ports.0:5432,db.ports.1:5433"
|
|
289
|
+
|
|
290
|
+
expected = {
|
|
291
|
+
"app": {"name": "myapp", "tags": ["web", "api"]},
|
|
292
|
+
"db": {"servers": ["primary", "secondary"], "ports": [5432, 5433]},
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
result = parse_config_string(config_str)
|
|
296
|
+
assert result == expected
|