pydantic-settings-export 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.
- pydantic_settings_export-0.1.0/LICENCE +21 -0
- pydantic_settings_export-0.1.0/PKG-INFO +108 -0
- pydantic_settings_export-0.1.0/README.md +82 -0
- pydantic_settings_export-0.1.0/pydantic_settings_export/__init__.py +7 -0
- pydantic_settings_export-0.1.0/pydantic_settings_export/cli.py +111 -0
- pydantic_settings_export-0.1.0/pydantic_settings_export/constants.py +23 -0
- pydantic_settings_export-0.1.0/pydantic_settings_export/exporter.py +39 -0
- pydantic_settings_export-0.1.0/pydantic_settings_export/generators/__init__.py +6 -0
- pydantic_settings_export-0.1.0/pydantic_settings_export/generators/abstract.py +58 -0
- pydantic_settings_export-0.1.0/pydantic_settings_export/generators/dotenv.py +45 -0
- pydantic_settings_export-0.1.0/pydantic_settings_export/generators/markdown.py +87 -0
- pydantic_settings_export-0.1.0/pydantic_settings_export/models.py +165 -0
- pydantic_settings_export-0.1.0/pydantic_settings_export/settings.py +122 -0
- pydantic_settings_export-0.1.0/pydantic_settings_export/utils.py +177 -0
- pydantic_settings_export-0.1.0/pydantic_settings_export/version.py +3 -0
- pydantic_settings_export-0.1.0/pyproject.toml +149 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Jag_k
|
|
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,108 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pydantic-settings-export
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Export your Pydantic settings to a Markdown and .env.example files!
|
|
5
|
+
Home-page: https://github.com/jag-k/pydantic-settings-export#readme
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: Jag_k
|
|
8
|
+
Author-email: jag-k@users.noreply.github.com
|
|
9
|
+
Requires-Python: >=3.11,<4.0
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Documentation
|
|
18
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: Utilities
|
|
21
|
+
Requires-Dist: pydantic (>=2,<3)
|
|
22
|
+
Requires-Dist: pydantic-settings (>=2,<3)
|
|
23
|
+
Project-URL: Repository, https://github.com/jag-k/pydantic-settings-export
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# pydantic-settings-export
|
|
27
|
+
|
|
28
|
+
[](https://pypi.org/project/pydantic-settings-export/)
|
|
29
|
+
|
|
30
|
+
*Export your Pydantic settings to a Markdown and .env.example files!*
|
|
31
|
+
|
|
32
|
+
This package provides a way to use [pydantic](https://docs.pydantic.dev/) (and [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/)) models to generate a Markdown file with the settings and their descriptions, and a `.env.example` file with the settings and their default values.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install pydantic-settings-export
|
|
38
|
+
# or
|
|
39
|
+
pipx install pydantic-settings-export # for a global installation and using as a CLI
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
You can see the examples of usage this package in the [./docs/Configuration.md](./docs/Configuration.md) and [.env.example](./.env.example).
|
|
45
|
+
|
|
46
|
+
### As code
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from pydantic import BaseSettings
|
|
50
|
+
from pydantic_settings_export import Exporter, MarkdownSettings, Settings as PSESettings
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Settings(BaseSettings):
|
|
54
|
+
my_setting: str = "default value"
|
|
55
|
+
another_setting: int = 42
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Export the settings to a Markdown file `docs/Configuration.md` and `.env.example` file
|
|
59
|
+
Exporter(
|
|
60
|
+
PSESettings(
|
|
61
|
+
markdown=MarkdownSettings(
|
|
62
|
+
save_dirs=["docs"],
|
|
63
|
+
),
|
|
64
|
+
),
|
|
65
|
+
).run_all(Settings)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### As CLI
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pydantic-settings-export --help
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Configuration
|
|
75
|
+
|
|
76
|
+
You can add a `pydantic_settings_export` section to your `pyproject.toml` file to configure the exporter.
|
|
77
|
+
|
|
78
|
+
```toml
|
|
79
|
+
|
|
80
|
+
[tool.pydantic_settings_export]
|
|
81
|
+
project_dir = "."
|
|
82
|
+
default_settings = [
|
|
83
|
+
"pydantic_settings_export.settings:Settings",
|
|
84
|
+
]
|
|
85
|
+
dotenv = { "name" = ".env.example" }
|
|
86
|
+
|
|
87
|
+
[tool.pydantic_settings_export.markdown]
|
|
88
|
+
name = "Configuration.md"
|
|
89
|
+
save_dirs = [
|
|
90
|
+
"docs",
|
|
91
|
+
"wiki",
|
|
92
|
+
]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Todo
|
|
96
|
+
|
|
97
|
+
- [ ] Add tests
|
|
98
|
+
- [ ] Add more configuration options
|
|
99
|
+
- [ ] Add more output formats
|
|
100
|
+
- [ ] TOML (and `pyproject.toml`)
|
|
101
|
+
- [ ] JSON
|
|
102
|
+
- [ ] YAML
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
[MIT](./LICENCE)
|
|
108
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# pydantic-settings-export
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/pydantic-settings-export/)
|
|
4
|
+
|
|
5
|
+
*Export your Pydantic settings to a Markdown and .env.example files!*
|
|
6
|
+
|
|
7
|
+
This package provides a way to use [pydantic](https://docs.pydantic.dev/) (and [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/)) models to generate a Markdown file with the settings and their descriptions, and a `.env.example` file with the settings and their default values.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install pydantic-settings-export
|
|
13
|
+
# or
|
|
14
|
+
pipx install pydantic-settings-export # for a global installation and using as a CLI
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
You can see the examples of usage this package in the [./docs/Configuration.md](./docs/Configuration.md) and [.env.example](./.env.example).
|
|
20
|
+
|
|
21
|
+
### As code
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from pydantic import BaseSettings
|
|
25
|
+
from pydantic_settings_export import Exporter, MarkdownSettings, Settings as PSESettings
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Settings(BaseSettings):
|
|
29
|
+
my_setting: str = "default value"
|
|
30
|
+
another_setting: int = 42
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Export the settings to a Markdown file `docs/Configuration.md` and `.env.example` file
|
|
34
|
+
Exporter(
|
|
35
|
+
PSESettings(
|
|
36
|
+
markdown=MarkdownSettings(
|
|
37
|
+
save_dirs=["docs"],
|
|
38
|
+
),
|
|
39
|
+
),
|
|
40
|
+
).run_all(Settings)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### As CLI
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pydantic-settings-export --help
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
You can add a `pydantic_settings_export` section to your `pyproject.toml` file to configure the exporter.
|
|
52
|
+
|
|
53
|
+
```toml
|
|
54
|
+
|
|
55
|
+
[tool.pydantic_settings_export]
|
|
56
|
+
project_dir = "."
|
|
57
|
+
default_settings = [
|
|
58
|
+
"pydantic_settings_export.settings:Settings",
|
|
59
|
+
]
|
|
60
|
+
dotenv = { "name" = ".env.example" }
|
|
61
|
+
|
|
62
|
+
[tool.pydantic_settings_export.markdown]
|
|
63
|
+
name = "Configuration.md"
|
|
64
|
+
save_dirs = [
|
|
65
|
+
"docs",
|
|
66
|
+
"wiki",
|
|
67
|
+
]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Todo
|
|
71
|
+
|
|
72
|
+
- [ ] Add tests
|
|
73
|
+
- [ ] Add more configuration options
|
|
74
|
+
- [ ] Add more output formats
|
|
75
|
+
- [ ] TOML (and `pyproject.toml`)
|
|
76
|
+
- [ ] JSON
|
|
77
|
+
- [ ] YAML
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
[MIT](./LICENCE)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from inspect import isclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic_settings import BaseSettings
|
|
9
|
+
|
|
10
|
+
from pydantic_settings_export.exporter import Exporter
|
|
11
|
+
from pydantic_settings_export.generators import ALL_GENERATORS, AbstractGenerator
|
|
12
|
+
from pydantic_settings_export.settings import Settings
|
|
13
|
+
from pydantic_settings_export.utils import ObjectImportAction
|
|
14
|
+
from pydantic_settings_export.version import __version__
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
CDW = Path.cwd()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SettingsAction(ObjectImportAction):
|
|
21
|
+
"""The settings action."""
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def callback(obj: Any) -> type[BaseSettings]:
|
|
25
|
+
"""Check if the object is a settings class."""
|
|
26
|
+
if isclass(obj) and issubclass(obj, BaseSettings):
|
|
27
|
+
return obj
|
|
28
|
+
elif not isclass(obj) and isinstance(obj, BaseSettings):
|
|
29
|
+
return obj.__class__
|
|
30
|
+
raise ValueError(f"The {obj!r} is not a settings class.")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GeneratorAction(ObjectImportAction):
|
|
34
|
+
"""The generator action."""
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def callback(obj: Any) -> type[AbstractGenerator]:
|
|
38
|
+
"""Check if the object is a settings class."""
|
|
39
|
+
if isclass(obj) and issubclass(obj, AbstractGenerator):
|
|
40
|
+
return obj
|
|
41
|
+
elif not isclass(obj) and isinstance(obj, AbstractGenerator):
|
|
42
|
+
return obj.__class__
|
|
43
|
+
raise ValueError(f"The {obj!r} is not a generator class.")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def dir_type(path: str) -> Path:
|
|
47
|
+
"""Check if the path is a directory."""
|
|
48
|
+
p = Path(path).resolve().absolute()
|
|
49
|
+
if p.is_dir():
|
|
50
|
+
return p
|
|
51
|
+
raise argparse.ArgumentTypeError(f"The {path} is not a directory.")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
parser = argparse.ArgumentParser(
|
|
55
|
+
description="Export pydantic settings to a file",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--version",
|
|
59
|
+
"-v",
|
|
60
|
+
action="version",
|
|
61
|
+
version=f"pydantic-settings-export {__version__}",
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"--project-dir",
|
|
66
|
+
"-d",
|
|
67
|
+
default=CDW,
|
|
68
|
+
type=dir_type,
|
|
69
|
+
help="The project directory. (default: current directory)",
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--config-file",
|
|
73
|
+
"-c",
|
|
74
|
+
default=CDW / "pyproject.toml",
|
|
75
|
+
type=argparse.FileType("rb"),
|
|
76
|
+
help="Path to `pyproject.toml` file. (default: ./pyproject.toml)",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--generator",
|
|
80
|
+
"-g",
|
|
81
|
+
default=ALL_GENERATORS,
|
|
82
|
+
action=GeneratorAction,
|
|
83
|
+
help=f"The generator class or object to use. (default: [{', '.join(g.__name__ for g in ALL_GENERATORS)}])",
|
|
84
|
+
)
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"settings",
|
|
87
|
+
nargs="*",
|
|
88
|
+
action=SettingsAction,
|
|
89
|
+
help="The settings classes or objects to export.",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def main(parse_args: Sequence[str] | None = None): # noqa: D103
|
|
94
|
+
args = parser.parse_args(parse_args)
|
|
95
|
+
s = Settings.from_pyproject(args.config_file)
|
|
96
|
+
|
|
97
|
+
s.project_dir = args.project_dir
|
|
98
|
+
s.generators = args.generator
|
|
99
|
+
settings = s.default_settings or args.settings
|
|
100
|
+
if not settings:
|
|
101
|
+
parser.exit(0, parser.format_help())
|
|
102
|
+
|
|
103
|
+
result = Exporter(s).run_all(*settings)
|
|
104
|
+
if result:
|
|
105
|
+
files = "\n".join(f"- {r}" for r in result)
|
|
106
|
+
parser.exit(0, f"Generated files ({len(result)}): \n{files}\n")
|
|
107
|
+
parser.exit(0, "No files generated.\n")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
main()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
from pydantic import BeforeValidator, SecretStr
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
__all__ = (
|
|
8
|
+
"StrAsPath",
|
|
9
|
+
"FIELD_TYPE_MAP",
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
StrAsPath = Annotated[Path, BeforeValidator(lambda v: Path(v))]
|
|
13
|
+
|
|
14
|
+
FIELD_TYPE_MAP = {
|
|
15
|
+
SecretStr: "string",
|
|
16
|
+
str: "string",
|
|
17
|
+
int: "integer",
|
|
18
|
+
float: "number",
|
|
19
|
+
bool: "boolean",
|
|
20
|
+
list: "array",
|
|
21
|
+
dict: "object",
|
|
22
|
+
None: "null",
|
|
23
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from pydantic_settings import BaseSettings
|
|
4
|
+
|
|
5
|
+
from pydantic_settings_export.generators import AbstractGenerator
|
|
6
|
+
from pydantic_settings_export.models import SettingsInfoModel
|
|
7
|
+
from pydantic_settings_export.settings import Settings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__ = ("Exporter",)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Exporter:
|
|
14
|
+
"""The exporter for pydantic settings."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
settings: Settings | None = None,
|
|
19
|
+
generators: list[type[AbstractGenerator]] | None = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
self.settings: Settings = settings or Settings.from_pyproject()
|
|
22
|
+
self.generators: list[type[AbstractGenerator]] = settings.generators if generators is None else generators
|
|
23
|
+
|
|
24
|
+
def run_all(self, *settings: BaseSettings | type[BaseSettings]) -> list[Path]:
|
|
25
|
+
"""Run all generators for the given settings.
|
|
26
|
+
|
|
27
|
+
:param settings: The settings to generate documentation for.
|
|
28
|
+
:return: The paths to generated documentations.
|
|
29
|
+
"""
|
|
30
|
+
settings_infos: list[SettingsInfoModel] = [
|
|
31
|
+
SettingsInfoModel.from_settings_model(s, self.settings) for s in settings
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
return [
|
|
35
|
+
# Run all generators for each setting info
|
|
36
|
+
path
|
|
37
|
+
for generator in self.generators
|
|
38
|
+
for path in generator.run(self.settings, *settings_infos)
|
|
39
|
+
]
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from pydantic_settings_export.models import SettingsInfoModel
|
|
5
|
+
from pydantic_settings_export.settings import Settings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
__all__ = ("AbstractGenerator",)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AbstractGenerator(ABC):
|
|
12
|
+
"""The abstract class for the configuration file generator."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, settings: Settings) -> None:
|
|
15
|
+
"""Initialize the AbstractGenerator.
|
|
16
|
+
|
|
17
|
+
:param settings: The settings for the generator.
|
|
18
|
+
"""
|
|
19
|
+
self.settings = settings
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def generate_single(self, settings_info: SettingsInfoModel, level: int = 1) -> str:
|
|
23
|
+
"""Generate the configuration file content.
|
|
24
|
+
|
|
25
|
+
:param settings_info: The settings class to generate documentation for.
|
|
26
|
+
:param level: The level of nesting. Used for indentation.
|
|
27
|
+
:return: The generated documentation.
|
|
28
|
+
"""
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
|
|
31
|
+
def generate(self, *settings_infos: SettingsInfoModel) -> str:
|
|
32
|
+
"""Generate the configuration file content.
|
|
33
|
+
|
|
34
|
+
:param settings_infos: The settings info classes to generate documentation for.
|
|
35
|
+
:return: The generated documentation.
|
|
36
|
+
"""
|
|
37
|
+
return "\n\n".join(self.generate_single(s).strip() for s in settings_infos).strip() + "\n"
|
|
38
|
+
|
|
39
|
+
@abstractmethod
|
|
40
|
+
def write_to_files(self, generated_result: str) -> list[Path]:
|
|
41
|
+
"""Write the generated content to files.
|
|
42
|
+
|
|
43
|
+
:param generated_result: The result to write to files.
|
|
44
|
+
:return: The list of file paths written to.
|
|
45
|
+
"""
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def run(cls, settings: Settings, settings_info: SettingsInfoModel) -> list[Path]:
|
|
50
|
+
"""Run the generator.
|
|
51
|
+
|
|
52
|
+
:param settings: The settings for the generator.
|
|
53
|
+
:param settings_info: The settings info to generate documentation for.
|
|
54
|
+
:return: The list of file paths written to.
|
|
55
|
+
"""
|
|
56
|
+
generator = cls(settings)
|
|
57
|
+
result = generator.generate(settings_info)
|
|
58
|
+
return generator.write_to_files(result)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from pydantic_settings_export.models import SettingsInfoModel
|
|
4
|
+
|
|
5
|
+
from .abstract import AbstractGenerator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
__all__ = ("DotEnvGenerator",)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DotEnvGenerator(AbstractGenerator):
|
|
12
|
+
"""The .env example generator."""
|
|
13
|
+
|
|
14
|
+
def write_to_files(self, generated_result: str) -> list[Path]:
|
|
15
|
+
"""Write the generated content to files.
|
|
16
|
+
|
|
17
|
+
:param generated_result: The result to write to files.
|
|
18
|
+
:return: The list of file paths written to.
|
|
19
|
+
"""
|
|
20
|
+
file_path = self.settings.project_dir / self.settings.dotenv.name
|
|
21
|
+
file_path.write_text(generated_result)
|
|
22
|
+
return [file_path]
|
|
23
|
+
|
|
24
|
+
def generate_single(self, settings_info: SettingsInfoModel, level=1) -> str:
|
|
25
|
+
"""Generate a .env example for a pydantic settings class.
|
|
26
|
+
|
|
27
|
+
:param level: The level of nesting. Used for indentation.
|
|
28
|
+
:param settings_info: The settings class to generate a .env example for.
|
|
29
|
+
:return: The generated .env example.
|
|
30
|
+
"""
|
|
31
|
+
result = f"### {settings_info.name}\n\n"
|
|
32
|
+
for field in settings_info.fields:
|
|
33
|
+
field_name = f"{settings_info.env_prefix}{field.name.upper()}"
|
|
34
|
+
field_string = f"{field_name}={field.default}\n"
|
|
35
|
+
|
|
36
|
+
if not field.is_required:
|
|
37
|
+
field_string = f"# {field_string}"
|
|
38
|
+
result += field_string
|
|
39
|
+
|
|
40
|
+
result = result.strip() + "\n\n"
|
|
41
|
+
|
|
42
|
+
for child in settings_info.child_settings:
|
|
43
|
+
result += self.generate_single(child)
|
|
44
|
+
|
|
45
|
+
return result
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import TypedDict
|
|
3
|
+
|
|
4
|
+
from pydantic_settings_export.models import SettingsInfoModel
|
|
5
|
+
from pydantic_settings_export.utils import make_pretty_md_table_from_dict
|
|
6
|
+
|
|
7
|
+
from .abstract import AbstractGenerator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__ = ("MarkdownGenerator",)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TableRowDict(TypedDict):
|
|
14
|
+
"""The table row dictionary."""
|
|
15
|
+
|
|
16
|
+
Name: str
|
|
17
|
+
Type: str
|
|
18
|
+
Default: str
|
|
19
|
+
Description: str
|
|
20
|
+
Example: str | None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class MarkdownGenerator(AbstractGenerator):
|
|
24
|
+
"""The Markdown configuration file generator."""
|
|
25
|
+
|
|
26
|
+
def generate_single(self, settings_info: SettingsInfoModel, level: int = 1) -> str: # noqa: C901
|
|
27
|
+
"""Generate Markdown documentation for a pydantic settings class.
|
|
28
|
+
|
|
29
|
+
:param settings_info: The settings class to generate documentation for.
|
|
30
|
+
:param level: The level of nesting. Used for indentation.
|
|
31
|
+
:return: The generated documentation.
|
|
32
|
+
"""
|
|
33
|
+
docs = ("\n\n" + settings_info.docs).rstrip()
|
|
34
|
+
|
|
35
|
+
# Generate header
|
|
36
|
+
result = f"{'#' * level} {settings_info.name}{docs}\n\n"
|
|
37
|
+
|
|
38
|
+
# Add environment prefix if it exists
|
|
39
|
+
if settings_info.env_prefix:
|
|
40
|
+
result += f"**Environment Prefix**: `{settings_info.env_prefix}`\n\n"
|
|
41
|
+
|
|
42
|
+
# Generate fields
|
|
43
|
+
rows: list[TableRowDict] = [
|
|
44
|
+
TableRowDict(
|
|
45
|
+
Name=f"`{settings_info.env_prefix}{field.name.upper()}`",
|
|
46
|
+
Type=f"`{field.type}`",
|
|
47
|
+
Default=f"`{field.default}`" if not field.is_required else "*required*",
|
|
48
|
+
Description=field.description,
|
|
49
|
+
Example=f"`{field.example}`" if field.example else None,
|
|
50
|
+
)
|
|
51
|
+
for field in settings_info.fields
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
if rows:
|
|
55
|
+
result += make_pretty_md_table_from_dict(rows) + "\n\n"
|
|
56
|
+
|
|
57
|
+
# Generate child settings
|
|
58
|
+
result += "\n\n".join(self.generate_single(child, level + 1).strip() for child in settings_info.child_settings)
|
|
59
|
+
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
def generate(self, *settings_infos: SettingsInfoModel) -> str:
|
|
63
|
+
"""Generate Markdown documentation for a pydantic settings class.
|
|
64
|
+
|
|
65
|
+
:param settings_infos: The settings class to generate documentation for.
|
|
66
|
+
:return: The generated documentation.
|
|
67
|
+
"""
|
|
68
|
+
return (
|
|
69
|
+
"# Configuration\n\n"
|
|
70
|
+
"Here you can find all available configuration options using ENV variables.\n\n"
|
|
71
|
+
+ "\n\n".join(self.generate_single(s, 2).strip() for s in settings_infos)
|
|
72
|
+
).strip() + "\n"
|
|
73
|
+
|
|
74
|
+
def write_to_files(self, generated_result: str) -> list[Path]:
|
|
75
|
+
"""Write the generated content to files.
|
|
76
|
+
|
|
77
|
+
:param generated_result: The result to write to files.
|
|
78
|
+
:return: The list of file paths written to.
|
|
79
|
+
"""
|
|
80
|
+
file_paths = []
|
|
81
|
+
for d in self.settings.markdown.save_dirs:
|
|
82
|
+
d = d.absolute().resolve()
|
|
83
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
84
|
+
p = d / self.settings.markdown.name
|
|
85
|
+
file_paths.append(p)
|
|
86
|
+
p.write_text(generated_result)
|
|
87
|
+
return file_paths
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from inspect import getdoc, isclass
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from types import UnionType
|
|
4
|
+
from typing import Self
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field, TypeAdapter
|
|
7
|
+
from pydantic.fields import FieldInfo
|
|
8
|
+
from pydantic_core import PydanticSerializationError, PydanticUndefined
|
|
9
|
+
from pydantic_settings import BaseSettings
|
|
10
|
+
|
|
11
|
+
from pydantic_settings_export.constants import FIELD_TYPE_MAP
|
|
12
|
+
from pydantic_settings_export.settings import Settings
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
__all__ = (
|
|
16
|
+
"FieldInfoModel",
|
|
17
|
+
"SettingsInfoModel",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class FieldInfoModel(BaseModel):
|
|
22
|
+
"""Info about the field of the settings model."""
|
|
23
|
+
|
|
24
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
25
|
+
|
|
26
|
+
name: str = Field(..., description="The name of the field.")
|
|
27
|
+
type: str = Field(..., description="The type of the field.")
|
|
28
|
+
default: str | None = Field(
|
|
29
|
+
None,
|
|
30
|
+
description="The default value of the field.",
|
|
31
|
+
validate_default=False,
|
|
32
|
+
)
|
|
33
|
+
description: str | None = Field(None, description="The description of the field.")
|
|
34
|
+
example: str | None = Field(None, description="The example of the field.")
|
|
35
|
+
alias: str | None = Field(None, description="The alias of the field.")
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def is_required(self) -> bool:
|
|
39
|
+
"""Check if the field is required."""
|
|
40
|
+
return self.default is PydanticUndefined
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def create_default(field: FieldInfo, global_settings: Settings | None = None) -> str | None:
|
|
44
|
+
"""Make default value for the field.
|
|
45
|
+
|
|
46
|
+
:param field: The field info to generate default value for.
|
|
47
|
+
:param global_settings: The global settings.
|
|
48
|
+
:return: The default value for the field as string, or None if there is no default value.
|
|
49
|
+
"""
|
|
50
|
+
default: object | PydanticUndefined = field.default
|
|
51
|
+
|
|
52
|
+
if default is PydanticUndefined and field.default_factory:
|
|
53
|
+
default = field.default_factory()
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
# if we need to replace absolute paths
|
|
57
|
+
global_settings
|
|
58
|
+
and global_settings.relative_to.replace_abs_paths
|
|
59
|
+
# Check if default is a Path and is absolute
|
|
60
|
+
and isinstance(default, Path)
|
|
61
|
+
and default.is_absolute()
|
|
62
|
+
):
|
|
63
|
+
# Make the default path relative to the global_settings
|
|
64
|
+
default = Path(global_settings.relative_to.alias) / default.relative_to(global_settings.project_dir)
|
|
65
|
+
|
|
66
|
+
if default is PydanticUndefined:
|
|
67
|
+
return None
|
|
68
|
+
try:
|
|
69
|
+
return TypeAdapter(field.annotation).dump_json(default).decode()
|
|
70
|
+
except PydanticSerializationError:
|
|
71
|
+
return str(default)
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_settings_field(
|
|
75
|
+
cls,
|
|
76
|
+
name: str,
|
|
77
|
+
field: FieldInfo,
|
|
78
|
+
global_settings: Settings | None = None,
|
|
79
|
+
) -> Self:
|
|
80
|
+
"""Generate FieldInfoModel using name and field.
|
|
81
|
+
|
|
82
|
+
:param name: The name of the field.
|
|
83
|
+
:param field: The field info to generate FieldInfoModel from.
|
|
84
|
+
:param global_settings: The global settings.
|
|
85
|
+
:return: Instance of FieldInfoModel.
|
|
86
|
+
"""
|
|
87
|
+
# Parse the annotation of the field
|
|
88
|
+
annotation = field.annotation
|
|
89
|
+
|
|
90
|
+
if isinstance(annotation, UnionType):
|
|
91
|
+
args = list(filter(bool, getattr(annotation, "__args__", [])))
|
|
92
|
+
annotation = args[0] if args else None
|
|
93
|
+
|
|
94
|
+
# Get the name from the alias if it exists
|
|
95
|
+
name: str = field.alias or name
|
|
96
|
+
# Get the type from the FIELD_TYPE_MAP if it exists
|
|
97
|
+
type_: str = FIELD_TYPE_MAP.get(annotation, annotation.__name__ if annotation else "any")
|
|
98
|
+
# Get the default value from the field if it exists
|
|
99
|
+
default = cls.create_default(field, global_settings)
|
|
100
|
+
# Get the description from the field if it exists
|
|
101
|
+
description: str | None = field.description or None
|
|
102
|
+
# Get the example from the field if it exists
|
|
103
|
+
example: str | None = str(field.examples[0]) if field.examples else default
|
|
104
|
+
|
|
105
|
+
return cls(
|
|
106
|
+
name=name,
|
|
107
|
+
type=type_,
|
|
108
|
+
default=default,
|
|
109
|
+
description=description,
|
|
110
|
+
example=example,
|
|
111
|
+
alias=field.alias,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class SettingsInfoModel(BaseModel):
|
|
116
|
+
"""Info about the settings model."""
|
|
117
|
+
|
|
118
|
+
name: str = Field(..., description="The name of the settings model.")
|
|
119
|
+
docs: str = Field("", description="The documentation of the settings model.")
|
|
120
|
+
env_prefix: str = Field("", description="The prefix of the environment variables.")
|
|
121
|
+
fields: list[FieldInfoModel] = Field(default_factory=list, description="The fields of the settings model.")
|
|
122
|
+
child_settings: list["SettingsInfoModel"] = Field(
|
|
123
|
+
default_factory=list, description="The child settings of the settings model."
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def from_settings_model(
|
|
128
|
+
cls,
|
|
129
|
+
settings: BaseSettings | type[BaseSettings],
|
|
130
|
+
global_settings: Settings | None = None,
|
|
131
|
+
) -> Self:
|
|
132
|
+
"""Generate SettingsInfoModel using a settings model.
|
|
133
|
+
|
|
134
|
+
:param settings: The settings model to generate SettingsInfoModel from.
|
|
135
|
+
:param global_settings: The global settings.
|
|
136
|
+
:return: Instance of SettingsInfoModel.
|
|
137
|
+
"""
|
|
138
|
+
conf = settings.model_config
|
|
139
|
+
fields_info = settings.model_fields
|
|
140
|
+
|
|
141
|
+
child_settings = []
|
|
142
|
+
fields = []
|
|
143
|
+
for name, field_info in fields_info.items():
|
|
144
|
+
if global_settings and global_settings.respect_exclude and field_info.exclude:
|
|
145
|
+
continue
|
|
146
|
+
annotation = field_info.annotation
|
|
147
|
+
if isclass(annotation) and issubclass(annotation, BaseSettings):
|
|
148
|
+
child_settings.append(cls.from_settings_model(annotation, global_settings=global_settings))
|
|
149
|
+
continue
|
|
150
|
+
fields.append(FieldInfoModel.from_settings_field(name, field_info, global_settings))
|
|
151
|
+
|
|
152
|
+
return cls(
|
|
153
|
+
name=(
|
|
154
|
+
# Get the title from the settings model if it exists
|
|
155
|
+
conf.get("title", None)
|
|
156
|
+
# Otherwise, get the name from the settings model if it exists
|
|
157
|
+
or getattr(settings, "__name__", None)
|
|
158
|
+
# Otherwise, get the class name from the settings model
|
|
159
|
+
or str(settings.__class__.__name__)
|
|
160
|
+
),
|
|
161
|
+
docs=(getdoc(settings) or "").strip(),
|
|
162
|
+
env_prefix=conf.get("env_prefix", ""),
|
|
163
|
+
fields=fields,
|
|
164
|
+
child_settings=child_settings,
|
|
165
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import TYPE_CHECKING, Self
|
|
3
|
+
|
|
4
|
+
from pydantic import Field, ImportString
|
|
5
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
6
|
+
|
|
7
|
+
from pydantic_settings_export.constants import StrAsPath
|
|
8
|
+
from pydantic_settings_export.utils import get_config_from_pyproject_toml
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from pydantic_settings_export.generators.abstract import AbstractGenerator # noqa: F401
|
|
13
|
+
|
|
14
|
+
__all__ = (
|
|
15
|
+
"MarkdownSettings",
|
|
16
|
+
"DotEnvSettings",
|
|
17
|
+
"RelativeToSettings",
|
|
18
|
+
"Settings",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RelativeToSettings(BaseSettings):
|
|
23
|
+
"""Settings for the relative directory."""
|
|
24
|
+
|
|
25
|
+
model_config = SettingsConfigDict(
|
|
26
|
+
title="Relative Directory Settings",
|
|
27
|
+
env_prefix="RELATIVE_TO_",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
replace_abs_paths: bool = Field(True, description="Replace absolute paths with relative path to project root.")
|
|
31
|
+
alias: str = Field("<project_dir>", description="The alias for the relative directory.")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MarkdownSettings(BaseSettings):
|
|
35
|
+
"""Settings for the markdown file."""
|
|
36
|
+
|
|
37
|
+
model_config = SettingsConfigDict(
|
|
38
|
+
title="Configuration File Settings",
|
|
39
|
+
env_prefix="CONFIG_FILE_",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
enabled: bool = Field(True, description="Enable the configuration file generation.")
|
|
43
|
+
name: str = Field("Configuration.md", description="The name of the configuration file.")
|
|
44
|
+
|
|
45
|
+
save_dirs: list[StrAsPath] = Field(
|
|
46
|
+
default_factory=list, description="The directories to save configuration files to."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def __bool__(self) -> bool:
|
|
50
|
+
"""Check if the configuration file is set."""
|
|
51
|
+
return self.enabled and bool(self.save_dirs)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DotEnvSettings(BaseSettings):
|
|
55
|
+
"""Settings for the .env file."""
|
|
56
|
+
|
|
57
|
+
model_config = SettingsConfigDict(
|
|
58
|
+
title=".env File Settings",
|
|
59
|
+
env_prefix="DOTENV_",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
name: str = Field(".env.example", description="The name of the .env file.")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Settings(BaseSettings):
|
|
66
|
+
"""Global settings for pydantic_settings_export."""
|
|
67
|
+
|
|
68
|
+
model_config = SettingsConfigDict(
|
|
69
|
+
title="Global Settings",
|
|
70
|
+
env_prefix="PYDANTIC_SETTINGS_EXPORT_",
|
|
71
|
+
plugin_settings={
|
|
72
|
+
"pyproject_toml": {
|
|
73
|
+
"package_name": "pydantic_settings_export",
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
default_settings: list[ImportString] = Field(
|
|
79
|
+
default_factory=list,
|
|
80
|
+
description="The default settings to use. The settings are applied in the order they are listed.",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
project_dir: Path = Field(
|
|
84
|
+
Path.cwd(),
|
|
85
|
+
description="The project directory. Used for relative paths in the configuration file and .env file.",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
relative_to: RelativeToSettings = Field(
|
|
89
|
+
default_factory=RelativeToSettings,
|
|
90
|
+
description="The relative directory settings.",
|
|
91
|
+
)
|
|
92
|
+
markdown: MarkdownSettings = Field(
|
|
93
|
+
default_factory=MarkdownSettings,
|
|
94
|
+
description="The configuration of markdown file settings.",
|
|
95
|
+
)
|
|
96
|
+
dotenv: DotEnvSettings = Field(
|
|
97
|
+
default_factory=DotEnvSettings,
|
|
98
|
+
description="The .env file settings.",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
respect_exclude: bool = Field(
|
|
102
|
+
True,
|
|
103
|
+
description="Respect the exclude attribute in the fields.",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
generators: list = Field( # type: list[type[AbstractGenerator]]
|
|
107
|
+
default_factory=list,
|
|
108
|
+
description="The list of generators to use.",
|
|
109
|
+
exclude=True,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def from_pyproject(cls, base_path: Path | None = None) -> Self:
|
|
114
|
+
"""Load settings from the pyproject.toml file.
|
|
115
|
+
|
|
116
|
+
:param base_path: The base path to search for the pyproject.toml file, or this file itself.
|
|
117
|
+
The current working directory is used by default.
|
|
118
|
+
:return: The loaded settings.
|
|
119
|
+
"""
|
|
120
|
+
config = get_config_from_pyproject_toml(cls, base_path)
|
|
121
|
+
config.setdefault("project_dir", str(base_path.parent))
|
|
122
|
+
return cls(**config)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import importlib
|
|
3
|
+
import sys
|
|
4
|
+
import tomllib
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__ = (
|
|
11
|
+
"find_pyproject_toml",
|
|
12
|
+
"make_pretty_md_table",
|
|
13
|
+
"make_pretty_md_table_from_dict",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
from pydantic_settings import BaseSettings
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def make_pretty_md_table(header: list[str], rows: list[list[str]]) -> str: # noqa: C901
|
|
20
|
+
"""Make a pretty Markdown table with column alignment.
|
|
21
|
+
|
|
22
|
+
:param header: The header of the table.
|
|
23
|
+
:param rows: The rows of the table.
|
|
24
|
+
:return: The prettied Markdown table.
|
|
25
|
+
"""
|
|
26
|
+
col_sizes = [len(h) for h in header]
|
|
27
|
+
for row in rows:
|
|
28
|
+
for i, cell in enumerate(row):
|
|
29
|
+
if cell is None:
|
|
30
|
+
cell = ""
|
|
31
|
+
col_sizes[i] = max(col_sizes[i], len(cell))
|
|
32
|
+
|
|
33
|
+
result = "|"
|
|
34
|
+
for i, h in enumerate(header):
|
|
35
|
+
result += f" {h}{' ' * (col_sizes[i] - len(h))} |"
|
|
36
|
+
result += "\n|"
|
|
37
|
+
for i, _ in enumerate(header):
|
|
38
|
+
result += f"{'-' * (col_sizes[i] + 2)}|"
|
|
39
|
+
for row in rows:
|
|
40
|
+
result += "\n|"
|
|
41
|
+
for i, cell in enumerate(row):
|
|
42
|
+
if cell is None:
|
|
43
|
+
cell = ""
|
|
44
|
+
result += f" {cell}{' ' * (col_sizes[i] - len(cell))} |"
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def make_pretty_md_table_from_dict(data: list[dict[str, str | None]]) -> str:
|
|
49
|
+
"""Make a pretty Markdown table with column alignment from a list of dictionaries.
|
|
50
|
+
|
|
51
|
+
:param data: The rows of the table as dictionaries.
|
|
52
|
+
:return: The prettied Markdown table.
|
|
53
|
+
"""
|
|
54
|
+
# Save unique keys from all rows and save order
|
|
55
|
+
header: list[str] = list(
|
|
56
|
+
{
|
|
57
|
+
# We need only key
|
|
58
|
+
key: 0
|
|
59
|
+
for row in data
|
|
60
|
+
for key in row.keys()
|
|
61
|
+
}.keys(),
|
|
62
|
+
)
|
|
63
|
+
rows = [[row.get(key, None) or "" for key in header] for row in data]
|
|
64
|
+
return make_pretty_md_table(header, rows)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def find_pyproject_toml(search_from: Path | None = None) -> Path | None:
|
|
68
|
+
"""Find the pyproject.toml file in the current working directory or its parents.
|
|
69
|
+
|
|
70
|
+
:param search_from: The directory to start searching from.
|
|
71
|
+
:return: The path to the pyproject.toml file or None if it wasn't found.
|
|
72
|
+
"""
|
|
73
|
+
if not search_from:
|
|
74
|
+
search_from = Path.cwd()
|
|
75
|
+
for parent in (search_from, *search_from.parents):
|
|
76
|
+
pyproject_toml = parent / "pyproject.toml"
|
|
77
|
+
if pyproject_toml.is_file():
|
|
78
|
+
return pyproject_toml
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def get_tool_name(settings: type[BaseSettings]) -> str | None:
|
|
83
|
+
"""Get the tool name from the settings.
|
|
84
|
+
|
|
85
|
+
:param settings: The settings class to get the tool name from.
|
|
86
|
+
:return: The tool name.
|
|
87
|
+
"""
|
|
88
|
+
return settings.model_config.get("plugin_settings", {}).get("pyproject_toml", {}).get("package_name", None)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_config_from_pyproject_toml(settings: type[BaseSettings], base_path: Path | None = None) -> dict:
|
|
92
|
+
"""Get the configuration from the pyproject.toml file.
|
|
93
|
+
|
|
94
|
+
:param base_path: The base path to search for the pyproject.toml file, or this file itself.
|
|
95
|
+
The current working directory is used by default.
|
|
96
|
+
:param settings: The settings class to create the settings from.
|
|
97
|
+
:return: The created settings.
|
|
98
|
+
"""
|
|
99
|
+
tool_name = get_tool_name(settings)
|
|
100
|
+
|
|
101
|
+
if not tool_name:
|
|
102
|
+
raise ValueError("The tool name is not set in the settings.")
|
|
103
|
+
|
|
104
|
+
if not base_path:
|
|
105
|
+
base_path = Path.cwd()
|
|
106
|
+
|
|
107
|
+
if not base_path.is_file():
|
|
108
|
+
base_path = find_pyproject_toml(base_path)
|
|
109
|
+
|
|
110
|
+
if not base_path:
|
|
111
|
+
raise FileNotFoundError("The pyproject.toml file was not found.")
|
|
112
|
+
|
|
113
|
+
with open(base_path, "rb") as file:
|
|
114
|
+
data = tomllib.load(file)
|
|
115
|
+
|
|
116
|
+
return data.get("tool", {}).get(tool_name, {})
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class ObjectImportAction(argparse.Action):
|
|
120
|
+
"""Import the object from the module."""
|
|
121
|
+
|
|
122
|
+
@staticmethod
|
|
123
|
+
def callback(obj: Any) -> Any:
|
|
124
|
+
"""Check if the object is a settings class."""
|
|
125
|
+
return obj
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def import_obj(value: str) -> Any:
|
|
129
|
+
"""Import the object from the module.
|
|
130
|
+
|
|
131
|
+
:param value: The value in the format 'module:class'.
|
|
132
|
+
:raise ValueError: If the value is not in the format 'module:class'.
|
|
133
|
+
:raise ValueError: If the class is not in the module.
|
|
134
|
+
:raise ModuleNotFoundError: If the module is not found.
|
|
135
|
+
:return: The imported object.
|
|
136
|
+
"""
|
|
137
|
+
try:
|
|
138
|
+
module_name, class_name = value.rsplit(":", 1)
|
|
139
|
+
except ValueError:
|
|
140
|
+
raise ValueError(f"The {value!r} is not in the format 'module:class'.") from None
|
|
141
|
+
|
|
142
|
+
module = importlib.import_module(module_name)
|
|
143
|
+
|
|
144
|
+
obj = getattr(module, class_name, None)
|
|
145
|
+
if obj is None:
|
|
146
|
+
raise ValueError(f"The {class_name!r} is not in the module {module_name!r}.")
|
|
147
|
+
return obj
|
|
148
|
+
|
|
149
|
+
def __call__(
|
|
150
|
+
self,
|
|
151
|
+
parser: argparse.ArgumentParser,
|
|
152
|
+
namespace: argparse.Namespace,
|
|
153
|
+
values: list[str],
|
|
154
|
+
option_string: str | None = None,
|
|
155
|
+
) -> None:
|
|
156
|
+
"""Import the object from the module."""
|
|
157
|
+
# Add the project directory to the sys.path
|
|
158
|
+
sys.path.insert(0, str(namespace.project_dir))
|
|
159
|
+
importlib.invalidate_caches()
|
|
160
|
+
|
|
161
|
+
if isinstance(values, str):
|
|
162
|
+
values = [values]
|
|
163
|
+
|
|
164
|
+
result = getattr(namespace, self.dest, [])
|
|
165
|
+
|
|
166
|
+
# Reset the default value
|
|
167
|
+
if result == self.default:
|
|
168
|
+
result = []
|
|
169
|
+
|
|
170
|
+
for value in values:
|
|
171
|
+
try:
|
|
172
|
+
result.append(self.callback(self.import_obj(value)))
|
|
173
|
+
except (ValueError, ModuleNotFoundError) as e:
|
|
174
|
+
parser.print_usage(sys.stderr)
|
|
175
|
+
parser.exit(2, f"{parser.prog}: error: {argparse.ArgumentError(self, str(e))}\n")
|
|
176
|
+
|
|
177
|
+
setattr(namespace, self.dest, result)
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "pydantic-settings-export"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Export your Pydantic settings to a Markdown and .env.example files!"
|
|
5
|
+
authors = ["Jag_k <jag-k@users.noreply.github.com>"]
|
|
6
|
+
classifiers = [
|
|
7
|
+
"License :: OSI Approved :: MIT License",
|
|
8
|
+
"Operating System :: OS Independent",
|
|
9
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
10
|
+
"Topic :: Software Development :: Code Generators",
|
|
11
|
+
"Topic :: Documentation",
|
|
12
|
+
"Topic :: Utilities",
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Programming Language :: Python",
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
|
|
19
|
+
]
|
|
20
|
+
readme = "README.md"
|
|
21
|
+
license = "MIT"
|
|
22
|
+
repository = "https://github.com/jag-k/pydantic-settings-export"
|
|
23
|
+
homepage = "https://github.com/jag-k/pydantic-settings-export#readme"
|
|
24
|
+
packages = [
|
|
25
|
+
{ include = "pydantic_settings_export", from = "." },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[tool.poetry.scripts]
|
|
29
|
+
pydantic-settings-export = "pydantic_settings_export.cli:main"
|
|
30
|
+
|
|
31
|
+
[tool.poetry.dependencies]
|
|
32
|
+
python = "^3.11"
|
|
33
|
+
pydantic = "^2"
|
|
34
|
+
pydantic-settings = "^2"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
[tool.poetry.group.dev.dependencies]
|
|
38
|
+
ruff = "*"
|
|
39
|
+
ruff-lsp = "*"
|
|
40
|
+
pre-commit = "*"
|
|
41
|
+
ssort = "^0.13.0"
|
|
42
|
+
|
|
43
|
+
[tool.poetry-dynamic-versioning]
|
|
44
|
+
enable = false
|
|
45
|
+
vcs = "git"
|
|
46
|
+
style = "semver"
|
|
47
|
+
|
|
48
|
+
[tool.poetry-dynamic-versioning.substitution]
|
|
49
|
+
files = [
|
|
50
|
+
"pydantic_settings_export/version.py",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[tool.poetry-dynamic-versioning.files."pydantic_settings_export/version.py"]
|
|
54
|
+
persistent-substitution = true
|
|
55
|
+
initial-content = """
|
|
56
|
+
# These version placeholders will be replaced later during substitution.
|
|
57
|
+
__version__ = "0.0.0"
|
|
58
|
+
__version_tuple__ = (0, 0, 0)
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
[build-system]
|
|
62
|
+
requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"]
|
|
63
|
+
build-backend = "poetry_dynamic_versioning.backend"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# https://docs.astral.sh/ruff/
|
|
67
|
+
[tool.ruff]
|
|
68
|
+
target-version = "py311"
|
|
69
|
+
line-length = 120
|
|
70
|
+
extend-exclude = [
|
|
71
|
+
".idea",
|
|
72
|
+
".vscode",
|
|
73
|
+
".fleet",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
# https://docs.astral.sh/ruff/settings/#lint
|
|
77
|
+
[tool.ruff.lint]
|
|
78
|
+
ignore-init-module-imports = true
|
|
79
|
+
select = [
|
|
80
|
+
'F', # flake8
|
|
81
|
+
'I', # isort
|
|
82
|
+
'B', # flake8-bugbear
|
|
83
|
+
'D', # pydocstyle
|
|
84
|
+
'W', # pycodestyle (warnings)
|
|
85
|
+
'E', # pycodestyle (errors)
|
|
86
|
+
'N', # pep8-naming
|
|
87
|
+
'PT', # flake8-pytest-style
|
|
88
|
+
'C90', # mccabe
|
|
89
|
+
]
|
|
90
|
+
ignore = [
|
|
91
|
+
'B012', # {name} inside finally blocks cause exceptions to be silenced
|
|
92
|
+
'D100', # Missing docstring in public module
|
|
93
|
+
'D104', # Missing docstring in public package
|
|
94
|
+
'D105', # Missing docstring in magic method
|
|
95
|
+
'D106', # Missing docstring in public nested class
|
|
96
|
+
'D107', # Missing docstring in __init__
|
|
97
|
+
'D203', # 1 blank line required before class docstring
|
|
98
|
+
'D401', # First line of docstring should be in imperative mood: "{first_line}"
|
|
99
|
+
'D404', # First word of the docstring should not be "This"
|
|
100
|
+
'D207', # Docstring is under-indented
|
|
101
|
+
'D208', # Docstring is over-indented
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
# https://docs.astral.sh/ruff/settings/#extend-per-file-ignores
|
|
107
|
+
[tool.ruff.lint.extend-per-file-ignores]
|
|
108
|
+
'__init__.py' = [
|
|
109
|
+
'F401', # {name} imported but unused; consider using importlib.util.find_spec to test for availability
|
|
110
|
+
'F403', # from {name} import * used; unable to detect undefined names
|
|
111
|
+
'F405', # {name} may be undefined, or defined from star imports
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
# https://docs.astral.sh/ruff/settings/#lintpydocstyle
|
|
115
|
+
[tool.ruff.lint.pydocstyle]
|
|
116
|
+
convention = 'pep257'
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# https://docs.astral.sh/ruff/settings/#lintmccabe
|
|
120
|
+
[tool.ruff.lint.mccabe]
|
|
121
|
+
max-complexity = 6
|
|
122
|
+
|
|
123
|
+
# https://docs.astral.sh/ruff/settings/#lintisort
|
|
124
|
+
[tool.ruff.lint.isort]
|
|
125
|
+
section-order = [
|
|
126
|
+
'future',
|
|
127
|
+
'standard-library',
|
|
128
|
+
'third-party',
|
|
129
|
+
'first-party',
|
|
130
|
+
'local-folder',
|
|
131
|
+
]
|
|
132
|
+
known-first-party = ["pydantic_settings_export"]
|
|
133
|
+
lines-after-imports = 2
|
|
134
|
+
lines-between-types = 1
|
|
135
|
+
|
|
136
|
+
# https://github.com/jag-k/pydantic-settings-export
|
|
137
|
+
[tool.pydantic_settings_export]
|
|
138
|
+
project_dir = "."
|
|
139
|
+
default_settings = [
|
|
140
|
+
"pydantic_settings_export.settings:Settings",
|
|
141
|
+
]
|
|
142
|
+
dotenv = {"name" = ".env.example"}
|
|
143
|
+
|
|
144
|
+
[tool.pydantic_settings_export.markdown]
|
|
145
|
+
name = "Configuration.md"
|
|
146
|
+
save_dirs = [
|
|
147
|
+
"docs",
|
|
148
|
+
"wiki",
|
|
149
|
+
]
|