Skip to content

Commit cd1275e

Browse files
authored
Merge pull request #7 from codezaur/pre-release
pre release
2 parents 30a5b5f + 852e8d0 commit cd1275e

37 files changed

+676
-430
lines changed

.pre-commit-config.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
repos:
2+
- repo: https://github.com/pycqa/isort
3+
rev: 5.13.2
4+
hooks:
5+
- id: isort
6+
args: ["--profile", "black"]
7+
- repo: https://github.com/psf/black
8+
rev: 24.8.0
9+
hooks:
10+
- id: black

MANIFEST.in

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
include README.md
2-
include LICENSE
2+
include LICENSE
3+
exclude **/test_*.py

README.md

Lines changed: 54 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
# case-boss
22

3+
[![PyPI version](https://img.shields.io/pypi/v/case-boss.svg)](https://pypi.org/project/case-boss/)
4+
[![Python versions](https://img.shields.io/pypi/pyversions/case-boss.svg)](https://pypi.org/project/case-boss/)
5+
[![License](https://img.shields.io/github/license/codezaur/case-boss.svg)](https://github.com/codezaur/case-boss/blob/main/LICENSE)
6+
37
A Python package for string case conversion and manipulation, featuring a flexible CLI and extensible core.
48

59
## Features
610

7-
- Convert strings between camelCase, snake_case, kebab-case, PascalCase, and more
11+
- Convert dictionary/JSON keys between camelCase, snake_case, kebab-case, PascalCase, and more
812
- Easy to use cli (command-line interface)
913

1014
## Installation
@@ -17,9 +21,9 @@ pip install case-boss
1721

1822
### ⌨️ CLI (Command Line Interface)
1923

20-
```bash
21-
# basic commands
24+
#### basic commands
2225

26+
```bash
2327
case-boss -v # show version
2428
case-boss --help # show options and usage
2529
case-boss cases # show available target case types
@@ -29,43 +33,64 @@ case-boss cases # show available target case types
2933
case-boss transform path-to/file.json
3034
```
3135

36+
#### options for transform command
3237
```bash
33-
# options for transform command
34-
3538
# passing path and selected target case type
3639
case-boss transform path-to/file.json --to pascal
40+
```
3741

42+
```bash
3843
# passing path and name of result file (will save to file instead of stdout)
3944
case-boss transform path-to/file.json -o result.json
4045
case-boss transform path-to/file.json --output result.json
4146
case-boss transform path-to/file.json > result.json
47+
```
4248

49+
```bash
4350
# modifying file inplace instead of creating new one or printing to stdout
4451
case-boss transform path-to/file.json --inplace
4552
case-boss transform path-to/file.json -i
53+
```
4654

47-
# using standart input (stdin), passing '-' as the source and piping JSON data.
55+
```bash
56+
# using standard input (stdin), passing '-' as the source and piping JSON data.
4857
echo '{"youShallNotPass": "ok"}' | case-boss transform - --to pascal
58+
# output: {"YouShallNotPass": "ok"}
59+
```
4960

50-
# passing --json directly instad of path to file
61+
```bash
62+
# passing --json directly instead of path to file
5163
case-boss transform --json '{"youShallNotPass": "ok"}' --to pascal
64+
```
5265

66+
```bash
5367
# printing transformation time in seconds
5468
case-boss transform path-to/file.json --benchmark
5569
case-boss transform path-to/file.json -b
70+
```
5671

72+
```bash
5773
# preserving acronyms or custom words (e.g., keep 'ID' or 'HTTP' uppercase):
5874
case-boss transform path-to/file.json --preserve ID,HTTP
75+
```
5976

60-
# rich example; passing path, selected target case type, name of result file and benchamark
61-
case-boss transform path-to/file.json --to pascal --output result.json --benchmark --preserve ID,SQL
62-
77+
```bash
6378
# limiting recursion depth (e.g., only transform top-level keys)
6479
case-boss transform path-to/file.json --limit 1
80+
```
6581

82+
```bash
6683
# excluding specific keys from transformation (stopping recursion on those keys)
6784
case-boss transform path-to/file.json --exclude someKey,anotherKey
85+
```
86+
87+
```bash
88+
# rich example; passing path, selected target case type, name of result file and benchmark
89+
case-boss transform path-to/file.json --to pascal --output result.json --benchmark --preserve ID,SQL
90+
```
6891

92+
```bash
93+
# help
6994
case-boss transform --help # show options and usage for the transform command
7095
```
7196

@@ -82,33 +107,41 @@ case-boss transform --help # show options and usage for the transform command
82107
### 🐍 Python API
83108

84109
```python
85-
from case-boss import CaseBoss
110+
from case_boss import CaseBoss
86111

87112
boss = CaseBoss()
113+
```
88114

89-
# Basic usage
115+
```python
116+
# Basic usage
90117
result = boss.transform(source=my_dict, case="camel")
91-
print(result)
118+
# assuming my_dict = { "you_shall_not_pass": True }, result will be: { "youShallNotPass": True }
119+
```
92120

121+
```python
93122
# Clone mode: return a new dict, leaving the original untouched
94123
result = boss.transform(source=my_dict, case="camel", clone=True)
95-
print(result)
124+
```
96125

126+
```python
97127
# Preserving acronyms or custom words (e.g., keep 'ID' or 'HTTP' uppercase):
98128
result = boss.transform(source=my_dict, case="camel", preserve_tokens=["ID", "HTTP"])
99-
print(result)
129+
# assuming my_dict = { "user_ID": 1, "http_status": "ok" }, result will be: { "userID": 1, "HTTPStatus": "ok" }
130+
```
100131

132+
```python
101133
# Limiting recursion depth (e.g., '1' only transform top-level keys):
102134
result = boss.transform(source=my_dict, case="camel", recursion_limit=1)
103-
print(result)
135+
```
104136

137+
```python
105138
# Excluding specific keys from transformation (stopping recursion on those keys):
106139
result = boss.transform(source=my_dict, case="camel", exclude_keys=["metaData", "anotherKey"])
107-
print(result)
140+
```
108141

109-
# For JSON strings:
142+
```python
143+
# For JSON strings (also returns JSON):
110144
json_result = boss.transform_from_json(source=my_json_str, case="camel")
111-
print(json_result)
112145
```
113146

114147

@@ -126,7 +159,7 @@ The `--limit` CLI option and `recursion_limit` Python argument set the maximum r
126159

127160
#### About `exclude_keys`
128161

129-
The `--exclude` CLI option and `exclude_keys` Python argument allow you to specify a comma-separated list (CLI) or list of strings (Python) of keys to skip entirely during transformation, including stopping recursion on those keys and their nested values.
162+
The `--exclude` CLI option (comma-separated list) and `exclude_keys` Python argument (list of strings) let you specify keys to skip entirely during transformation, halting recursion on those keys and their nested values.
130163

131164
## Supported Case Types
132165

@@ -145,7 +178,7 @@ The following case types are supported:
145178
## Contributing
146179

147180
1. Fork the repository
148-
2. Create your feature branch (`git checkout -b feature/YourFeature`)
181+
2. Create your feature branch (`git checkout -b feature/your-feature`)
149182
3. Commit your changes
150183
4. Push to the branch
151184
5. Open a pull request
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
from .case_boss import CaseBoss
22

3-
__version__ = "0.1.0"
3+
__all__ = ["CaseBoss"]

case_boss/abstract/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .case_converter import CaseConverter
Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,42 @@
44

55
class CaseConverter(ABC):
66
def __init__(
7-
self,
8-
preserve_tokens: List[str] | None = None,
9-
exclude_keys: List[str] | None = None,
10-
recursion_limit: int = 0,
11-
):
12-
self._preserve_tokens = { p.upper() for p in (preserve_tokens or []) }
13-
self._exclude_keys = { p for p in (exclude_keys or []) }
7+
self,
8+
preserve_tokens: List[str] | None = None,
9+
exclude_keys: List[str] | None = None,
10+
recursion_limit: int = 0,
11+
):
12+
self._preserve_tokens = {p.upper() for p in (preserve_tokens or [])}
13+
self._exclude_keys = {p for p in (exclude_keys or [])}
1414
self._recursion_limit = recursion_limit
1515

16-
1716
def convert(
1817
self,
1918
source: Dict[Hashable, Any],
20-
current_recursion_depth = 1,
19+
current_recursion_depth=1,
2120
) -> None:
2221
items = list(source.items())
2322
source.clear()
2423

2524
for key, value in items:
2625
should_skip = key in self._exclude_keys
27-
should_convert = (not should_skip and isinstance(key, str))
28-
has_reached_recursion_limit = current_recursion_depth == self._recursion_limit
26+
should_convert = not should_skip and isinstance(key, str)
27+
has_reached_recursion_limit = (
28+
current_recursion_depth == self._recursion_limit
29+
)
2930
should_go_deeper = (
30-
not should_skip
31+
not should_skip
3132
and isinstance(value, dict)
3233
and (self._recursion_limit == 0 or not has_reached_recursion_limit)
3334
)
34-
35+
3536
new_key = self._convert_key(key) if should_convert else key
3637
if should_go_deeper:
37-
self.convert(source=value, current_recursion_depth=(current_recursion_depth+1))
38+
self.convert(
39+
source=value, current_recursion_depth=(current_recursion_depth + 1)
40+
)
3841
source[new_key] = value
3942

40-
4143
@abstractmethod
4244
def _convert_key(self, key: str) -> str:
4345
pass
Lines changed: 40 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,15 @@
22
import json
33
from typing import Any, Dict, Hashable, List, Type
44

5-
from core.const import CASE_TYPE_CONVERTER_MAPPING
6-
from core.errors import ERROR_UNKNOWN_CASE_TYPE, ERROR_INVALID_JSON
7-
from core.abstract.case_converter import CaseConverter
8-
from core.types import CaseType
9-
from core.utils import normalize_type, validate_is_dict
5+
from case_boss.abstract.case_converter import CaseConverter
6+
from case_boss.const import CASE_TYPE_CONVERTER_MAPPING
7+
from case_boss.errors import ERROR_INVALID_JSON, ERROR_UNKNOWN_CASE_TYPE
8+
from case_boss.types import CaseType
9+
from case_boss.utils import normalize_type, validate_is_dict
1010

1111

1212
class CaseBoss:
1313

14-
1514
def transform(
1615
self,
1716
source: Dict[Hashable, Any],
@@ -27,9 +26,9 @@ def transform(
2726
2827
Args:
2928
source (dict): The data to process.
30-
type (CaseType): Target key case format (e.g., CaseType.SNAKE, CaseType.CAMEL)
29+
case (CaseType): Target key case format (e.g., CaseType.SNAKE, CaseType.CAMEL)
3130
clone (bool): Will return clone, leaving original object untouched (defaults to False)
32-
preserve_tokens (List[str]): List of preservable strings, eg. acronyms like HTTP, ID
31+
preserve_tokens (List[str]): List of preservable strings, e.g., acronyms like HTTP, ID
3332
exclude_keys (List[str]): Keys to skip (together with their children); excluded keys are not transformed and recursion does not descend into their values.
3433
recursion_limit: (int): How deep will recursion go for nested dicts, defaults to 0 (no limit),
3534
@@ -47,33 +46,40 @@ def transform(
4746
if clone:
4847
source = copy.deepcopy(source)
4948

50-
converter_cls: Type[CaseConverter] = CASE_TYPE_CONVERTER_MAPPING.get(case.value, None)
49+
converter_cls: Type[CaseConverter] = CASE_TYPE_CONVERTER_MAPPING.get(
50+
case.value, None
51+
)
5152
if not converter_cls:
52-
raise ValueError(ERROR_UNKNOWN_CASE_TYPE.format(type_=type(case).__name__, allowed=[t.value for t in CaseType]))
53+
raise ValueError(
54+
ERROR_UNKNOWN_CASE_TYPE.format(
55+
type_=type(case).__name__, allowed=[t.value for t in CaseType]
56+
)
57+
)
5358

5459
converter: CaseConverter = converter_cls(
55-
preserve_tokens=preserve_tokens,
60+
preserve_tokens=preserve_tokens,
5661
exclude_keys=exclude_keys,
57-
recursion_limit=recursion_limit)
62+
recursion_limit=recursion_limit,
63+
)
5864
converter.convert(source=source)
5965

6066
return source
6167

62-
6368
def transform_from_json(
64-
self, source: str,
65-
case: CaseType,
66-
preserve_tokens: List[str] | None = None,
67-
exclude_keys: List[str] | None = None,
68-
recursion_limit: int = 0,
69-
) -> str:
69+
self,
70+
source: str,
71+
case: CaseType,
72+
preserve_tokens: List[str] | None = None,
73+
exclude_keys: List[str] | None = None,
74+
recursion_limit: int = 0,
75+
) -> str:
7076
"""
7177
Transforms input JSON keys to specified case-type, and returns the result as a JSON string.
7278
7379
Args:
7480
source (str): The data to process.
75-
type (CaseType): Target key case format (e.g., CaseType.SNAKE, CaseType.CAMEL)
76-
preserve_tokens (List[str]): List of preservable strings, eg. acronyms like HTTP, ID
81+
case (CaseType): Target key case format (e.g., CaseType.SNAKE, CaseType.CAMEL)
82+
preserve_tokens (List[str]): List of preservable strings, e.g., acronyms like HTTP, ID
7783
exclude_keys (List[str]): Keys to skip (together with their children); excluded keys are not transformed and recursion does not descend into their values.
7884
recursion_limit: (int): How deep will recursion go for nested dicts, defaults to 0 (no limit),
7985
@@ -90,11 +96,16 @@ def transform_from_json(
9096
try:
9197
data = json.loads(source)
9298
except json.JSONDecodeError as e:
93-
raise json.JSONDecodeError(ERROR_INVALID_JSON.format(msg=e.msg), e.doc, e.pos) from e
94-
95-
return json.dumps(self.transform(
96-
source=data,
97-
case=case,
98-
preserve_tokens=preserve_tokens,
99-
exclude_keys=exclude_keys,
100-
recursion_limit=recursion_limit))
99+
raise json.JSONDecodeError(
100+
ERROR_INVALID_JSON.format(msg=e.msg), e.doc, e.pos
101+
) from e
102+
103+
return json.dumps(
104+
self.transform(
105+
source=data,
106+
case=case,
107+
preserve_tokens=preserve_tokens,
108+
exclude_keys=exclude_keys,
109+
recursion_limit=recursion_limit,
110+
)
111+
)
Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
from core.converters import (
1+
from case_boss.converters import (
22
CamelCaseConverter,
33
KebabCaseConverter,
44
PascalCaseConverter,
55
SnakeCaseConverter,
66
SpaceCaseConverter,
77
StartCaseConverter,
88
)
9-
from core.types import CaseType
9+
from case_boss.types import CaseType
1010

1111
CASE_TYPE_CONVERTER_MAPPING = {
1212
CaseType.SNAKE.value: SnakeCaseConverter,
@@ -18,10 +18,10 @@
1818
}
1919

2020
CASE_DESCRIPTIONS = {
21-
CaseType.CAMEL.value: "camel case (eg. 'youShallNotPass')",
22-
CaseType.KEBAB.value: "kebab case (eg. 'you-shall-not-pass')",
23-
CaseType.PASCAL.value: "pascal case (eg. 'YouShallNotPass')",
24-
CaseType.SNAKE.value: "snake case (eg. 'you_shall_not_pass')",
25-
CaseType.SPACE.value: "space case (eg. 'you shall not pass')",
26-
CaseType.START.value: "start case (eg. 'You Shall Not Pass')",
27-
}
21+
CaseType.CAMEL.value: "camel case (e.g., 'youShallNotPass')",
22+
CaseType.KEBAB.value: "kebab case (e.g., 'you-shall-not-pass')",
23+
CaseType.PASCAL.value: "pascal case (e.g., 'YouShallNotPass')",
24+
CaseType.SNAKE.value: "snake case (e.g., 'you_shall_not_pass')",
25+
CaseType.SPACE.value: "space case (e.g., 'you shall not pass')",
26+
CaseType.START.value: "start case (e.g., 'You Shall Not Pass')",
27+
}

0 commit comments

Comments
 (0)