# Init Helper
Tools to parse args, read config, init logs and etc
<details>
<summary>Копирование кода для тестов</summary>

```sh
cp -r $ORIGIN_DIR/init_helpers ./init_helpers
```

</details>

## Парсинг конфигов
### Создадим toml конфиг файл
```bash
printf 'some = 123\nother = "www"\nbar = "empty"' > example.toml

```
Проверим содержимое
```bash
cat example.toml
```
получим:
```toml
some = 123
other = "www"
bar = "empty"
```
### Считаем конфиг как экземпляр датакласса
#### Подготовим python файл, имитирующий чтение конфига и печатающий результат
```bash
printf '
import dataclasses
import init_helpers
@dataclasses.dataclass
class Config:
    other: str
    some: int = 42
    foo: float = 1.23    
    topics: tuple[str, ...] = ("one", )    
config = init_helpers.parse_args_as_dataclass(Config)
print(config)
' > init.py
```
#### Запустим инициализацию, передав ей конфиг
```bash
python init.py example.toml
```
Получим
```text
Config(other='www', some=123, foo=1.23, topics=('one',))
```
#### Запустим инициализацию с конфигом и переопределив значения из конфига 
```bash
python init.py example.toml -v foo=4.2 -v some=42
```
Получим
```text
Config(other='www', some=42, foo=4.2, topics=('one',))
```
#### Запустим инициализацию без конфига, передав необходимые значения через аргументы запуска
```bash
python init.py -v other=qqq -v 'topics=["two","four"]'
```
Получим
```text
Config(other='qqq', some=42, foo=1.23, topics=('two', 'four'))
```
### Создадим ini конфиг файл
```bash
printf '[main]\ntimeout = 10\nurl = http://\ntopics = ["ini"]' > example2.ini
```
Проверим содержимое
```bash
cat example2.ini
```
получим:
```ini
[main]
timeout = 10
url = http://
topics = ["ini"]
```
### Считаем ini конфиг как экземпляр датакласса
#### Подготовим python файл, имитирующий чтение конфига и печатающий результат
```bash
printf '
import dataclasses
import init_helpers
@dataclasses.dataclass
class Config:
    @dataclasses.dataclass
    class MainConfig:
        url: str
        topics: tuple[str]
        timeout: float = 5
    main: MainConfig    
config = init_helpers.parse_args_as_dataclass(Config)
print(config)
' > init.py
```
#### Запустим инициализацию, передав ей ini конфиг
```bash
python init.py example2.ini
```
Получим
```text
Config(main=Config.MainConfig(url='http://', topics=('ini',), timeout=10.0))
```
### Примечание
`init_helpers.parse_args_as_dataclass` парсит файл на основе расширения, поэтому один и тот же код может работать
и с `.ini` и `.toml` файлами. Для примера создадим toml файл и запустим с ним приложение:
```bash
printf '[main]\ntimeout = 10\nurl = "http://"\ntopics = ["toml"]' > example2.toml
python init.py example2.toml
```
Получим:
```text
Config(main=Config.MainConfig(url='http://', topics=('toml',), timeout=10.0))
```
## Conditional insert
### Conditional append
Appends passed args to list passed as first argument, one by one checking condition.  
Default condition is `init_helpers.is_not_none`. Example:
```python
from init_helpers import conditional_append
conditional_append(target := [73, 123], 'value', None, 1, -2, 3)
print(target)
conditional_append(target := [73, 123], 'value', None, 1, -2, 3, 
                   _condition=lambda x: isinstance(x, int) and x > 0)
print(target)
```
will produce:
```text
[73, 123, 'value', 1, -2, 3]
[73, 123, 1, 3]
```
### Conditional add
Adds passed args to set passed as first argument, one by one checking condition.  
Default condition is `init_helpers.is_not_none`. Example:
```python
from init_helpers import conditional_add
conditional_add(target := {73, 123}, None, 1, -2, 3)
print(target)
conditional_add(target := {73, 123}, 1, -2, 3, _condition=lambda x: x > 0)
print(target)
```
will produce:
```text
{1, 3, 73, 123, -2}
{3, 73, 123, 1}
```
### Conditional set
Sets passed kwargs to MutableMapping passed as first argument, one by one checking values by condition.  
Default condition is `init_helpers.is_not_none`. Example:
```python
from init_helpers import conditional_set
conditional_set(target := {'test': 'q'}, key='value', skip=None)
print(target)
conditional_set(target := {'test': 'q'}, key1=1, key2=2, skip1=-1, skip2=-2, _condition=lambda x: x > 0)
print(target)
```
will produce (sets do not keep ordering, so values are shuffled):
```text
{'test': 'q', 'key': 'value'}
{'test': 'q', 'key1': 1, 'key2': 2}
```

## CustomDumps
Extended version of json.dumps
### Basic types serialise as in original

```python
from init_helpers import custom_dumps
print("None:", custom_dumps(None))
print("int:", custom_dumps(42))
print("float:", custom_dumps(4.2))
print("bool:", custom_dumps(True))
print("list:", custom_dumps([42]))
print("dict:", custom_dumps({42: 4.2}))
```
will produce
```text
None: null
int: 42
float: 4.2
bool: true
list: [42]
dict: {"42": 4.2}
```
### Additional types serialise

```python
import dataclasses
import zoneinfo
import datetime
from decimal import Decimal
from init_helpers import ReprInDumps, custom_dumps, read_json_file_by_path

print("set:", custom_dumps({42}))  # serialised as list
print("Decimal:", custom_dumps(Decimal("1.20") + Decimal("1.30")))  # serialised as string
print("Exception:", custom_dumps(ValueError("payload")))  # serialised as string by repr method
print("bytes:", custom_dumps(bytes.fromhex('ffeeaa')))  # as base64 with prefix: data:application/octet-stream;base64,
print("time:", custom_dumps(datetime.time(12, 34, 56)))  # serialised as iso string
print("date:", custom_dumps(datetime.date(2011, 11, 11)))  # serialised as iso string
print("aware datetime:", custom_dumps(datetime.datetime(
    2024, 11, 11, tzinfo=zoneinfo.ZoneInfo('Europe/Moscow'))))  # tz-aware serialised as int millisecond timestamp
try:
    custom_dumps(datetime.datetime(2024, 11, 11))  # naive datetime is disallowed
except TypeError as e:
    print("naive datetime:", e)
  

@dataclasses.dataclass
class Foo:
    a: int
    b: str = 'www'
    c: float = dataclasses.field(default=123, repr=False)

print("dataclass instance:", custom_dumps(Foo(42)))  # serialised as dict WITHOUT field with repr=False
```

will produce
```text
set: [42]
Decimal: "2.50"
Exception: {"_t": "PY::Exception", "key": "ValueError", "args": ["payload"]}
bytes: "data:application/octet-stream;base64,b'/+6q'"
time: "12:34:56"
date: "2011-11-11"
aware datetime: "2024-11-11T00:00:00+03:00"
naive datetime: TypeError: datetime.datetime WITHOUT tzinfo is not JSON serializable
dataclass instance: {"a": 42, "b": "www"}
```
## Dataclass Protocol
Can be used to use `isinstance` on dataclasses. Example:
### Create a file with typing based on DataclassProtocol
```python
from typing import Any
from dataclasses import dataclass
from init_helpers import DataclassProtocol

def is_dc(obj: DataclassProtocol | Any) -> bool:
    return isinstance(obj, DataclassProtocol)
    
@dataclass
class DC:
    a: int = 1

class NonDC:
    a: int
    
print(f'{is_dc(DC)=}')
print(f'{is_dc(NonDC)=}')
print(f'{is_dc(DC())=}')
print(f'{is_dc(NonDC())=}')
```
```text
is_dc(DC)=True
is_dc(NonDC)=False
is_dc(DC())=True
is_dc(NonDC())=False
```
