semolina 0.3.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.
Files changed (33) hide show
  1. semolina-0.3.0/PKG-INFO +29 -0
  2. semolina-0.3.0/README.md +3 -0
  3. semolina-0.3.0/pyproject.toml +125 -0
  4. semolina-0.3.0/src/semolina/__init__.py +41 -0
  5. semolina-0.3.0/src/semolina/__main__.py +6 -0
  6. semolina-0.3.0/src/semolina/cli/__init__.py +51 -0
  7. semolina-0.3.0/src/semolina/cli/codegen.py +119 -0
  8. semolina-0.3.0/src/semolina/cli/utils.py +98 -0
  9. semolina-0.3.0/src/semolina/codegen/__init__.py +1 -0
  10. semolina-0.3.0/src/semolina/codegen/introspector.py +65 -0
  11. semolina-0.3.0/src/semolina/codegen/python_renderer.py +289 -0
  12. semolina-0.3.0/src/semolina/codegen/templates/python_model.py.jinja2 +25 -0
  13. semolina-0.3.0/src/semolina/codegen/type_map.py +134 -0
  14. semolina-0.3.0/src/semolina/config.py +87 -0
  15. semolina-0.3.0/src/semolina/conftest.py +81 -0
  16. semolina-0.3.0/src/semolina/cursor.py +179 -0
  17. semolina-0.3.0/src/semolina/dialect.py +69 -0
  18. semolina-0.3.0/src/semolina/engines/__init__.py +26 -0
  19. semolina-0.3.0/src/semolina/engines/base.py +159 -0
  20. semolina-0.3.0/src/semolina/engines/databricks.py +367 -0
  21. semolina-0.3.0/src/semolina/engines/mock.py +371 -0
  22. semolina-0.3.0/src/semolina/engines/snowflake.py +374 -0
  23. semolina-0.3.0/src/semolina/engines/sql.py +906 -0
  24. semolina-0.3.0/src/semolina/fields.py +733 -0
  25. semolina-0.3.0/src/semolina/filters.py +172 -0
  26. semolina-0.3.0/src/semolina/models.py +240 -0
  27. semolina-0.3.0/src/semolina/pool.py +243 -0
  28. semolina-0.3.0/src/semolina/py.typed +0 -0
  29. semolina-0.3.0/src/semolina/query.py +490 -0
  30. semolina-0.3.0/src/semolina/registry.py +161 -0
  31. semolina-0.3.0/src/semolina/results.py +171 -0
  32. semolina-0.3.0/src/semolina/testing/__init__.py +17 -0
  33. semolina-0.3.0/src/semolina/testing/credentials.py +222 -0
@@ -0,0 +1,29 @@
1
+ Metadata-Version: 2.3
2
+ Name: semolina
3
+ Version: 0.3.0
4
+ Summary: A Pythonic ORM for querying data warehouse semantic views (Snowflake, Databricks)
5
+ Author: Anentropic
6
+ Author-email: Anentropic <ego@anentropic.com>
7
+ Requires-Dist: adbc-poolhouse>=1.2.0
8
+ Requires-Dist: typer>=0.12.0
9
+ Requires-Dist: rich>=13.0.0
10
+ Requires-Dist: jinja2>=3.1.0
11
+ Requires-Dist: sphinx-autobuild[dev]>=2025.8.25
12
+ Requires-Dist: semolina[snowflake,databricks] ; extra == 'all'
13
+ Requires-Dist: databricks-sql-connector[pyarrow]>=4.2.5 ; extra == 'databricks'
14
+ Requires-Dist: adbc-poolhouse[snowflake] ; extra == 'snowflake'
15
+ Requires-Dist: snowflake-connector-python>=4.3.0 ; extra == 'snowflake'
16
+ Requires-Python: >=3.11
17
+ Project-URL: Homepage, https://github.com/anentropic/semolina
18
+ Project-URL: Documentation, https://anentropic.github.io/semolina/
19
+ Project-URL: Repository, https://github.com/anentropic/semolina
20
+ Project-URL: Issues, https://github.com/anentropic/semolina/issues
21
+ Project-URL: Changelog, https://anentropic.github.io/semolina/changelog/
22
+ Provides-Extra: all
23
+ Provides-Extra: databricks
24
+ Provides-Extra: snowflake
25
+ Description-Content-Type: text/markdown
26
+
27
+ # Semolina
28
+
29
+ A Python "OAM" (Object Analytics Model) for data warehouse Semantic Layer backends.
@@ -0,0 +1,3 @@
1
+ # Semolina
2
+
3
+ A Python "OAM" (Object Analytics Model) for data warehouse Semantic Layer backends.
@@ -0,0 +1,125 @@
1
+ [project]
2
+ name = "semolina"
3
+ version = "0.3.0"
4
+ description = "A Pythonic ORM for querying data warehouse semantic views (Snowflake, Databricks)"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Anentropic", email = "ego@anentropic.com" }
8
+ ]
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "adbc-poolhouse>=1.2.0",
12
+ "typer>=0.12.0",
13
+ "rich>=13.0.0",
14
+ "jinja2>=3.1.0",
15
+ "sphinx-autobuild[dev]>=2025.8.25",
16
+ ]
17
+
18
+ [project.scripts]
19
+ semolina = "semolina.cli:app"
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/anentropic/semolina"
23
+ Documentation = "https://anentropic.github.io/semolina/"
24
+ Repository = "https://github.com/anentropic/semolina"
25
+ Issues = "https://github.com/anentropic/semolina/issues"
26
+ Changelog = "https://anentropic.github.io/semolina/changelog/"
27
+
28
+ [project.optional-dependencies]
29
+ databricks = [
30
+ # Databricks ADBC driver is Foundry-distributed (not on PyPI).
31
+ # Keep databricks-sql-connector as fallback for non-ADBC usage.
32
+ "databricks-sql-connector[pyarrow]>=4.2.5",
33
+ ]
34
+ snowflake = [
35
+ "adbc-poolhouse[snowflake]",
36
+ # SnowflakeEngine still uses snowflake-connector-python directly.
37
+ "snowflake-connector-python>=4.3.0",
38
+ ]
39
+ all = [
40
+ "semolina[snowflake,databricks]",
41
+ ]
42
+
43
+ [build-system]
44
+ requires = ["uv_build>=0.9.18,<0.11.0"]
45
+ build-backend = "uv_build"
46
+
47
+ [dependency-groups]
48
+ dev = [
49
+ "basedpyright>=1.38.0",
50
+ "ipython>=9.10.0",
51
+ "pdbpp>=0.12.0.post1",
52
+ "pydantic-settings>=2.7.0",
53
+ "pytest>=8.0.0",
54
+ "pytest-cov>=6.0.0",
55
+ "pytest-xdist>=3.6.0",
56
+ "ruff>=0.15.1",
57
+ "syrupy>=5.1.0",
58
+ ]
59
+ docs = [
60
+ "sphinx>=8.0",
61
+ "shibuya>=2025.1.1",
62
+ "sphinx-autoapi>=3.6.0",
63
+ "sphinx-design>=0.6.0",
64
+ "sphinx-copybutton>=0.5.2",
65
+ ]
66
+
67
+ [tool.basedpyright]
68
+ pythonVersion = "3.11"
69
+ typeCheckingMode = "strict"
70
+ include = ["src", "tests"]
71
+ # Tests legitimately access private members (_metrics, _fields, etc.) for validation
72
+ reportPrivateUsage = false
73
+ # Field.__eq__ and __ne__ intentionally return Q objects instead of bool for query DSL
74
+ reportIncompatibleMethodOverride = false
75
+ # Field, Metric, Dimension, Fact are Generic[T] descriptors. Unsubscripted usage (e.g.
76
+ # revenue = Metric()) is valid when the type parameter is irrelevant to the usage context
77
+ # (tests, doctests, example models). Python 3.13 TypeVar defaults (PEP 696) would resolve
78
+ # this at the language level; for 3.11/3.12 we suppress the cascade of Unknown errors.
79
+ reportUnknownMemberType = false
80
+ reportUnknownVariableType = false
81
+ reportUnknownArgumentType = false
82
+ # Lambda parameters in MagicMock.side_effect can't be annotated without wrapper function
83
+ reportUnknownLambdaType = false
84
+
85
+ [tool.ruff]
86
+ target-version = "py311"
87
+ line-length = 100
88
+ src = ["src"]
89
+
90
+ [tool.ruff.lint]
91
+ select = ["E", "F", "W", "I", "UP", "B", "SIM", "TCH", "D"]
92
+ ignore = [
93
+ "D1", # don't require docstrings everywhere
94
+ "D202", # allow blank line after docstring (common in tests)
95
+ "D203", # conflicts with D211 (no blank line before class docstring)
96
+ "D212", # conflicts with D213 (multiline summary on second line)
97
+ "D401", # imperative mood is awkward for dunder methods
98
+ "D413", # blank line after last section not required
99
+ ]
100
+
101
+ [tool.uv.workspace]
102
+ members = ["semolina-jaffle-shop"]
103
+
104
+ [tool.uv.sources]
105
+ semolina = { workspace = true }
106
+
107
+ [tool.pytest.ini_options]
108
+ addopts = [
109
+ "-v",
110
+ "--doctest-modules",
111
+ "--doctest-continue-on-failure",
112
+ ]
113
+ doctest_optionflags = ["ELLIPSIS", "NORMALIZE_WHITESPACE"]
114
+ testpaths = ["tests", "src"]
115
+ filterwarnings = [
116
+ "ignore::DeprecationWarning:syrupy.*",
117
+ "ignore:Field name .schema. in.*shadows an attribute in parent:UserWarning",
118
+ ]
119
+ markers = [
120
+ "mock: Fast tests using MockEngine (no warehouse required)",
121
+ "unit: Pure unit tests with no external dependencies",
122
+ "warehouse: Integration tests against real warehouse (requires credentials)",
123
+ "snowflake: Snowflake-specific warehouse tests",
124
+ "databricks: Databricks-specific warehouse tests",
125
+ ]
@@ -0,0 +1,41 @@
1
+ """
2
+ Semolina - A Pythonic ORM for querying data warehouse semantic views.
3
+
4
+ Semolina provides typed model definitions and a fluent query API for
5
+ Snowflake and Databricks semantic views.
6
+ """
7
+
8
+ from .config import pool_from_config
9
+ from .cursor import SemolinaCursor
10
+ from .dialect import Dialect
11
+ from .engines.base import SemolinaConnectionError, SemolinaViewNotFoundError
12
+ from .fields import Dimension, Fact, Metric, NullsOrdering, OrderTerm
13
+ from .filters import Predicate
14
+ from .models import SemanticView
15
+ from .pool import MockPool
16
+ from .registry import get_engine, get_pool, register, unregister
17
+ from .results import Row
18
+
19
+ __version__ = __import__("importlib.metadata").metadata.version("semolina")
20
+
21
+ __all__ = [
22
+ "__version__",
23
+ "Dialect",
24
+ "Dimension",
25
+ "Fact",
26
+ "Metric",
27
+ "MockPool",
28
+ "NullsOrdering",
29
+ "OrderTerm",
30
+ "Predicate",
31
+ "Row",
32
+ "SemolinaCursor",
33
+ "SemolinaConnectionError",
34
+ "SemolinaViewNotFoundError",
35
+ "SemanticView",
36
+ "get_engine",
37
+ "get_pool",
38
+ "pool_from_config",
39
+ "register",
40
+ "unregister",
41
+ ]
@@ -0,0 +1,6 @@
1
+ """Entry point for `python -m semolina`."""
2
+
3
+ from semolina.cli import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
@@ -0,0 +1,51 @@
1
+ """
2
+ Semolina command-line interface.
3
+
4
+ Entry point for the `semolina` CLI command. Registers subcommands
5
+ for code generation and future tooling.
6
+ """
7
+
8
+ import typer
9
+
10
+ from .codegen import codegen
11
+
12
+ app = typer.Typer(
13
+ name="semolina",
14
+ help="Semolina — warehouse-native semantic view tooling.",
15
+ no_args_is_help=True,
16
+ add_completion=False,
17
+ )
18
+
19
+ app.command(
20
+ "codegen",
21
+ epilog=(
22
+ "[bold]Exit codes[/bold]\n\n"
23
+ " [green]0[/green] Success\n\n"
24
+ " [yellow]1[/yellow] Unexpected error\n\n"
25
+ " [yellow]2[/yellow] Invalid [bold]--backend[/bold] value (or omitted)\n\n"
26
+ " [red]3[/red] View not found in the warehouse\n\n"
27
+ " [red]4[/red] Connection or authentication failure"
28
+ ),
29
+ )(codegen)
30
+
31
+
32
+ def version_callback(value: bool) -> None:
33
+ """Print version and exit."""
34
+ if value:
35
+ from semolina import __version__
36
+
37
+ typer.echo(f"semolina {__version__}")
38
+ raise typer.Exit()
39
+
40
+
41
+ @app.callback()
42
+ def main(
43
+ version: bool = typer.Option(
44
+ False,
45
+ "--version",
46
+ callback=version_callback,
47
+ is_eager=True,
48
+ help="Show version and exit.",
49
+ ),
50
+ ) -> None:
51
+ """Semolina — warehouse-native semantic view tooling."""
@@ -0,0 +1,119 @@
1
+ """Codegen subcommand: introspect warehouse semantic views and generate Python model classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from typing import TYPE_CHECKING, Annotated
7
+
8
+ import typer
9
+ from rich.console import Console
10
+
11
+ if TYPE_CHECKING:
12
+ from semolina.codegen.introspector import IntrospectedView
13
+ from semolina.engines.base import Engine
14
+
15
+ # Diagnostics-only console: writes to stderr
16
+ # NOTE: _stderr is module-level for error messages outside the command function.
17
+ # Python source output uses typer.echo() so CliRunner captures it correctly.
18
+ _stderr = Console(file=sys.stderr, stderr=True)
19
+
20
+ # Exit code constants for scripted callers.
21
+ # Note: Typer also uses exit code 2 for missing required arguments (fires earlier).
22
+ # EXIT_INVALID_BACKEND=2 fires when --backend value is provided but unrecognized.
23
+ EXIT_INVALID_BACKEND = 2
24
+ EXIT_VIEW_NOT_FOUND = 3
25
+ EXIT_CONNECTION_ERROR = 4
26
+
27
+
28
+ def _resolve_backend(backend_spec: str) -> Engine:
29
+ """
30
+ Resolve a backend specifier string to an Engine instance.
31
+
32
+ Recognises the shorthand aliases ``'snowflake'`` and ``'databricks'``, and
33
+ also accepts any fully-qualified ``dotted.path.ClassName`` string which is
34
+ dynamically imported and instantiated with no arguments.
35
+
36
+ Args:
37
+ backend_spec: One of ``'snowflake'``, ``'databricks'``, or a dotted
38
+ import path such as ``'mypackage.backends.CustomEngine'``.
39
+
40
+ Returns:
41
+ Engine: An instantiated engine ready for introspection calls.
42
+
43
+ Raises:
44
+ typer.BadParameter: If the specifier is not recognised or cannot be
45
+ imported.
46
+ """
47
+ if backend_spec == "snowflake":
48
+ from semolina.engines.snowflake import SnowflakeEngine
49
+ from semolina.testing.credentials import SnowflakeCredentials
50
+
51
+ creds = SnowflakeCredentials.load()
52
+ params = creds.model_dump(by_alias=True)
53
+ params["password"] = creds.password.get_secret_value()
54
+ return SnowflakeEngine(**params)
55
+ elif backend_spec == "databricks":
56
+ from semolina.engines.databricks import DatabricksEngine
57
+ from semolina.testing.credentials import DatabricksCredentials
58
+
59
+ creds = DatabricksCredentials.load()
60
+ params = creds.model_dump(by_alias=True)
61
+ params["access_token"] = creds.access_token.get_secret_value()
62
+ return DatabricksEngine(**params)
63
+ else:
64
+ import importlib
65
+
66
+ module_path, _, class_name = backend_spec.rpartition(".")
67
+ if not module_path:
68
+ raise typer.BadParameter(
69
+ f"Unknown backend {backend_spec!r}. "
70
+ "Use 'snowflake', 'databricks', or a dotted import path."
71
+ )
72
+ try:
73
+ module = importlib.import_module(module_path)
74
+ cls = getattr(module, class_name)
75
+ return cls() # type: ignore[no-any-return]
76
+ except (ImportError, AttributeError) as e:
77
+ raise typer.BadParameter(f"Cannot import backend {backend_spec!r}: {e}") from e
78
+
79
+
80
+ def codegen(
81
+ views: Annotated[
82
+ list[str],
83
+ typer.Argument(help="Schema-qualified view names (e.g. my_schema.my_view)"),
84
+ ],
85
+ backend: Annotated[
86
+ str,
87
+ typer.Option(
88
+ "--backend", "-b", help="Backend: snowflake, databricks, or dotted.path.ClassName"
89
+ ),
90
+ ],
91
+ ) -> None:
92
+ """Introspect warehouse semantic views and generate Semolina model classes."""
93
+ from semolina.engines.base import SemolinaConnectionError, SemolinaViewNotFoundError
94
+
95
+ try:
96
+ engine = _resolve_backend(backend)
97
+ except typer.BadParameter as e:
98
+ _stderr.print(f"[bold red]Error:[/bold red] {e}")
99
+ raise typer.Exit(code=EXIT_INVALID_BACKEND) from e
100
+
101
+ introspected_views: list[IntrospectedView] = []
102
+ for view_name in views:
103
+ try:
104
+ introspected = engine.introspect(view_name)
105
+ introspected_views.append(introspected)
106
+ except SemolinaViewNotFoundError as e:
107
+ _stderr.print(f"[bold red]Error:[/bold red] {e}")
108
+ raise typer.Exit(code=EXIT_VIEW_NOT_FOUND) from e
109
+ except SemolinaConnectionError as e:
110
+ _stderr.print(f"[bold red]Error:[/bold red] {e}")
111
+ raise typer.Exit(code=EXIT_CONNECTION_ERROR) from e
112
+ except RuntimeError as e:
113
+ _stderr.print(f"[bold red]Error:[/bold red] {e}")
114
+ raise typer.Exit(code=1) from e
115
+
116
+ from semolina.codegen.python_renderer import render_and_format
117
+
118
+ source = render_and_format(introspected_views)
119
+ typer.echo(source)
@@ -0,0 +1,98 @@
1
+ """
2
+ CLI utility functions for Semolina codegen commands.
3
+
4
+ Provides path resolution, stderr console creation, and progress helpers.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import glob as _glob
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ import typer
14
+ from rich.console import Console
15
+
16
+
17
+ def make_stderr_console(verbose: bool = False) -> Console:
18
+ """
19
+ Create a Rich Console writing to stderr for diagnostic output.
20
+
21
+ Args:
22
+ verbose: If True, suppress no output (show everything). If False, only
23
+ show warnings and errors.
24
+
25
+ Returns:
26
+ Configured Rich Console for stderr.
27
+ """
28
+ return Console(file=sys.stderr, stderr=True)
29
+
30
+
31
+ def resolve_input_paths(input_spec: str, stderr: Console) -> list[Path]:
32
+ """
33
+ Resolve an input specification to a list of Python file paths.
34
+
35
+ Handles three forms:
36
+ - Explicit file path: must exist, returned as single-element list
37
+ - Directory path: scanned recursively for *.py files
38
+ - Glob pattern: expanded via pathlib from cwd
39
+
40
+ Fails immediately (raises SystemExit via typer.Exit) if an explicit
41
+ file path doesn't exist or can't be read.
42
+
43
+ Args:
44
+ input_spec: File path, directory path, or glob pattern string.
45
+ stderr: Rich Console for diagnostic messages.
46
+
47
+ Returns:
48
+ Sorted list of resolved .py file paths.
49
+
50
+ Raises:
51
+ typer.Exit: If an explicit path doesn't exist or is unreadable.
52
+ """
53
+ path = Path(input_spec)
54
+
55
+ # If input contains glob special characters, treat as glob regardless of suffix.
56
+ # This handles patterns like '/path/to/*.py' which have suffix '.py' but aren't
57
+ # explicit file paths.
58
+ _GLOB_CHARS = {"*", "?", "["}
59
+ is_glob = any(ch in input_spec for ch in _GLOB_CHARS)
60
+
61
+ if not is_glob:
62
+ # Explicit file: must exist
63
+ if path.suffix == ".py":
64
+ if not path.exists():
65
+ stderr.print(
66
+ f"[bold red]Error:[/bold red] File not found: [cyan]{path}[/cyan]\n"
67
+ f" Check the path and try again."
68
+ )
69
+ raise typer.Exit(code=1)
70
+ if not path.is_file():
71
+ stderr.print(f"[bold red]Error:[/bold red] Not a file: [cyan]{path}[/cyan]")
72
+ raise typer.Exit(code=1)
73
+ return [path.resolve()]
74
+
75
+ # Directory: scan recursively for *.py files
76
+ if path.is_dir():
77
+ files = sorted(path.rglob("*.py"))
78
+ # Exclude __pycache__ and hidden directories
79
+ files = [
80
+ f
81
+ for f in files
82
+ if "__pycache__" not in f.parts and not any(p.startswith(".") for p in f.parts)
83
+ ]
84
+ return files
85
+
86
+ # Explicit directory path that doesn't exist: fail immediately
87
+ if not path.suffix and not path.exists():
88
+ stderr.print(
89
+ f"[bold red]Error:[/bold red] Directory not found: [cyan]{path}[/cyan]\n"
90
+ f" Check the path and try again."
91
+ )
92
+ raise typer.Exit(code=1)
93
+
94
+ # Glob pattern: expand via glob.glob which handles both relative and absolute patterns.
95
+ # Path.cwd().glob() raises NotImplementedError for absolute patterns in Python 3.14+.
96
+ matched = sorted(Path(m) for m in _glob.glob(input_spec, recursive=True))
97
+ py_files = [f for f in matched if f.suffix == ".py" and f.is_file()]
98
+ return py_files
@@ -0,0 +1 @@
1
+ """Semolina code generation engine."""
@@ -0,0 +1,65 @@
1
+ """
2
+ Intermediate representation dataclasses for reverse codegen introspection.
3
+
4
+ Defines the frozen dataclasses that form the contract between warehouse
5
+ introspection engines and the Python code renderer.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Literal
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class IntrospectedField:
16
+ """
17
+ Intermediate representation of a single field from an introspected semantic view.
18
+
19
+ Produced by warehouse introspection engines and consumed by the Python code
20
+ renderer to generate Metric, Dimension, or Fact field declarations.
21
+
22
+ Attributes:
23
+ name: Python attribute name for the field (e.g., 'revenue', 'country').
24
+ Always lowercase (Pythonic). For Snowflake views, this is the
25
+ warehouse column name lowercased.
26
+ field_type: Semantic role of the field: 'metric', 'dimension', or 'fact'.
27
+ data_type: Python annotation string (e.g., 'int', 'str', 'datetime.date'),
28
+ or None if the SQL type has no clean Python equivalent (triggers a
29
+ TODO comment in the generated output).
30
+ description: Human-readable description of the field. Defaults to empty
31
+ string if the warehouse provides no description metadata.
32
+ source_name: Original warehouse column name when it differs from what
33
+ ``normalize_identifier`` would produce. Set only when the column
34
+ was created with a quoted identifier (e.g., ``"order_id"`` in
35
+ Snowflake, stored as lowercase). None for standard columns that
36
+ round-trip correctly through normalize_identifier.
37
+ """
38
+
39
+ name: str
40
+ field_type: Literal["metric", "dimension", "fact"]
41
+ data_type: str | None
42
+ description: str = ""
43
+ source_name: str | None = None
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class IntrospectedView:
48
+ """
49
+ Intermediate representation of a semantic view returned by engine introspection.
50
+
51
+ Produced by Engine.introspect() and consumed by the Python code renderer to
52
+ generate a complete SemanticView subclass.
53
+
54
+ Attributes:
55
+ view_name: Warehouse identifier for the view (e.g., 'sales_view'). Used
56
+ as the ``view=`` parameter in the generated SemanticView subclass.
57
+ class_name: Python class name to use in the generated code (e.g., 'Sales').
58
+ Derived from view_name by the introspection layer (snake_case → PascalCase).
59
+ fields: All fields discovered in the view, in the order returned by the
60
+ warehouse API. Includes metrics, dimensions, and facts.
61
+ """
62
+
63
+ view_name: str
64
+ class_name: str
65
+ fields: list[IntrospectedField] = field(default_factory=list)