xyz.josephcz.dictmapper 1.1.0
Map a dict to another, without complex and ugly code.
Requires Python: >=3.8

# DictMapper
DictMapper can map one dict or data in some popular format to another, without complex and ugly code. DictMapper uses a "Chain Caller" or "Fluent Style" API, and a mapper can written once, apply anywhere in your code.

DictMapper is inspired by [AutoMapper](https://automapper.org/).

# Installation
```bash
pip install xyz.josephcz.dictmapper
```

If you want to map from/to JSON or YAML, you may install the dependencies below:
 - `PyYAML>=5.4.1` if you want to use YAML.
 - `toml>=0.10.2` if you want to use TOML.

You can run command below:
```bash
pip install xyz.josephcz.dictmapper pyyaml toml
```

Or better:
```bash
# if you need YAML
pip install "pyyaml>=5.4.1"

# if you need TOML
pip install "toml>=0.10.2"
```

# Usage
## Quickstart
```python
import xyz.josephcz.dictmapper as dictmapper;

example_srcdata = {
    "first_name": "Hatsune",
    "last_name": "Miku",
    "avail_languages": ["ja-JP", "zh-CN", "en-US"],
    "type": "Vocal Synthesizer Application",
    "is_open_source_and_free": False
};

mapper = (dictmapper.DictMapper()
    .AddMapping("avail_languages", "languages")
    .AddAdvancedMapping(lambda data: {"fullName": data["first_name"] + " " + data["last_name"]})
    .AddProcessedKeyMapping("is_open_source_and_free", "license", (lambda x: "FOSS" if x else "Proprietary")));
destdata = mapper.MapFrom(example_srcdata).ToDict();
print(destdata);
# {"languages": ["ja-JP", "zh-CN", "en-US"], "fullName": "Hatsune Miku", "license": "Proprietary", "type": "Vocal Synthesizer Application"}
```

## Examples
**Add Static Value**
```python
import xyz.josephcz.dictmapper as dictmapper;
http_headers = {
    "Content-Type": "text/html
};

mapper = dictmapper.DictMapper().AddStaticValue("User-Agent", "python/3.9");
mapper.FromDict(http_headers).ToDict();
# {"Content-Type": "text/html", "User-Agent": "python/3.9"}
```

**Add Value By Dict Mapping**
```python
import xyz.josephcz.dictmapper as dictmapper;
gender_mapping = {
    0: "F",
    1: "M",
    2: "MtF",
    3: "FtM",
    # Other genders, get a full list from server so we can update it when needed.
    4: "__SEE_EXTENSION_FIELD" 
};

user = {
    "uid": 1234567,
    "gender": 0,
    "bio": "this.isQuestion = 2B || !2B",
}

mapper = dictmapper.DictMapper().AddValueByDictMapping("gender", "genderString", gender_mapping);
mapper.FromDict(http_headers).ToDict();
# {
#     "uid": 1234567,
#     "genderString": "F",
#     "bio": "this.isQuestion = 2B || !2B",
# }
```

**Using StdClass**
```python
import xyz.josephcz.dictmapper as dictmapper;
from xyz.josephcz.dict2class import DictStdClass;

X509 = {
    "CN": "Joseph Chris",
    "SAN": "*.josephcz.xyz"
}

mapper = (dictmapper.DictMapper()
    .AddMapping("CN", "CommonName")
    .AddMapping("SAN", "SubjectAlternaveName"));
X509DataStd = mapper.FromDict(http_headers).ToStdClass();
print(X509DataStd.CommonName)           # "Joseph Chris"
print(X509DataStd.SubjectAlternaveName) # "*.josephcz.xyz"

class X509CertData(DictStdClass):
    pass;
X509Data = mapper.FromDict(http_headers).ToClass(X509CertData);
print(X509DataStd.CommonName)           # "Joseph Chris"
print(X509DataStd.SubjectAlternaveName) # "*.josephcz.xyz"

print(type(X509DataStd)) # <class 'xyz.josephcz.dict2class.DictStdClass'>
print(type(X509Data))    # <class '__main__.DictStdClass'>
```

**Read and Modify TOML Configuration File**
```python
"""Configuration File Content:
ip = "10.0.0.1"
port = 12450
password = "P@ssw0rd"

<EOF>"""
# Do't do like this!
# Place password in configuration file without encryption is unsafe, this is only for example!!!

import xyz.josephcz.dictmapper as dictmapper;
mapper = (dictmapper.DictMapper()
    .AddMapping("ip", "ServerIPAddress")
    .AddMapping("port", "ServerPortNumber")
    .AddMapping("password", "ServerAuthPassword"));

new_mapper = dictmapper.DictMapper().AddMapping("ServerAuthPassword", "ServerAuthHash");

def upgrade():
    config = mapper.FromTomlFile("/etc/rickroll-web/config.toml").ToDict();
    passwd(config);
    config_string = new_mapper.MapFrom(config).ToToml();
    with open("/etc/rickroll-web/config.toml", "w", encoding="utf-8") as f:
        f.write(config_string);

def passwd(config):
    print("Your password is too weak. Please change it now.")
    while True:
        old_password = input("Enter old password:");
        if config["ServerAuthPassword"] == old_password:
            break;
        print("Sorry, old password is not correct.");
    new_password = input("Enter new password:");
    import random;
    randval = str(random.randint(100000, 999999));
    import hashlib;
    password_hash = hashlib.sha1(new_password.encode("utf-8")).hexdigest();
    config["ServerAuthPassword"] = password_hash + "!" + random;
```

**Shape JSON Data You Get From Internet**
```python
import requests;
import xyz.josephcz.dictmapper as dictmapper;

res = requests.get("https://api.bilibili.com/x/web-interface/view?aid=360940");
resdata = res.json()["data"];
print(resdata);
"""
{
    "bvid": "BV1ix411F7TB", 
    "aid": 360940, 
    "videos": 1, 
    "tid": 30, 
    "tname": "VOCALOID·UTAU", 
    "copyright": 1, 
    "pic": "http://i0.hdslb.com/bfs/archive/a4cf8de9c883109826664f4d9bd870c81cdd65de.jpg", 
    "title": "66CCFF", 
    "pubdate": 1348274446, 
    "ctime": 1497378101, 
    "state": 0, 
    "duration": 235, 
    "owner": {
        "mid": 111254, 
        "face": "http://i0.hdslb.com/bfs/face/bc7b1d5365041c2e89f35187ccf7739096994f7c.gif"
    }, 
    "dynamic": "", 
    "cid": 555439,
    ...
}
"""

mapper = (dictmapper.DictMapper(True)
    .AddMapper("bvid",    "BvId")
    .AddMapper("aid",     "AvId")
    .AddMapper("title",   "title")
    .AddMapper("ctime",   "uploadedAt")
    .AddMapper("owner",   "uploader")
    .AddMapper("cid",     "videoId"));
data = mapper.MapFrom(resdata).ToStdClass();
jsonstr = mapper.MapFrom(resdata).ToJson();
filename = "/var/www/bilibili/by-avid/" + str(data.AvId) + "/meta-info.json";
with open(filename, "w+", encoding="utf-8") as f:
    f.write(jsonstr);
```

## API Reference
```
Note: Table is not showen in pypi, please go github: https://github.com/baobao1270/pylib-dictmapper
```

**Class: DictMapper**
```python
class DictMapper(removeNotDeclared:bool = False)
```
 - Namespace: `xyz.josephcz`
 - Module: `dictmapper`
 - File: `xyz/josephcz/dictmapper/DictMapper.py`

Create a mapper and add rules to it. The example below supposes `src` is a dict you have (usually you get it from a HTTP request), and `dest` is the dict you want.

Functions:
Function                                         |Usage
-------------------------------------------------|-----
`__init__(removeNotDeclared:bool = False) -> self`|Create a new `DictMapper` instance. If `removeNotDeclared` set to `True`, data not mapped by a mapping rule will not appear in the destination dict `dict`. Or the data not mapped will be copied to the new dict.
`AddMapping(srcKey:str, destKey:str) -> self`    |Add a mapping rule: `dest[destKey] = src[srcKey]`
`AddRemoval(removingSrcKey:str) -> self`         |Add a mapping rule: The `src[removingSrcKey]` will not appear in the destination dict `dest`.
`AddStaticValue(destKey:str, value:str) -> self` |Add a mapping rule: `dest[destKey] = value`.
`AddValueByDictMapping(searchingSrcKey:str, destKey:str, mapDict:dict[str,str]) -> self`|You have a `dict` that maps `searchingSrcKey` to a value, then add a mapping rule: `dest[destKey] = mapDict[src[searchingSrcKey]]`.
`AddProcessedKeyMapping(srcKey:str, destKey:str, func:Callable[[Any], Any]) -> self`|The function `func` receives the `src[srcKey]` data from source dict and returns the value you want to set to `dest[destKey]`. Same as `dest[destKey] = func(src[srcKey])`.
`AddAdvancedMapping(func:Callable[[dict], dict[str, Any]]) -> self`|Advanced Mapping. The function `func` receives the full source data and returns a dict that will be merge to destination dict `dest`. Same as: `dest.update(func(src))`.
`MapFrom(data) -> DictMapperSource`|Set source data.
`FromJson(jsonStr:str) -> DictMapperSource`|Set source dict `src` from a JSON string.
`FromToml(jsonStr:str) -> DictMapperSource`|Set source dict `src` from a TOML string.
`FromYaml(jsonStr:str) -> DictMapperSource`|Set source dict `src` from a YAML string.
`def FromJsonFile(filename:str, encoding:str = "utf-8") -> DictMapperSource`|Set source dict from a JSON file. The `encoding` is a parameter of python built-in function `open` (See: [Python Documentation](https://docs.python.org/3/library/functions.html#open)).
`def FromTomlFile(filename:str) -> DictMapperSource`                        |Set source dict from a TOML file. The file encoding should be UTF-8 and can not be changed.
`def FromYamlFile(filename:str, encoding:str = "utf-8") -> DictMapperSource`|Set source dict from a YAML file. The `encoding` is a parameter of python built-in function `open` (See: [Python Documentation](https://docs.python.org/3/library/functions.html#open)).

**Class: DictMapperSource**
```python
class DictMapperSource(mapper:DictMapper, data)
```
 - Namespace: `xyz.josephcz`
 - Module: `dictmapper`
 - File: `xyz/josephcz/dictmapper/DictMapper.py`

Create a mapper-data link. You are not recommended to create instances of this class.

Functions:
Function                                         |Usage
-------------------------------------------------|-----
`IsList() -> bool`|Return `True` when the source data is an instance of `list`, or reutrn `False`.
`ToDict() -> dict`|Return the mapping result, aka destination dict `dest`.
`ToClass(destClass) -> Any`            |Return the mapping result in class. You can access the `key` by `dest.key` instead of `dest["key"]`. The `destClass` should be a subclass of `xyz.josephcz.dict2class.DictStdClass`. See [dict2class Documentation](https://pypi.org/project/xyz.josephcz.dict2class/).
`ToStdClass(destClass) -> DictStdClass`|Return the mapping result in class. You can access the `key` by `dest.key` instead of `dest["key"]`. The return value is an instance of `xyz.josephcz.dict2class.DictStdClass`. See [dict2class Documentation](https://pypi.org/project/xyz.josephcz.dict2class/).
`ToTomlWithRootKey(rootKey:str) -> str`|Return the mapping result in TOML string format. For TOML does not allow unnamed array as root element, you should add a root key name to it or use `ToTomlList()`. And this function also applies to dict.
`ToTomlList() -> list[str]`            |If the source data `src` is a list, return the mapping result in a list of TOML string, or raise `OperationNotAllowedError`. For TOML does not allow unnamed array as root element, you should use this function or `ToTomlWithRootKey(rootKey:str)`. Each item of the list will be converted to a TOML string.
`ToToml() -> str`|If the source data `src` is not a list, return the mapping result in TOML string format, or raise `OperationNotAllowedError`.
`ToYaml() -> str`|Return the mapping result in YAML string format.
`ToJson(skipkeys = False, ensure_ascii = True, check_circular = True, allow_nan = True, cls = None, indent = None, separators = None, default = None, sort_keys  =False) -> str`|Return the mapping result in JSON string format. Each arguments will be passed to `json.dumps()`. For meanings of arguments, see: [Python Documentation: json](https://docs.python.org/3/library/json.html#json.dumps).

## Errors
**OperationNotAllowedError**
```python
class OperationNotAllowedError(Exception):
    def __init__(self, operation:str, on:str)
```
 - Namespace: `xyz.josephcz`
 - Module: `dictmapper`
 - File: `xyz/josephcz/dictmapper/OperationNotAllowedError.py`

Raise when operation on an object is not allowed.

Members:
Member         |Meaning
---------------|-------
`operation:str`|Operation Name, usually the name of method in class.
`on:str`       |Operation Target Name, usually the name of class.

May Caused By:
Function                                        |Reason
------------------------------------------------|-------
`DictMapper.AddRemoval(srcKey:str, destKey:str)`|When `DictMapper(emoveNotDeclared=True)`, removing a key from source data is not needed and allowed for all unmapped keys will be removed automatically.
`DictMapperSource.ToToml()`    |Source is a list, but you try to convert to a TOML, which is not supportted by TOML Syntax. Use `ToTomlList()` instead.
`DictMapperSource.ToTomlList()`|Source is not a list, but you try to convert to a TOML list. Use `ToToml()` instead.

**ParameterValidationError**
```python
class ParameterValidationError(Exception):
    def __init__(self, parameter:str, excepted:str, given:str)
```
 - Namespace: `xyz.josephcz`
 - Module: `dictmapper`
 - File: `xyz/josephcz/dictmapper/ParameterValidationError.py`

Raise the type, value or format of a given parameter is not correct.

Members:
Member         |Meaning
---------------|-------
`parameter:str`|Validating parameter.
`excepted:str` |Excepted parameter type, value or format.
`given:str`    |Given  parameter type, value or format in actual.

May Caused By:
Function                                        |Reason
------------------------------------------------|-------
`DictMapperShaper`                   |The source data you given is not a dict or list of dict.
`DictMapperShaper.ToClass(destClass)`|The `destClasss` is not a subclass of `xyz.josephcz.dict2class.DictStdClass`.

# License
MIT