overture-schema-codegen 0.1.1.dev0__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.
Files changed (60) hide show
  1. overture_schema_codegen-0.1.1.dev0/PKG-INFO +13 -0
  2. overture_schema_codegen-0.1.1.dev0/pyproject.toml +62 -0
  3. overture_schema_codegen-0.1.1.dev0/pyproject.toml.orig +46 -0
  4. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/__init__.py +1 -0
  5. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/cli.py +228 -0
  6. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/__init__.py +0 -0
  7. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/docstring.py +46 -0
  8. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/enum_extraction.py +40 -0
  9. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/examples.py +367 -0
  10. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/field.py +172 -0
  11. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/field_constraints.py +185 -0
  12. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/field_walk.py +275 -0
  13. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/length_constraints.py +49 -0
  14. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/literal_alternatives.py +26 -0
  15. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/model_constraints.py +252 -0
  16. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/model_extraction.py +240 -0
  17. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/newtype_extraction.py +73 -0
  18. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/numeric_extraction.py +74 -0
  19. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/pydantic_extraction.py +33 -0
  20. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/specs.py +295 -0
  21. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/type_analyzer.py +693 -0
  22. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/type_registry.py +137 -0
  23. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/extraction/union_extraction.py +270 -0
  24. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/layout/__init__.py +0 -0
  25. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/layout/module_layout.py +139 -0
  26. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/layout/type_collection.py +122 -0
  27. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/__init__.py +0 -0
  28. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/link_computation.py +70 -0
  29. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/path_assignment.py +114 -0
  30. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/pipeline.py +198 -0
  31. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/renderer.py +641 -0
  32. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/reverse_references.py +169 -0
  33. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/templates/_used_by.md.jinja2 +10 -0
  34. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/templates/enum.md.jinja2 +13 -0
  35. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/templates/feature.md.jinja2 +45 -0
  36. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/templates/geometric.md.jinja2 +11 -0
  37. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/templates/newtype.md.jinja2 +17 -0
  38. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/templates/numeric.md.jinja2 +27 -0
  39. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/templates/pydantic_type.md.jinja2 +8 -0
  40. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/markdown/type_format.py +383 -0
  41. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/py.typed +0 -0
  42. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/__init__.py +1 -0
  43. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/_primitive_fill.py +23 -0
  44. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/_render_common.py +477 -0
  45. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/check_builder.py +961 -0
  46. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/check_ir.py +223 -0
  47. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/constraint_dispatch.py +753 -0
  48. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/pipeline.py +220 -0
  49. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/renderer.py +816 -0
  50. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/schema_builder.py +187 -0
  51. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/templates/_check_function.py.jinja2 +10 -0
  52. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/templates/model_module.py.jinja2 +83 -0
  53. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/templates/test_module.py.jinja2 +129 -0
  54. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/test_data/__init__.py +9 -0
  55. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/test_data/base_row.py +835 -0
  56. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/test_data/constraint_values.py +203 -0
  57. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/test_data/invalid_value.py +105 -0
  58. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/test_data/scaffold.py +390 -0
  59. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/pyspark/test_renderer.py +708 -0
  60. overture_schema_codegen-0.1.1.dev0/src/overture/schema/codegen/spec_discovery.py +66 -0
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: overture-schema-codegen
3
+ Version: 0.1.1.dev0
4
+ Summary: Code generator that produces documentation and code from Pydantic models
5
+ License-Expression: MIT
6
+ Requires-Dist: click>=8.1
7
+ Requires-Dist: jinja2>=3.0
8
+ Requires-Dist: overture-schema-cli>=0.1.1
9
+ Requires-Dist: overture-schema-common>=0.1.1
10
+ Requires-Dist: overture-schema-system>=0.1.1
11
+ Requires-Dist: tomli>=2.0 ; python_full_version < '3.11'
12
+ Requires-Dist: typing-extensions>=4.0
13
+ Requires-Python: >=3.10
@@ -0,0 +1,62 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.11.32,<0.13"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ dependencies = [
7
+ "click>=8.1",
8
+ "jinja2>=3.0",
9
+ "overture-schema-cli>=0.1.1",
10
+ "overture-schema-common>=0.1.1",
11
+ "overture-schema-system>=0.1.1",
12
+ "tomli>=2.0; python_version < '3.11'",
13
+ "typing-extensions>=4.0",
14
+ ]
15
+ description = "Code generator that produces documentation and code from Pydantic models"
16
+ version = "0.1.1.dev0"
17
+ license = "MIT"
18
+ name = "overture-schema-codegen"
19
+ requires-python = ">=3.10"
20
+
21
+ [project.scripts]
22
+ overture-codegen = "overture.schema.codegen.cli:main"
23
+
24
+ [tool.uv.sources.overture-schema-cli]
25
+ workspace = true
26
+
27
+ [tool.uv.sources.overture-schema-common]
28
+ workspace = true
29
+
30
+ [tool.uv.sources.overture-schema-system]
31
+ workspace = true
32
+
33
+ [tool.uv.sources.overture-schema-theme-addresses]
34
+ workspace = true
35
+
36
+ [tool.uv.sources.overture-schema-theme-base]
37
+ workspace = true
38
+
39
+ [tool.uv.sources.overture-schema-theme-buildings]
40
+ workspace = true
41
+
42
+ [tool.uv.sources.overture-schema-theme-divisions]
43
+ workspace = true
44
+
45
+ [tool.uv.sources.overture-schema-theme-places]
46
+ workspace = true
47
+
48
+ [tool.uv.sources.overture-schema-theme-transportation]
49
+ workspace = true
50
+
51
+ [tool.uv.build-backend]
52
+ module-name = "overture.schema.codegen"
53
+
54
+ [dependency-groups]
55
+ test = [
56
+ "overture-schema-theme-addresses",
57
+ "overture-schema-theme-base",
58
+ "overture-schema-theme-buildings",
59
+ "overture-schema-theme-divisions",
60
+ "overture-schema-theme-places",
61
+ "overture-schema-theme-transportation",
62
+ ]
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["uv_build>=0.11.32,<0.13"]
3
+ build-backend = "uv_build"
4
+
5
+ [project]
6
+ dependencies = [
7
+ "click>=8.1",
8
+ "jinja2>=3.0",
9
+ "overture-schema-cli>=0.1.1",
10
+ "overture-schema-common>=0.1.1",
11
+ "overture-schema-system>=0.1.1",
12
+ "tomli>=2.0; python_version < '3.11'",
13
+ "typing-extensions>=4.0",
14
+ ]
15
+ description = "Code generator that produces documentation and code from Pydantic models"
16
+ version = "0.1.1.dev0"
17
+ license = "MIT"
18
+ name = "overture-schema-codegen"
19
+ requires-python = ">=3.10"
20
+
21
+ [project.scripts]
22
+ overture-codegen = "overture.schema.codegen.cli:main"
23
+
24
+ [tool.uv.sources]
25
+ overture-schema-cli = { workspace = true }
26
+ overture-schema-common = { workspace = true }
27
+ overture-schema-system = { workspace = true }
28
+ overture-schema-theme-addresses = { workspace = true }
29
+ overture-schema-theme-base = { workspace = true }
30
+ overture-schema-theme-buildings = { workspace = true }
31
+ overture-schema-theme-divisions = { workspace = true }
32
+ overture-schema-theme-places = { workspace = true }
33
+ overture-schema-theme-transportation = { workspace = true }
34
+
35
+ [dependency-groups]
36
+ test = [
37
+ "overture-schema-theme-addresses",
38
+ "overture-schema-theme-base",
39
+ "overture-schema-theme-buildings",
40
+ "overture-schema-theme-divisions",
41
+ "overture-schema-theme-places",
42
+ "overture-schema-theme-transportation",
43
+ ]
44
+
45
+ [tool.uv.build-backend]
46
+ module-name = "overture.schema.codegen"
@@ -0,0 +1 @@
1
+ """Code generator for Overture Schema Pydantic models."""
@@ -0,0 +1,228 @@
1
+ """CLI entrypoint for schema code generation."""
2
+
3
+ import json
4
+ import logging
5
+ from collections.abc import Mapping
6
+ from pathlib import Path, PurePosixPath
7
+
8
+ import click
9
+
10
+ from overture.schema.cli.tag_options import build_selector, tag_selection_options
11
+ from overture.schema.system.discovery import (
12
+ discover_models,
13
+ filter_models,
14
+ split_entry_point,
15
+ )
16
+
17
+ from .extraction.specs import ModelSpec, SupplementarySpec, TypeIdentity
18
+ from .layout.module_layout import (
19
+ OUTPUT_ROOT,
20
+ compute_schema_root,
21
+ entry_point_module,
22
+ )
23
+ from .markdown.pipeline import generate_markdown_pages
24
+ from .pyspark.pipeline import generate_pyspark_modules
25
+ from .spec_discovery import extract_alias_spec, extract_model_spec
26
+
27
+ log = logging.getLogger(__name__)
28
+
29
+ __all__ = ["cli"]
30
+
31
+ _OUTPUT_FORMATS = ("markdown", "pyspark")
32
+
33
+ _FEATURE_FRONTMATTER = "---\nsidebar_position: 1\n---\n\n"
34
+
35
+
36
+ def _write_output(
37
+ content: str,
38
+ output_dir: Path | None,
39
+ output_path: PurePosixPath,
40
+ ) -> None:
41
+ """Write content to a file under output_dir, or stdout."""
42
+ if output_dir:
43
+ file_path = output_dir / output_path
44
+ file_path.parent.mkdir(parents=True, exist_ok=True)
45
+ file_path.write_text(content)
46
+ else:
47
+ click.echo(content)
48
+ click.echo() # separate entries with a blank line in stdout mode
49
+
50
+
51
+ @click.group()
52
+ def cli() -> None:
53
+ """Overture Schema code generator.
54
+
55
+ Generate documentation and code from Pydantic schema models.
56
+ """
57
+
58
+
59
+ @cli.command("list")
60
+ def list_models() -> None:
61
+ """List all discovered models."""
62
+ models = discover_models()
63
+ # Name every entry from its entry point, not the loaded object: a
64
+ # discriminated union loads as an `Annotated[...]` alias with no
65
+ # `__name__`, so `str(model)` would print the whole type expression.
66
+ names = []
67
+ for key in models:
68
+ _, class_name = split_entry_point(key.entry_point)
69
+ names.append(class_name)
70
+ for name in sorted(names):
71
+ click.echo(name)
72
+
73
+
74
+ @cli.command()
75
+ @click.option(
76
+ "--format",
77
+ "output_format",
78
+ required=True,
79
+ type=click.Choice(_OUTPUT_FORMATS),
80
+ help="Output format",
81
+ )
82
+ @tag_selection_options
83
+ @click.option(
84
+ "--output-dir",
85
+ type=click.Path(path_type=Path),
86
+ default=None,
87
+ help="Write output files directly into this directory (default: stdout). "
88
+ "For pyspark, writes expression modules (*.py). "
89
+ "For markdown, writes theme subdirectories.",
90
+ )
91
+ @click.option(
92
+ "--test-output-dir",
93
+ type=click.Path(path_type=Path),
94
+ default=None,
95
+ help="Write test modules (test_*.py) into this directory (pyspark only).",
96
+ )
97
+ def generate(
98
+ output_format: str,
99
+ tags: tuple[str, ...],
100
+ filters: tuple[str, ...],
101
+ excludes: tuple[str, ...],
102
+ output_dir: Path | None,
103
+ test_output_dir: Path | None,
104
+ ) -> None:
105
+ """Generate code/docs from discovered models."""
106
+ if output_format != "pyspark" and test_output_dir is not None:
107
+ raise click.UsageError("--test-output-dir is only valid with --format pyspark")
108
+
109
+ all_models = discover_models()
110
+
111
+ models = filter_models(all_models, build_selector(tags, filters, excludes))
112
+
113
+ if output_dir:
114
+ output_dir.mkdir(parents=True, exist_ok=True)
115
+
116
+ model_specs: list[ModelSpec] = [
117
+ spec
118
+ for key, entry in models.items()
119
+ if (spec := extract_model_spec(key, entry)) is not None
120
+ ]
121
+
122
+ if output_format == "pyspark":
123
+ _generate_pyspark(model_specs, output_dir, test_output_dir)
124
+ else:
125
+ # RootModel entry points yield no ModelSpec, so they document as
126
+ # named aliases -- reachable no other way, since a RootModel field
127
+ # unwraps to its bare shape and names no type.
128
+ external_specs: dict[TypeIdentity, SupplementarySpec] = {
129
+ alias.identity: alias
130
+ for entry in models.values()
131
+ if (alias := extract_alias_spec(entry)) is not None
132
+ }
133
+ module_paths = [entry_point_module(k.entry_point) for k in all_models]
134
+ schema_root = compute_schema_root(module_paths)
135
+ _generate_markdown(model_specs, schema_root, output_dir, external_specs)
136
+
137
+
138
+ def _generate_markdown(
139
+ model_specs: list[ModelSpec],
140
+ schema_root: str,
141
+ output_dir: Path | None,
142
+ external_specs: Mapping[TypeIdentity, SupplementarySpec],
143
+ ) -> None:
144
+ """Generate markdown with directory layout and placement-aware links."""
145
+ pages = generate_markdown_pages(
146
+ model_specs, schema_root, external_specs=external_specs
147
+ )
148
+
149
+ for page in pages:
150
+ content = (
151
+ f"{_FEATURE_FRONTMATTER}{page.content}" if page.is_model else page.content
152
+ )
153
+ _write_output(content, output_dir, page.path)
154
+
155
+ if output_dir:
156
+ feature_paths = {page.path for page in pages if page.is_model}
157
+ all_paths = {page.path for page in pages}
158
+ _write_category_files(output_dir, all_paths, feature_paths)
159
+
160
+
161
+ def _generate_pyspark(
162
+ model_specs: list[ModelSpec],
163
+ output_dir: Path | None,
164
+ test_output_dir: Path | None = None,
165
+ ) -> None:
166
+ """Generate PySpark validation modules.
167
+
168
+ Output is syntactically valid Python; we assume a code formatter runs
169
+ over the written directories afterwards to match existing conventions.
170
+ """
171
+ modules = generate_pyspark_modules(model_specs)
172
+ for mod in modules.source:
173
+ _write_output(mod.content, output_dir, mod.path)
174
+ if test_output_dir is not None:
175
+ for mod in modules.test:
176
+ _write_output(mod.content, test_output_dir, mod.path)
177
+
178
+
179
+ def _ancestor_dirs(paths: set[PurePosixPath]) -> set[PurePosixPath]:
180
+ """Collect all ancestor directories for a set of file paths."""
181
+ dirs: set[PurePosixPath] = set()
182
+ for path in paths:
183
+ parent = path.parent
184
+ while parent != OUTPUT_ROOT:
185
+ dirs.add(parent)
186
+ parent = parent.parent
187
+ return dirs
188
+
189
+
190
+ def _top_level_positions(
191
+ dirs: set[PurePosixPath],
192
+ feature_paths: set[PurePosixPath],
193
+ ) -> dict[PurePosixPath, int]:
194
+ """Assign sidebar positions: feature dirs first, then non-feature, both alphabetical."""
195
+ feature_dir_names = {p.parts[0] for p in feature_paths}
196
+ top_level = sorted(d for d in dirs if d.parent == OUTPUT_ROOT)
197
+ feature_dirs = [d for d in top_level if d.name in feature_dir_names]
198
+ non_feature_dirs = [d for d in top_level if d.name not in feature_dir_names]
199
+ return {d: i for i, d in enumerate(feature_dirs + non_feature_dirs, start=1)}
200
+
201
+
202
+ def _write_category_files(
203
+ output_dir: Path,
204
+ all_paths: set[PurePosixPath],
205
+ feature_paths: set[PurePosixPath],
206
+ ) -> None:
207
+ """Write _category_.json files for Docusaurus sidebar navigation."""
208
+ dirs = _ancestor_dirs(all_paths)
209
+ positions = _top_level_positions(dirs, feature_paths)
210
+
211
+ for dir_path in sorted(dirs):
212
+ label = dir_path.name.replace("_", " ").title()
213
+ category: dict[str, object] = {"label": label}
214
+ if dir_path in positions:
215
+ category["position"] = positions[dir_path]
216
+
217
+ file_path = output_dir / dir_path / "_category_.json"
218
+ file_path.parent.mkdir(parents=True, exist_ok=True)
219
+ file_path.write_text(json.dumps(category, indent=2) + "\n")
220
+
221
+
222
+ def main() -> None:
223
+ """Run the CLI entry point."""
224
+ cli()
225
+
226
+
227
+ if __name__ == "__main__":
228
+ main()
@@ -0,0 +1,46 @@
1
+ """Docstring extraction and cleaning utilities."""
2
+
3
+ import inspect
4
+ from enum import Enum
5
+ from typing import NewType
6
+
7
+ __all__ = ["clean_docstring", "first_docstring_line", "is_custom_docstring"]
8
+
9
+
10
+ # Probe auto-generated docstrings so we can distinguish them from explicit ones.
11
+ # Both Enum and NewType generate default docstrings that vary by Python version;
12
+ # capturing at import time adapts automatically if the format changes.
13
+ class _DocstringProbeEnum(Enum):
14
+ pass
15
+
16
+
17
+ _ENUM_DEFAULT_DOCSTRING = _DocstringProbeEnum.__doc__
18
+ del _DocstringProbeEnum
19
+ _NewtypeProbe = NewType("_NewtypeProbe", int)
20
+ _NEWTYPE_DEFAULT_DOCSTRING = _NewtypeProbe.__doc__
21
+ del _NewtypeProbe
22
+
23
+
24
+ def clean_docstring(doc: str | None) -> str | None:
25
+ """Return cleaned docstring, or None if absent or whitespace-only."""
26
+ if not doc:
27
+ return None
28
+ cleaned = inspect.cleandoc(doc)
29
+ return cleaned or None
30
+
31
+
32
+ def first_docstring_line(doc: str | None) -> str | None:
33
+ """Return the first line of a docstring, or None if absent."""
34
+ cleaned = clean_docstring(doc)
35
+ if not cleaned:
36
+ return None
37
+ return cleaned.split("\n")[0]
38
+
39
+
40
+ def is_custom_docstring(doc: str | None, inherited_doc: str | None = None) -> bool:
41
+ """Check if a docstring was explicitly written, not auto-generated or inherited."""
42
+ return bool(doc) and doc not in (
43
+ _ENUM_DEFAULT_DOCSTRING,
44
+ _NEWTYPE_DEFAULT_DOCSTRING,
45
+ inherited_doc,
46
+ )
@@ -0,0 +1,40 @@
1
+ """Enum extraction."""
2
+
3
+ from enum import Enum
4
+
5
+ from .docstring import clean_docstring, is_custom_docstring
6
+ from .specs import EnumMemberSpec, EnumSpec
7
+
8
+ __all__ = ["extract_enum"]
9
+
10
+
11
+ def extract_enum(enum_class: type[Enum]) -> EnumSpec:
12
+ """Extract enum specification from an Enum class.
13
+
14
+ Handles both simple str Enums and DocumentedEnums where members
15
+ have per-value descriptions via the __doc__ attribute.
16
+ """
17
+ class_doc = enum_class.__doc__
18
+ description = clean_docstring(class_doc) if is_custom_docstring(class_doc) else None
19
+
20
+ members: list[EnumMemberSpec] = []
21
+ for member in enum_class:
22
+ member_doc = getattr(member, "__doc__", None)
23
+ member_description = (
24
+ member_doc if is_custom_docstring(member_doc, class_doc) else None
25
+ )
26
+
27
+ members.append(
28
+ EnumMemberSpec(
29
+ name=member.name,
30
+ value=str(member.value),
31
+ description=member_description,
32
+ )
33
+ )
34
+
35
+ return EnumSpec(
36
+ name=enum_class.__name__,
37
+ description=description,
38
+ members=members,
39
+ source_type=enum_class,
40
+ )