quartopydoc 0.12.0__py3-none-any.whl

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.
Files changed (98) hide show
  1. quartodoc/__init__.py +30 -0
  2. quartodoc/__main__.py +283 -0
  3. quartodoc/_griffe_compat/__init__.py +11 -0
  4. quartodoc/_griffe_compat/_generate_stubs.py +71 -0
  5. quartodoc/_griffe_compat/dataclasses.py +35 -0
  6. quartodoc/_griffe_compat/docstrings.py +38 -0
  7. quartodoc/_griffe_compat/expressions.py +48 -0
  8. quartodoc/_pydantic_compat.py +18 -0
  9. quartodoc/ast.py +391 -0
  10. quartodoc/autosummary.py +911 -0
  11. quartodoc/builder/__init__.py +0 -0
  12. quartodoc/builder/_node.py +17 -0
  13. quartodoc/builder/blueprint.py +589 -0
  14. quartodoc/builder/collect.py +86 -0
  15. quartodoc/builder/utils.py +120 -0
  16. quartodoc/builder/write.py +17 -0
  17. quartodoc/interlinks.py +431 -0
  18. quartodoc/inventory.py +174 -0
  19. quartodoc/layout.py +522 -0
  20. quartodoc/pandoc/__init__.py +7 -0
  21. quartodoc/pandoc/blocks.py +421 -0
  22. quartodoc/pandoc/components.py +76 -0
  23. quartodoc/pandoc/inlines.py +261 -0
  24. quartodoc/parsers.py +8 -0
  25. quartodoc/py.typed +0 -0
  26. quartodoc/qpyd/README.md +78 -0
  27. quartodoc/qpyd/__init__.py +16 -0
  28. quartodoc/qpyd/__main__.py +6 -0
  29. quartodoc/qpyd/clean.py +91 -0
  30. quartodoc/qpyd/cli.py +89 -0
  31. quartodoc/qpyd/config.py +206 -0
  32. quartodoc/qpyd/convert.py +273 -0
  33. quartodoc/qpyd/execute.py +274 -0
  34. quartodoc/qpyd/nb.py +93 -0
  35. quartodoc/qpyd/prerender.py +141 -0
  36. quartodoc/qpyd/quarto_utils.py +351 -0
  37. quartodoc/qpyd/render.py +85 -0
  38. quartodoc/qpyd/scaffold.py +133 -0
  39. quartodoc/qpyd/tests/__init__.py +0 -0
  40. quartodoc/qpyd/tests/conftest.py +15 -0
  41. quartodoc/qpyd/tests/data/nb_fail.qmd +9 -0
  42. quartodoc/qpyd/tests/data/nb_ok.qmd +11 -0
  43. quartodoc/qpyd/tests/data/prose.qmd +5 -0
  44. quartodoc/qpyd/tests/test_cli.py +78 -0
  45. quartodoc/qpyd/tests/test_config.py +39 -0
  46. quartodoc/qpyd/tests/test_convert.py +69 -0
  47. quartodoc/qpyd/tests/test_execute.py +47 -0
  48. quartodoc/qpyd/tests/test_safety.py +163 -0
  49. quartodoc/qpyd/tests/test_variables.py +34 -0
  50. quartodoc/qpyd/variables.py +90 -0
  51. quartodoc/renderers/__init__.py +4 -0
  52. quartodoc/renderers/base.py +119 -0
  53. quartodoc/renderers/md_renderer.py +966 -0
  54. quartodoc/static/styles.css +15 -0
  55. quartodoc/tests/__init__.py +0 -0
  56. quartodoc/tests/__snapshots__/test_builder.ambr +47 -0
  57. quartodoc/tests/__snapshots__/test_renderers.ambr +1306 -0
  58. quartodoc/tests/__snapshots__/test_validation.ambr +39 -0
  59. quartodoc/tests/example.py +26 -0
  60. quartodoc/tests/example_alias_target.py +13 -0
  61. quartodoc/tests/example_alias_target__nested.py +21 -0
  62. quartodoc/tests/example_attribute.py +7 -0
  63. quartodoc/tests/example_class.py +63 -0
  64. quartodoc/tests/example_docstring_full.py +68 -0
  65. quartodoc/tests/example_docstring_styles.py +81 -0
  66. quartodoc/tests/example_dynamic.py +51 -0
  67. quartodoc/tests/example_interlinks/.gitignore +2 -0
  68. quartodoc/tests/example_interlinks/README.md +3 -0
  69. quartodoc/tests/example_interlinks/_inv/other_objects.json +1 -0
  70. quartodoc/tests/example_interlinks/_quarto.yml +10 -0
  71. quartodoc/tests/example_interlinks/interlinks.lua +411 -0
  72. quartodoc/tests/example_interlinks/objects.json +1 -0
  73. quartodoc/tests/example_interlinks/spec.yml +107 -0
  74. quartodoc/tests/example_interlinks/test.md +176 -0
  75. quartodoc/tests/example_interlinks/test.qmd +266 -0
  76. quartodoc/tests/example_signature.py +44 -0
  77. quartodoc/tests/example_star_imports.py +1 -0
  78. quartodoc/tests/example_stubs.py +2 -0
  79. quartodoc/tests/example_stubs.pyi +2 -0
  80. quartodoc/tests/pandoc/test_blocks.py +430 -0
  81. quartodoc/tests/pandoc/test_inlines.py +121 -0
  82. quartodoc/tests/test_ast.py +110 -0
  83. quartodoc/tests/test_basic.py +225 -0
  84. quartodoc/tests/test_builder.py +227 -0
  85. quartodoc/tests/test_builder_blueprint.py +435 -0
  86. quartodoc/tests/test_cli.py +29 -0
  87. quartodoc/tests/test_interlinks.py +117 -0
  88. quartodoc/tests/test_layout.py +59 -0
  89. quartodoc/tests/test_qpyd.py +20 -0
  90. quartodoc/tests/test_renderers.py +500 -0
  91. quartodoc/tests/test_validation.py +57 -0
  92. quartodoc/validation.py +88 -0
  93. quartopydoc-0.12.0.dist-info/METADATA +215 -0
  94. quartopydoc-0.12.0.dist-info/RECORD +98 -0
  95. quartopydoc-0.12.0.dist-info/WHEEL +5 -0
  96. quartopydoc-0.12.0.dist-info/entry_points.txt +4 -0
  97. quartopydoc-0.12.0.dist-info/licenses/LICENSE +21 -0
  98. quartopydoc-0.12.0.dist-info/top_level.txt +1 -0
quartodoc/__init__.py ADDED
@@ -0,0 +1,30 @@
1
+ """
2
+ quartopydoc is a package for building delightful python API documentation.
3
+
4
+ It is a fork of quartodoc by Michael Chow, with formatting customizations and
5
+ extra utilities for working with notebooks.
6
+ """
7
+
8
+ # flake8: noqa
9
+
10
+ from .autosummary import get_function, get_object, Builder
11
+ from .renderers import MdRenderer
12
+ from .inventory import convert_inventory, create_inventory
13
+ from .ast import preview
14
+ from .builder.blueprint import blueprint
15
+ from .builder.collect import collect
16
+ from .layout import Auto
17
+
18
+ __all__ = (
19
+ "Auto",
20
+ "blueprint",
21
+ "collect",
22
+ "convert_inventory",
23
+ "create_inventory",
24
+ "get_object",
25
+ "preview",
26
+ "Builder",
27
+ "BuilderPkgdown",
28
+ "BuilderSinglePage",
29
+ "MdRenderer",
30
+ )
quartodoc/__main__.py ADDED
@@ -0,0 +1,283 @@
1
+ import click
2
+ import contextlib
3
+ import os
4
+ import sys
5
+ import time
6
+ import sphobjinv as soi
7
+ import yaml
8
+ import importlib
9
+ from pathlib import Path
10
+ from watchdog.observers import Observer
11
+ from functools import partial
12
+ from watchdog.events import PatternMatchingEventHandler
13
+ from quartodoc import Builder, convert_inventory
14
+ from ._pydantic_compat import BaseModel
15
+ from .interlinks import inventory_from_url
16
+
17
+
18
+ def get_package_path(package_name):
19
+ """
20
+ Get the path to a package installed in the current environment.
21
+ """
22
+ try:
23
+ lib = importlib.import_module(package_name)
24
+ return lib.__path__[0]
25
+ except ModuleNotFoundError:
26
+ raise ModuleNotFoundError(
27
+ f"Package {package_name} not found. Please install it in your environment."
28
+ )
29
+
30
+
31
+ class FileInfo(BaseModel):
32
+ size: int
33
+ mtime: float
34
+ name: str = ""
35
+
36
+
37
+ class QuartoDocFileChangeHandler(PatternMatchingEventHandler):
38
+ """
39
+ A handler for file changes.
40
+ """
41
+
42
+ # Ignore patterns for the file watcher that are not relevant to the docs
43
+ py_ignore_patterns = [
44
+ "*/__pycache__/*", # These are the compiled python code files which are automatically generated by Python
45
+ "*/.ipynb_checkpoints/*", # This directory is created by Jupyter Notebook for auto-saving notebooks
46
+ "*/.vscode/*", # If you're using Visual Studio Code, it creates this directory to store settings specific to that project.
47
+ "*/.idea/*", # Similar to .vscode/, but for JetBrains IDEs like PyCharm.
48
+ "*/.git/*", # i This directory is created by Git. It's not relevant to the docs.
49
+ "*/venv/*",
50
+ "*/env/*",
51
+ "*/.env/*", # Common names for directories containing a Python virtual environment.
52
+ "*/.pytest_cache/*", # This directory is created when you run Pytest.
53
+ "*/.eggs/*",
54
+ "*/dist/*",
55
+ "*/build/*",
56
+ "*/*.egg-info/*", # These are typically created when building Python packages with setuptools.
57
+ "*.pyo", # These are optimized .pyc files, created when Python is run with the -O flag.
58
+ "*.pyd", # This is the equivalent of a .pyc file, but for C extensions on Windows.
59
+ "*/.mypy_cache/*", # This directory is created when you run mypy.
60
+ ]
61
+
62
+ def __init__(self, callback):
63
+ super().__init__(
64
+ ignore_patterns=self.py_ignore_patterns, ignore_directories=True
65
+ )
66
+ self.callback = callback
67
+ self.old_file_info = FileInfo(size=-1, mtime=-1, name="")
68
+
69
+ def get_file_info(self, path: str) -> FileInfo:
70
+ """
71
+ Get the file size and modification time.
72
+ """
73
+ return FileInfo(
74
+ size=os.stat(path).st_size, mtime=os.stat(path).st_mtime, name=path
75
+ )
76
+
77
+ def is_diff(self, old: FileInfo, new: FileInfo) -> bool:
78
+ """
79
+ Check if a file has changed. Prevents duplicate events from being triggered.
80
+ """
81
+ same_nm = old.name == new.name
82
+ diff_sz = old.size != new.size
83
+ diff_tm = new.mtime - old.mtime # wait 1/4 second before triggering
84
+
85
+ if (
86
+ diff_tm < 0.25
87
+ ): # if consequetive events are less than 1/4th of a second apart, ignore
88
+ return False
89
+ elif same_nm:
90
+ if diff_sz or diff_tm >= 0.25:
91
+ return True
92
+ else:
93
+ return False
94
+ else:
95
+ return True
96
+
97
+ def callback_if_diff(self, event):
98
+ """
99
+ Call the callback if the file has changed.
100
+ """
101
+ new_file_info = self.get_file_info(event.src_path)
102
+ if self.is_diff(self.old_file_info, new_file_info):
103
+ self.print_event(event)
104
+ self.callback()
105
+ self.old_file_info = new_file_info
106
+
107
+ @classmethod
108
+ def print_event(cls, event):
109
+ print(f"Rebuilding docs. Detected: {event.event_type} path : {event.src_path}")
110
+
111
+ def on_modified(self, event):
112
+ self.callback_if_diff(event)
113
+
114
+ def on_created(self, event):
115
+ self.callback_if_diff(event)
116
+
117
+
118
+ def _enable_logs():
119
+ import logging
120
+
121
+ root = logging.getLogger("quartodoc")
122
+ root.setLevel(logging.INFO)
123
+
124
+ handler = logging.StreamHandler(sys.stdout)
125
+ handler.setLevel(logging.DEBUG)
126
+ formatter = logging.Formatter(
127
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
128
+ )
129
+ handler.setFormatter(formatter)
130
+ root.addHandler(handler)
131
+
132
+
133
+ @contextlib.contextmanager
134
+ def chdir(new_dir):
135
+ prev = os.getcwd()
136
+ os.chdir(new_dir)
137
+ try:
138
+ yield new_dir
139
+ finally:
140
+ os.chdir(prev)
141
+
142
+
143
+ @click.group()
144
+ def cli():
145
+ pass
146
+
147
+
148
+ @click.command()
149
+ @click.option(
150
+ "--config",
151
+ default="_quarto.yml",
152
+ help="Change the path to the configuration file. The default is `./_quarto.yml`",
153
+ )
154
+ @click.option(
155
+ "--filter",
156
+ nargs=1,
157
+ default="*",
158
+ help="Specify the filter to select specific files. The default is '*' which selects all files.",
159
+ )
160
+ @click.option(
161
+ "--dry-run",
162
+ is_flag=True,
163
+ default=False,
164
+ help="If set, prevents new documents from being generated.",
165
+ )
166
+ @click.option(
167
+ "--watch",
168
+ is_flag=True,
169
+ default=False,
170
+ help="If set, the command will keep running and watch for changes in the package directory.",
171
+ )
172
+ @click.option("--verbose", is_flag=True, default=False, help="Enable verbose logging.")
173
+ def build(config, filter, dry_run, watch, verbose):
174
+ """
175
+ Generate API docs based on the given configuration file (`./_quarto.yml` by default).
176
+ """
177
+
178
+ cfg_path = Path(config).absolute()
179
+ if not cfg_path.exists():
180
+ raise FileNotFoundError(
181
+ f"Configuration file {cfg_path} not found. Please create one."
182
+ )
183
+ if verbose:
184
+ _enable_logs()
185
+
186
+ # allow users to include files like _renderer.py in their quarto docs folder
187
+ if config != "_quarto.yml":
188
+ sys.path.append(str(Path(config).parent.absolute()))
189
+
190
+ builder = Builder.from_quarto_config(config)
191
+ doc_build = partial(builder.build, filter=filter)
192
+
193
+ if dry_run:
194
+ pass
195
+ else:
196
+ # add current directory to path, and then temporarily switch directories.
197
+ # this supports the case where a user is depending on the current directory
198
+ # for imports
199
+ sys.path.append(os.getcwd())
200
+ with chdir(Path(config).parent):
201
+ if watch:
202
+ pkg_path = get_package_path(builder.package)
203
+ print(f"Watching {pkg_path} for changes...")
204
+ observer = Observer()
205
+ observer._event_queue.maxsize = 1 # the default is 0 which is infinite, and there isn't a way to set this in the constructor
206
+ event_handler = QuartoDocFileChangeHandler(callback=doc_build)
207
+ observer.schedule(event_handler, pkg_path, recursive=True)
208
+ observer.schedule(event_handler, cfg_path, recursive=True)
209
+ observer.start()
210
+ try:
211
+ while True:
212
+ time.sleep(1)
213
+ except KeyboardInterrupt:
214
+ pass
215
+ finally:
216
+ observer.stop()
217
+ observer.join()
218
+ else:
219
+ doc_build()
220
+
221
+
222
+ @click.command(
223
+ short_help="Generate inventory files that the Quarto "
224
+ "`interlink` extension can use to auto-link to other docs."
225
+ )
226
+ @click.argument("config", default="_quarto.yml")
227
+ @click.option("--dry-run", is_flag=True, default=False)
228
+ @click.option("--fast", is_flag=True, default=False)
229
+ def interlinks(config, dry_run, fast):
230
+ """
231
+ Generate inventory files that the Quarto `interlink` extension can use to
232
+ auto-link to other docs.
233
+
234
+ The files are stored in a cache directory, which defaults to _inv.
235
+ The Quarto extension `interlinks` will look for these files in the cache
236
+ and add links to your docs accordingly.
237
+ """
238
+
239
+ # config loading ----
240
+ cfg = yaml.safe_load(open(config))
241
+ interlinks = cfg.get("interlinks", {})
242
+
243
+ p_root = Path(config).parent
244
+
245
+ if not interlinks:
246
+ print("No interlinks field found in your quarto config. Quitting.")
247
+ return
248
+
249
+ # interlinks config settings ----
250
+ cache = p_root / "_inv"
251
+ cfg_fast = interlinks.get("fast", False)
252
+
253
+ fast = cfg_fast or fast
254
+
255
+ for k, v in interlinks["sources"].items():
256
+ # don't include user's own docs (users don't need to specify their own docs in
257
+ # the interlinks config anymore, so this is for backwards compat).
258
+ if v["url"] == "/":
259
+ continue
260
+
261
+ url = v["url"] + v.get("inv", "objects.inv")
262
+
263
+ inv = inventory_from_url(url)
264
+
265
+ p_dst = p_root / cache / f"{k}_objects"
266
+ p_dst.parent.mkdir(exist_ok=True, parents=True)
267
+
268
+ if fast:
269
+ # use sphobjinv to dump inv in txt format
270
+ df = inv.data_file()
271
+ soi.writebytes(p_dst.with_suffix(".txt"), df)
272
+
273
+ else:
274
+ # old behavior of converting to custom json format
275
+ convert_inventory(inv, p_dst.with_suffix(".json"))
276
+
277
+
278
+ cli.add_command(build)
279
+ cli.add_command(interlinks)
280
+
281
+
282
+ if __name__ == "__main__":
283
+ cli()
@@ -0,0 +1,11 @@
1
+ # flake8: noqa
2
+
3
+ from griffe import GriffeLoader
4
+ from griffe import ModulesCollection, LinesCollection
5
+
6
+ from . import dataclasses
7
+ from . import docstrings
8
+ from . import expressions
9
+
10
+ from griffe import Parser, parse, parse_numpy
11
+ from griffe import AliasResolutionError
@@ -0,0 +1,71 @@
1
+ """Generate the submodules which import parts of griffe (e.g. dataclasses.py)
2
+
3
+ This process balances having to import specific objects from griffe, against
4
+ importing everything via the top-level griffe module. It does this by generating
5
+ modules for objects commonly used together in quartodoc.
6
+
7
+ * dataclasses: represent python objects
8
+ * docstrings: represent python docstrings
9
+ * expressions: represent annotation expressions
10
+
11
+ Run using: python -m quartodoc._griffe_compat._generate_stubs
12
+ """
13
+
14
+ import ast
15
+ import black
16
+ import copy
17
+ import griffe
18
+ import inspect
19
+ from pathlib import Path
20
+
21
+
22
+ def fetch_submodule(ast_body, submodule: str):
23
+ ast_imports = [
24
+ obj
25
+ for obj in ast_body
26
+ if isinstance(obj, ast.ImportFrom) and obj.module == submodule
27
+ ]
28
+
29
+ if len(ast_imports) > 1:
30
+ raise Exception(f"Found {len(ast_imports)} imports for {submodule}")
31
+ elif not ast_imports:
32
+ raise Exception(f"Could not find import for {submodule}")
33
+
34
+ return ast_imports[0]
35
+
36
+
37
+ def code_for_imports(mod_code: str, submodules: list[str]) -> str:
38
+ res = []
39
+ mod = ast.parse(mod_code)
40
+ for submod in submodules:
41
+ expr = fetch_submodule(mod.body, submod)
42
+ expr.module = "griffe"
43
+ new_expr = copy.copy(expr)
44
+ new_expr.module = "griffe"
45
+ res.append(ast.unparse(new_expr))
46
+
47
+ return black.format_str(
48
+ "# flake8: noqa\n\n" + "\n".join(res), mode=black.FileMode()
49
+ )
50
+
51
+
52
+ def generate_griffe_stub(out_path: Path, mod, submodules: list[str]):
53
+ res = code_for_imports(inspect.getsource(mod), submodules)
54
+ out_path.write_text(res)
55
+
56
+
57
+ MAPPINGS = {
58
+ "dataclasses": [
59
+ "griffe._internal.models",
60
+ "griffe._internal.mixins",
61
+ "griffe._internal.enumerations",
62
+ ],
63
+ "docstrings": ["griffe._internal.docstrings.models"],
64
+ "expressions": ["griffe._internal.expressions"],
65
+ }
66
+
67
+ if __name__ == "__main__":
68
+ for out_name, submodules in MAPPINGS.items():
69
+ generate_griffe_stub(
70
+ Path(__file__).parent / f"{out_name}.py", griffe, submodules
71
+ )
@@ -0,0 +1,35 @@
1
+ # flake8: noqa
2
+
3
+ from griffe import (
4
+ Alias,
5
+ Attribute,
6
+ Class,
7
+ Decorator,
8
+ Docstring,
9
+ Function,
10
+ Module,
11
+ Object,
12
+ Parameter,
13
+ Parameters,
14
+ TypeAlias,
15
+ TypeParameter,
16
+ TypeParameters,
17
+ )
18
+ from griffe import (
19
+ DelMembersMixin,
20
+ GetMembersMixin,
21
+ ObjectAliasMixin,
22
+ SerializationMixin,
23
+ SetMembersMixin,
24
+ )
25
+ from griffe import (
26
+ BreakageKind,
27
+ DocstringSectionKind,
28
+ ExplanationStyle,
29
+ Kind,
30
+ LogLevel,
31
+ ObjectKind,
32
+ ParameterKind,
33
+ Parser,
34
+ TypeParameterKind,
35
+ )
@@ -0,0 +1,38 @@
1
+ # flake8: noqa
2
+
3
+ from griffe import (
4
+ DocstringAdmonition,
5
+ DocstringAttribute,
6
+ DocstringClass,
7
+ DocstringDeprecated,
8
+ DocstringElement,
9
+ DocstringFunction,
10
+ DocstringModule,
11
+ DocstringNamedElement,
12
+ DocstringParameter,
13
+ DocstringRaise,
14
+ DocstringReceive,
15
+ DocstringReturn,
16
+ DocstringSection,
17
+ DocstringSectionAdmonition,
18
+ DocstringSectionAttributes,
19
+ DocstringSectionClasses,
20
+ DocstringSectionDeprecated,
21
+ DocstringSectionExamples,
22
+ DocstringSectionFunctions,
23
+ DocstringSectionModules,
24
+ DocstringSectionOtherParameters,
25
+ DocstringSectionParameters,
26
+ DocstringSectionRaises,
27
+ DocstringSectionReceives,
28
+ DocstringSectionReturns,
29
+ DocstringSectionText,
30
+ DocstringSectionTypeAliases,
31
+ DocstringSectionTypeParameters,
32
+ DocstringSectionWarns,
33
+ DocstringSectionYields,
34
+ DocstringTypeAlias,
35
+ DocstringTypeParameter,
36
+ DocstringWarn,
37
+ DocstringYield,
38
+ )
@@ -0,0 +1,48 @@
1
+ # flake8: noqa
2
+
3
+ from griffe import (
4
+ Expr,
5
+ ExprAttribute,
6
+ ExprBinOp,
7
+ ExprBoolOp,
8
+ ExprCall,
9
+ ExprCompare,
10
+ ExprComprehension,
11
+ ExprConstant,
12
+ ExprDict,
13
+ ExprDictComp,
14
+ ExprExtSlice,
15
+ ExprFormatted,
16
+ ExprGeneratorExp,
17
+ ExprIfExp,
18
+ ExprInterpolation,
19
+ ExprJoinedStr,
20
+ ExprKeyword,
21
+ ExprLambda,
22
+ ExprList,
23
+ ExprListComp,
24
+ ExprName,
25
+ ExprNamedExpr,
26
+ ExprParameter,
27
+ ExprSet,
28
+ ExprSetComp,
29
+ ExprSlice,
30
+ ExprSubscript,
31
+ ExprTemplateStr,
32
+ ExprTuple,
33
+ ExprUnaryOp,
34
+ ExprVarKeyword,
35
+ ExprVarPositional,
36
+ ExprYield,
37
+ ExprYieldFrom,
38
+ get_annotation,
39
+ get_base_class,
40
+ get_class_keyword,
41
+ get_condition,
42
+ get_expression,
43
+ safe_get_annotation,
44
+ safe_get_base_class,
45
+ safe_get_class_keyword,
46
+ safe_get_condition,
47
+ safe_get_expression,
48
+ )
@@ -0,0 +1,18 @@
1
+ try:
2
+ from pydantic.v1 import (
3
+ BaseModel,
4
+ Field,
5
+ Extra,
6
+ PrivateAttr,
7
+ ValidationError,
8
+ validator,
9
+ ) # noqa
10
+ except ImportError:
11
+ from pydantic import ( # noqa
12
+ BaseModel,
13
+ Field,
14
+ Extra,
15
+ PrivateAttr,
16
+ ValidationError,
17
+ validator,
18
+ )