pydantic-parse 0.1.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mariia Redchuk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.4
2
+ Name: pydantic-parse
3
+ Version: 0.1.0
4
+ Summary: Pydantic adaptor for argparse
5
+ License-File: LICENSE
6
+ Author: sagitta42
7
+ Author-email: mariia.redchuk@gmail.com
8
+ Requires-Python: >=3.12
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: pydantic (>=2.13.5,<3.0.0)
14
+ Description-Content-Type: text/markdown
15
+
16
+ # pydantic-parse
17
+
18
+ Pydantic adaptor for argparse
19
+
20
+ Example
21
+ ```python
22
+
23
+ def main():
24
+
25
+ class TestChoices(enum.StrEnum):
26
+ alice = "Alice"
27
+ bob = "Bob"
28
+
29
+ class TestModel(ArgModel):
30
+ name: TestChoices = ArgField(description="Name")
31
+ some_value: Optional[str] = ArgField(
32
+ description="value",
33
+ optional=True,
34
+ default=None,
35
+ flag=True
36
+ )
37
+ flag: bool = ArgField(description="flag", default=False, flag=True)
38
+
39
+ parser = PydanticArgParser()
40
+ parser.add_arguments_from_model(TestModel)
41
+ args = parser.parse_args()
42
+ ```
43
+
44
+ CLI
45
+ ```bash
46
+ my-package Alice --some-value 42
47
+ ```
48
+
49
+ Results in `vars(args)`:
50
+
51
+ ```python
52
+ {
53
+ 'name': <TestChoices.alice: 'Alice'>,
54
+ 'some_value': '42',
55
+ 'flag': False
56
+ }
57
+ ```
58
+
59
+ Or of course
60
+ ```python
61
+ model = TestModel(**vars(args))
62
+ ```
63
+
64
+ gives
65
+ ```python
66
+ TestModel
67
+ name=<TestChoices.alice: 'Alice'> some_value='42' flag=False
68
+ ```
69
+
70
+ -----
71
+ *Made with [poetiq](https://pypi.org/project/poetiq)*
72
+
@@ -0,0 +1,56 @@
1
+ # pydantic-parse
2
+
3
+ Pydantic adaptor for argparse
4
+
5
+ Example
6
+ ```python
7
+
8
+ def main():
9
+
10
+ class TestChoices(enum.StrEnum):
11
+ alice = "Alice"
12
+ bob = "Bob"
13
+
14
+ class TestModel(ArgModel):
15
+ name: TestChoices = ArgField(description="Name")
16
+ some_value: Optional[str] = ArgField(
17
+ description="value",
18
+ optional=True,
19
+ default=None,
20
+ flag=True
21
+ )
22
+ flag: bool = ArgField(description="flag", default=False, flag=True)
23
+
24
+ parser = PydanticArgParser()
25
+ parser.add_arguments_from_model(TestModel)
26
+ args = parser.parse_args()
27
+ ```
28
+
29
+ CLI
30
+ ```bash
31
+ my-package Alice --some-value 42
32
+ ```
33
+
34
+ Results in `vars(args)`:
35
+
36
+ ```python
37
+ {
38
+ 'name': <TestChoices.alice: 'Alice'>,
39
+ 'some_value': '42',
40
+ 'flag': False
41
+ }
42
+ ```
43
+
44
+ Or of course
45
+ ```python
46
+ model = TestModel(**vars(args))
47
+ ```
48
+
49
+ gives
50
+ ```python
51
+ TestModel
52
+ name=<TestChoices.alice: 'Alice'> some_value='42' flag=False
53
+ ```
54
+
55
+ -----
56
+ *Made with [poetiq](https://pypi.org/project/poetiq)*
@@ -0,0 +1,33 @@
1
+ [project]
2
+ name = "pydantic-parse"
3
+ version = "0.1.0"
4
+ description = "Pydantic adaptor for argparse"
5
+ authors = [
6
+ {name = "sagitta42",email = "mariia.redchuk@gmail.com"}
7
+ ]
8
+ readme = "README.md"
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "pydantic (>=2.13.5,<3.0.0)"
12
+ ]
13
+
14
+ [project.scripts]
15
+ pydantic-parse = "pydantic_parse.__main__:main"
16
+
17
+ [tool.poetry]
18
+ packages = [{include = "pydantic_parse", from = "src"}]
19
+
20
+ [tool.poetiq]
21
+ type = "package"
22
+ settings = false
23
+ progressbar = false
24
+ my_base_model = false
25
+
26
+ [build-system]
27
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
28
+ build-backend = "poetry.core.masonry.api"
29
+
30
+ [dependency-groups]
31
+ dev = [
32
+ "pytest (>=9.1.1,<10.0.0)"
33
+ ]
@@ -0,0 +1,2 @@
1
+ from pydantic_parse.__version__ import __version__
2
+ from pydantic_parse.foo import is_answer as is_answer
@@ -0,0 +1,51 @@
1
+ import argparse
2
+ import enum
3
+ import sys
4
+ from typing import Optional
5
+
6
+ from pydantic_parse.argparse.argument_parser import PydanticArgParser
7
+ from pydantic_parse.argparse_model.field import ArgField
8
+ from pydantic_parse.argparse_model.model import ArgModel
9
+ from pydantic_parse.foo import is_answer
10
+ from pydantic_parse.logger import logg
11
+
12
+
13
+ def main():
14
+
15
+ class TestChoices(enum.StrEnum):
16
+ alice = "Alice"
17
+ bob = "Bob"
18
+
19
+ class TestModel(ArgModel):
20
+ name: TestChoices = ArgField(description="Name")
21
+ some_value: Optional[str] = ArgField(
22
+ description="value",
23
+ optional=True,
24
+ default=None,
25
+ flag=True
26
+ )
27
+ flag: bool = ArgField(description="flag", default=False, flag=True)
28
+
29
+ parser = PydanticArgParser()
30
+
31
+ parser.add_arguments_from_model(TestModel)
32
+
33
+ if len(sys.argv) == 1:
34
+ parser.print_help()
35
+ sys.exit(0)
36
+
37
+ args = parser.parse_args()
38
+
39
+ # subparsers = parser.add_subparsers(dest="command")
40
+ # foo_subparser = subparsers.add_parser("foo", help="foo functionalities")
41
+ # foo_subparser.add_argument("answer", type=int, help="Answer to check")
42
+
43
+ # output = is_answer(args.answer)
44
+ logg.info(vars(args))
45
+
46
+ model = TestModel(**vars(args))
47
+ logg.info(model)
48
+
49
+
50
+ if __name__ == "__main__":
51
+ main()
@@ -0,0 +1,2 @@
1
+ from importlib.metadata import version
2
+ __version__ = version("pydantic-parse")
@@ -0,0 +1,46 @@
1
+ import argparse
2
+ from typing import Type
3
+
4
+ from pydantic_parse.argparse_model.field import ArgFieldInfo
5
+ from pydantic_parse.argparse_model.model import ArgModel
6
+
7
+
8
+ class PydanticArgParser(argparse.ArgumentParser):
9
+ def add_arguments_from_model(self, model: Type[ArgModel]):
10
+ for arg_name, arg_info in model.arg_fields().items():
11
+ self.add_argument_from_field(arg_name, arg_info)
12
+
13
+ def add_argument_from_field(
14
+ self, name: str, arg_info: ArgFieldInfo, **kwargs
15
+ ) -> argparse.Action:
16
+ arg_name = name.replace("_", "-")
17
+ if arg_info.flag:
18
+ arg_name = f"--{arg_name}"
19
+
20
+ if arg_info.flag and arg_info.arg_type is bool:
21
+ return self.add_argument(
22
+ arg_name,
23
+ action="store_true",
24
+ default=False if arg_info.is_required() else arg_info.default,
25
+ help=arg_info.description,
26
+ )
27
+
28
+ # FIXME: currently for bool
29
+ # if default was set but is not optional,
30
+ # ends up giving None in default but allows to be not given
31
+ # in theory, no non-optional flags --> unify, currently quickfix
32
+ return super().add_argument(
33
+ arg_name,
34
+ type=arg_info.arg_type,
35
+ choices=arg_info.choices,
36
+ default=arg_info.default if arg_info.optional else None,
37
+ nargs=(
38
+ "?"
39
+ if (arg_info.flag and arg_info.informative)
40
+ or (not arg_info.flag and arg_info.optional)
41
+ else None
42
+ ),
43
+ const=arg_info.const if arg_info.flag and arg_info.informative else None,
44
+ help=arg_info.description,
45
+ **kwargs,
46
+ )
@@ -0,0 +1,105 @@
1
+ import enum
2
+ from typing import Any, Union, get_args, get_origin
3
+
4
+ from pydantic import Field
5
+ from pydantic.fields import FieldInfo
6
+ from pydantic_core import PydanticUndefined
7
+
8
+ # from pydantic.fields import _FieldInfoInputs, _FieldInfoAsDict
9
+
10
+
11
+ class ArgFieldInfo(FieldInfo): # type: ignore[misc]
12
+ __slots__ = ("flag", "optional", "informative", "const")
13
+
14
+ def __init__(
15
+ self, flag: bool, optional: bool, informative: bool, const: Any, **kwargs: Any
16
+ ) -> None:
17
+ super().__init__(**kwargs)
18
+
19
+ self.flag: bool = flag
20
+ self.optional: bool = optional
21
+ self.informative: bool = informative
22
+ self.const: Any = const
23
+
24
+ def as_dict(self) -> dict[str, Any]:
25
+ """
26
+ Serialize argument properties as dict.
27
+
28
+ Get FieldInfo serialization and extract annotation and attributes.
29
+ Add custom argument field info slots.
30
+ Ignore pydantic undefined properties.
31
+ """
32
+
33
+ ret = dict(self.asdict())
34
+ ret.pop("metadata")
35
+
36
+ attr: dict[str, Any] = ret.pop("attributes")
37
+ slots = {name: getattr(self, name) for name in self.__class__.__slots__}
38
+ full_attr = attr | slots
39
+ defined_attr = {
40
+ name: value
41
+ for name, value in full_attr.items()
42
+ if value is not PydanticUndefined
43
+ }
44
+
45
+ ret |= defined_attr
46
+
47
+ return ret
48
+
49
+ @property
50
+ def choices(self) -> list | None:
51
+ assert self.arg_type is not None
52
+ if issubclass(self.arg_type, enum.Enum):
53
+ ret = [item.value for item in self.arg_type]
54
+ return ret
55
+ return None
56
+
57
+ @classmethod
58
+ def from_field_info(
59
+ cls,
60
+ field_info: FieldInfo,
61
+ *,
62
+ flag: bool,
63
+ optional: bool,
64
+ informative: bool,
65
+ const: Any,
66
+ ) -> "ArgFieldInfo":
67
+ new = cls.__new__(cls)
68
+ for slot in FieldInfo.__slots__:
69
+ setattr(new, slot, getattr(field_info, slot))
70
+ new.flag = flag
71
+ new.optional = optional
72
+ new.informative = informative
73
+ new.const = const
74
+ return new
75
+
76
+ @property
77
+ def arg_type(self) -> type:
78
+ """
79
+ Get argument type from annotation.
80
+
81
+ Extract real type from type union to cover Optional[type] case.
82
+ """
83
+ # TODO: validator
84
+ assert self.annotation is not None
85
+ if get_origin(self.annotation) is Union:
86
+ types = get_args(self.annotation)
87
+ real_types = [tp for tp in types if not tp is type(None)]
88
+ # TODO: validator
89
+ assert len(real_types) == 1
90
+ return real_types[0]
91
+ return self.annotation
92
+
93
+
94
+ def ArgField(
95
+ *,
96
+ flag: bool = False,
97
+ optional: bool = False,
98
+ informative: bool = False,
99
+ const: Any = None,
100
+ **kwargs: Any,
101
+ ) -> Any:
102
+ field_info = Field(**kwargs)
103
+ return ArgFieldInfo.from_field_info(
104
+ field_info, flag=flag, optional=optional, informative=informative, const=const
105
+ )
@@ -0,0 +1,17 @@
1
+ import enum
2
+
3
+
4
+ class InternalAttr(enum.StrEnum):
5
+ subparser = "subparser__"
6
+
7
+ @classmethod
8
+ def values(cls) -> list[str]:
9
+ return [c.value for c in cls]
10
+
11
+
12
+ class AttrDescription(enum.StrEnum):
13
+ subparser = "Subparser"
14
+
15
+ @classmethod
16
+ def from_attr(cls, attr: InternalAttr) -> str:
17
+ return cls[attr.name].value
@@ -0,0 +1,78 @@
1
+ from typing import Any, Type
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from pydantic_parse.argparse_model.field import ArgField, ArgFieldInfo
6
+ from pydantic_parse.argparse_model.internal_attr import AttrDescription, InternalAttr
7
+ from pydantic_parse.exceptions import PydanticParseTypeError
8
+
9
+
10
+ def nested_merge(first: dict, second: dict) -> dict:
11
+ for key, b_val in second.items():
12
+ if key in first and isinstance(first[key], dict) and isinstance(b_val, dict):
13
+ nested_merge(first[key], b_val)
14
+ else:
15
+ first[key] = b_val
16
+ return first
17
+
18
+
19
+ class ArgModelMeta(type(BaseModel)):
20
+ """
21
+ Metaclass for ArgModel creation.
22
+
23
+ Takes care of fields hidden to user.
24
+ Requires model fields to be defined via ArgField rather than standard Field.
25
+ """
26
+
27
+ def __new__(
28
+ mcs,
29
+ name: str,
30
+ bases: tuple[Type, ...],
31
+ namespace: dict[str, Any],
32
+ /,
33
+ # subparser: str | None = None,
34
+ **kwds: Any,
35
+ ):
36
+ namespace.setdefault("__annotations__", {})
37
+
38
+ # mcs._add_field_namespace_info(namespace, InternalAttr.subparser, subparser)
39
+
40
+ cls = super().__new__(mcs, name, bases, namespace, **kwds)
41
+
42
+ for field_name, field_info in getattr(cls, "__pydantic_fields__", {}).items():
43
+ if field_name in InternalAttr.values():
44
+ continue
45
+
46
+ if not isinstance(field_info, ArgFieldInfo):
47
+ raise PydanticParseTypeError(
48
+ f"{cls.__name__}.{field_name} must be declared with {ArgField.__name__}(...), "
49
+ f"not Field() or a bare default (got {type(field_info).__name__})"
50
+ )
51
+ return cls
52
+
53
+ @classmethod
54
+ def _add_field_namespace_info(
55
+ mcs, namespace: dict, field_name: InternalAttr, parameter: Any
56
+ ):
57
+ nested_merge(namespace, mcs._get_field_namespace_info(field_name, parameter))
58
+
59
+ @classmethod
60
+ def _get_field_namespace_info(
61
+ mcs, field_name: InternalAttr, parameter: Any
62
+ ) -> dict:
63
+ """
64
+ Create information to add to namespace to create field.
65
+
66
+ parameter: parameter to be added - defines annotation and default value.
67
+ name: field name
68
+
69
+ The default value is crucial to set fixed argument model parameters once during
70
+ child class definition.
71
+ """
72
+ ret = {
73
+ "__annotations__": {field_name: type(parameter)},
74
+ field_name: Field(
75
+ default=parameter, description=AttrDescription.from_attr(field_name)
76
+ ),
77
+ }
78
+ return ret
@@ -0,0 +1,72 @@
1
+ from typing import Any, Self, Type
2
+
3
+ from pydantic import BaseModel, model_validator
4
+
5
+ from pydantic_parse.argparse_model.field import ArgFieldInfo
6
+ from pydantic_parse.argparse_model.internal_attr import InternalAttr
7
+ from pydantic_parse.argparse_model.meta import ArgModelMeta
8
+
9
+
10
+ class ArgModel(BaseModel, metaclass=ArgModelMeta):
11
+ """
12
+ Argument model.
13
+
14
+ Defines a set of arguments, their type and description.
15
+ """
16
+
17
+ def arg_dump(self, **kwargs) -> dict[str, Any]:
18
+ """
19
+ Argument dump.
20
+
21
+ Model dump of arguments present in ArgModel instance.
22
+ Exclude internal attributes (non-arguments)
23
+
24
+ Arguments can be further excluded via exclude={name: True} in **kwargs.
25
+ Internal arguments however are always excluded.
26
+ User standard model_dump()
27
+ """
28
+ exclude_args = {key: True for key in InternalAttr}
29
+
30
+ if not "exclude" in kwargs:
31
+ kwargs["exclude"] = {}
32
+
33
+ kwargs["exclude"] |= exclude_args
34
+
35
+ return self.model_dump(**kwargs)
36
+
37
+ # @classmethod
38
+ # def subparser(cls) -> str:
39
+ # """
40
+ # Subparser.
41
+
42
+ # Default value of internal field is set at child class definition.
43
+ # """
44
+ # ret = cls.model_fields[InternalAttr.subparser].default
45
+ # return ret
46
+
47
+ # TODO: property like model_fields
48
+ @classmethod
49
+ def arg_fields(cls) -> dict[str, ArgFieldInfo]:
50
+ """
51
+ Model info that represent arguments.
52
+
53
+ Non-argument (internal) attribuges are skipped.
54
+ """
55
+ ret = {
56
+ field_name: field_info
57
+ for field_name, field_info in cls.model_fields.items()
58
+ if not field_name in InternalAttr
59
+ }
60
+
61
+ return ret
62
+
63
+ # @model_validator(mode="before")
64
+ # def check_hidden(self) -> Self:
65
+ # # TODO: check that hidden fields have not been given in input; raise error that they are reserved
66
+ # # raise ValueError("Provide subparser in your ArgModel class definition!")
67
+ # return self
68
+
69
+ @model_validator(mode="before")
70
+ def check_field_info(self) -> Self:
71
+ # TODO: mode-before model validator that all fields have a description and annotation
72
+ return self
@@ -0,0 +1,2 @@
1
+ class PydanticParseTypeError(TypeError):
2
+ pass
@@ -0,0 +1,5 @@
1
+ def is_answer(x: int) -> bool:
2
+ """
3
+ Checks if input is answer to the question of life, universe, and everything.
4
+ """
5
+ return x == 42
@@ -0,0 +1,154 @@
1
+ import enum
2
+ import inspect
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import Any
6
+ from dotenv import dotenv_values
7
+
8
+
9
+ class AnsiStyle(enum.StrEnum):
10
+ normal = "0"
11
+ bold = "1"
12
+ start = "\033["
13
+ end = "\033[0m"
14
+ fg8bit = "38;5"
15
+
16
+
17
+ class AnsiColor(enum.StrEnum):
18
+ green = "32"
19
+ grey = "90"
20
+ red = "31"
21
+ yellow = "33"
22
+ white = "37"
23
+ lila = "91"
24
+
25
+ def apply(self, message: Any, bold: bool = False) -> str:
26
+ """
27
+ To be used with color based.
28
+
29
+ Bold not implemented for 8 bit colors.
30
+ """
31
+ if self.is_8bit:
32
+ style = AnsiStyle.fg8bit
33
+ else:
34
+ style = AnsiStyle.bold if bold else AnsiStyle.normal
35
+
36
+ ret = f"{AnsiStyle.start}{style};{self}m{message}{AnsiStyle.end}"
37
+ return ret
38
+
39
+ def bold(self, message: Any) -> str:
40
+ """
41
+ Shortcut for bold colored text
42
+ """
43
+ ret = self.apply(message, bold=True)
44
+ return ret
45
+
46
+ @property
47
+ def is_8bit(self) -> bool:
48
+ return self == AnsiColor.lila
49
+
50
+
51
+ class LevelFormatter(logging.Formatter):
52
+ def __init__(self, formats, default_fmt=None, datefmt=None):
53
+ super().__init__(datefmt=datefmt)
54
+ self.formats = {
55
+ level: logging.Formatter(fmt, datefmt=datefmt)
56
+ for level, fmt in formats.items()
57
+ }
58
+ self.default_formatter = logging.Formatter(
59
+ default_fmt or "%(message)s", datefmt=datefmt
60
+ )
61
+
62
+ def format(self, record):
63
+ formatter = self.formats.get(record.levelno, self.default_formatter)
64
+ return formatter.format(record)
65
+
66
+
67
+ class Logger:
68
+ def __init__(self, log_level=logging.INFO):
69
+ self.log_level = log_level
70
+
71
+ self._logger = logging.getLogger(__name__)
72
+
73
+ full_format = "%(asctime)s [%(levelname)s] %(classname)s.%(funcName)s:%(lineno)d - %(message)s"
74
+ short_format = "[%(levelname)s] %(classname)s - %(message)s"
75
+ no_format = ""
76
+ level_format = full_format if log_level == logging.DEBUG else no_format
77
+ formatter = LevelFormatter(
78
+ {
79
+ logging.DEBUG: full_format,
80
+ logging.ERROR: level_format,
81
+ logging.INFO: level_format,
82
+ logging.WARNING: level_format,
83
+ }
84
+ )
85
+
86
+ handler = logging.StreamHandler()
87
+ handler.setFormatter(formatter)
88
+ handler.setLevel(log_level)
89
+ self._logger.addHandler(handler)
90
+
91
+ self._logger.setLevel(log_level)
92
+ self._logger.propagate = False
93
+
94
+ def info(self, message: Any, header: bool = False, poetiq: bool = False):
95
+ if poetiq:
96
+ color = AnsiColor.lila
97
+ elif header:
98
+ color = AnsiColor.green
99
+ else:
100
+ color = AnsiColor.white
101
+
102
+ return self._log(logging.INFO, color.apply(message, header))
103
+
104
+ def error(self, message: Any):
105
+ return self._log(logging.ERROR, AnsiColor.red.apply(message))
106
+
107
+ def warning(self, message: Any, important: bool = False):
108
+ if important:
109
+ message = f"! WARNING ! {message}"
110
+ return self._log(logging.WARNING, AnsiColor.yellow.apply(message, important))
111
+
112
+ def debug(self, message: Any):
113
+ return self._log(logging.DEBUG, AnsiColor.grey.apply(message))
114
+
115
+ @property
116
+ def is_debug(self) -> bool:
117
+ return self.log_level == logging.DEBUG
118
+
119
+ def _log(self, level, message: Any):
120
+ """
121
+ Common log interface for info/error/warning/debug.
122
+
123
+ Auto-detect class name of caller.
124
+ Account for stack level to display correct funcName and lineno.
125
+ Skip 2 stack levels including this method,
126
+ the info/error/warning/debug method calling it,
127
+ """
128
+ self._logger.log(
129
+ level,
130
+ message,
131
+ extra={"classname": self._get_caller_class_name()},
132
+ stacklevel=3,
133
+ )
134
+
135
+ def _get_caller_class_name(self):
136
+ """
137
+ Determine class name of where logger is called from.
138
+
139
+ Obtain frame index 3 corresponding to actual caller
140
+ (0 = this method, 1 = _log internal method, 2 = info/warning/error/debug logger call).
141
+ Return class or module name of that frame.
142
+ """
143
+ frame = inspect.stack()[3].frame
144
+ if "self" in frame.f_locals:
145
+ return type(frame.f_locals["self"]).__name__
146
+ elif "cls" in frame.f_locals:
147
+ return frame.f_locals["cls"].__name__
148
+ return frame.f_globals.get("__name__", "-")
149
+
150
+
151
+ # NOTE: dotenv_values() in some cases yielded empty .env for unexplained reason
152
+ env_config = dotenv_values(Path.cwd() / ".env")
153
+ is_debug = env_config.get("DEBUG_PYDANTIC_PARSE", "").lower() in ("true", "1")
154
+ logg = Logger(log_level=logging.DEBUG if is_debug else logging.INFO)
File without changes