planar 0.11.0__py3-none-any.whl → 0.13.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.
- planar/ai/__init__.py +2 -0
- planar/ai/agent.py +2 -2
- planar/ai/agent_base.py +19 -18
- planar/ai/{state.py → tool_context.py} +3 -3
- planar/app.py +8 -0
- planar/cli.py +43 -1
- planar/config.py +110 -82
- planar/data/__init__.py +1 -0
- planar/data/config.py +12 -1
- planar/data/connection.py +140 -4
- planar/data/dataset.py +13 -7
- planar/data/utils.py +145 -25
- planar/db/alembic/env.py +68 -57
- planar/db/alembic.ini +1 -1
- planar/files/storage/config.py +7 -1
- planar/human/models.py +2 -3
- planar/logging/attributes.py +1 -0
- planar/routers/dataset_router.py +5 -1
- planar/routers/info.py +10 -10
- planar/scaffold_templates/pyproject.toml.j2 +11 -3
- planar/sse/proxy.py +7 -1
- planar/testing/planar_test_client.py +8 -0
- planar/version.py +27 -0
- planar/workflows/notifications.py +5 -1
- planar-0.13.0.dist-info/METADATA +203 -0
- {planar-0.11.0.dist-info → planar-0.13.0.dist-info}/RECORD +28 -27
- {planar-0.11.0.dist-info → planar-0.13.0.dist-info}/WHEEL +1 -1
- planar-0.11.0.dist-info/METADATA +0 -331
- {planar-0.11.0.dist-info → planar-0.13.0.dist-info}/entry_points.txt +0 -0
planar/ai/__init__.py
CHANGED
planar/ai/agent.py
CHANGED
@@ -50,8 +50,8 @@ class AgentWorkflowNotifier(AgentEventEmitter):
|
|
50
50
|
class Agent[
|
51
51
|
TInput: BaseModel | str,
|
52
52
|
TOutput: BaseModel | str,
|
53
|
-
|
54
|
-
](AgentBase[TInput, TOutput,
|
53
|
+
TToolContext,
|
54
|
+
](AgentBase[TInput, TOutput, TToolContext]):
|
55
55
|
model: models.KnownModelName | models.Model = "openai:gpt-4o"
|
56
56
|
|
57
57
|
async def run_step(
|
planar/ai/agent_base.py
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
1
|
import abc
|
4
2
|
from dataclasses import dataclass, field
|
5
3
|
from typing import (
|
@@ -15,7 +13,7 @@ from pydantic import BaseModel
|
|
15
13
|
from pydantic_ai.settings import ModelSettings
|
16
14
|
|
17
15
|
from planar.ai.models import AgentConfig, AgentEventEmitter, AgentRunResult
|
18
|
-
from planar.ai.
|
16
|
+
from planar.ai.tool_context import clear_tool_context, set_tool_context
|
19
17
|
from planar.logging import get_logger
|
20
18
|
from planar.modeling.field_helpers import JsonSchema
|
21
19
|
from planar.utils import P, R, T, U
|
@@ -30,7 +28,7 @@ class AgentBase[
|
|
30
28
|
# TODO: add `= str` default when we upgrade to 3.13
|
31
29
|
TInput: BaseModel | str,
|
32
30
|
TOutput: BaseModel | str,
|
33
|
-
|
31
|
+
TToolContext,
|
34
32
|
](abc.ABC):
|
35
33
|
"""An LLM-powered agent that can be called directly within workflows."""
|
36
34
|
|
@@ -47,7 +45,7 @@ class AgentBase[
|
|
47
45
|
)
|
48
46
|
event_emitter: AgentEventEmitter | None = None
|
49
47
|
durable: bool = True
|
50
|
-
|
48
|
+
tool_context_type: Type[TToolContext] | None = None
|
51
49
|
|
52
50
|
# TODO: move here to serialize to frontend
|
53
51
|
#
|
@@ -94,16 +92,16 @@ class AgentBase[
|
|
94
92
|
|
95
93
|
@overload
|
96
94
|
async def __call__(
|
97
|
-
self: "AgentBase[TInput, str,
|
95
|
+
self: "AgentBase[TInput, str, TToolContext]",
|
98
96
|
input_value: TInput,
|
99
|
-
|
97
|
+
tool_context: TToolContext | None = None,
|
100
98
|
) -> AgentRunResult[str]: ...
|
101
99
|
|
102
100
|
@overload
|
103
101
|
async def __call__(
|
104
|
-
self: "AgentBase[TInput, TOutput,
|
102
|
+
self: "AgentBase[TInput, TOutput, TToolContext]",
|
105
103
|
input_value: TInput,
|
106
|
-
|
104
|
+
tool_context: TToolContext | None = None,
|
107
105
|
) -> AgentRunResult[TOutput]: ...
|
108
106
|
|
109
107
|
def as_step_if_durable(
|
@@ -125,7 +123,7 @@ class AgentBase[
|
|
125
123
|
async def __call__(
|
126
124
|
self,
|
127
125
|
input_value: TInput,
|
128
|
-
|
126
|
+
tool_context: TToolContext | None = None,
|
129
127
|
) -> AgentRunResult[Any]:
|
130
128
|
if self.input_type is not None and not isinstance(input_value, self.input_type):
|
131
129
|
raise ValueError(
|
@@ -153,22 +151,25 @@ class AgentBase[
|
|
153
151
|
return_type=AgentRunResult[self.output_type],
|
154
152
|
)
|
155
153
|
|
156
|
-
if
|
157
|
-
if self.
|
158
|
-
raise ValueError(
|
159
|
-
|
154
|
+
if tool_context is not None:
|
155
|
+
if self.tool_context_type is None:
|
156
|
+
raise ValueError(
|
157
|
+
"tool_context cannot be provided when tool_context_type is not set"
|
158
|
+
)
|
159
|
+
if not isinstance(tool_context, self.tool_context_type):
|
160
160
|
raise ValueError(
|
161
|
-
f"
|
161
|
+
f"tool_context must be of type {self.tool_context_type}, "
|
162
|
+
f"but got {type(tool_context)}"
|
162
163
|
)
|
163
|
-
|
164
|
+
set_tool_context(cast(TToolContext, tool_context))
|
164
165
|
|
165
166
|
try:
|
166
167
|
result = await run_step(input_value=input_value)
|
167
168
|
# Cast the result to ensure type compatibility
|
168
169
|
return cast(AgentRunResult[TOutput], result)
|
169
170
|
finally:
|
170
|
-
if
|
171
|
-
|
171
|
+
if tool_context is not None:
|
172
|
+
clear_tool_context()
|
172
173
|
|
173
174
|
@abc.abstractmethod
|
174
175
|
async def run_step(
|
@@ -5,13 +5,13 @@ from planar.task_local import TaskLocal
|
|
5
5
|
data: TaskLocal[Any] = TaskLocal()
|
6
6
|
|
7
7
|
|
8
|
-
def
|
8
|
+
def set_tool_context(ctx: Any):
|
9
9
|
return data.set(ctx)
|
10
10
|
|
11
11
|
|
12
|
-
def
|
12
|
+
def get_tool_context[T](_: Type[T]) -> T:
|
13
13
|
return cast(T, data.get())
|
14
14
|
|
15
15
|
|
16
|
-
def
|
16
|
+
def clear_tool_context():
|
17
17
|
return data.clear()
|
planar/app.py
CHANGED
@@ -274,6 +274,14 @@ class PlanarApp:
|
|
274
274
|
# Reset the config in the context
|
275
275
|
config_var.reset(config_tok)
|
276
276
|
|
277
|
+
if self.config.data:
|
278
|
+
try:
|
279
|
+
from planar.data.connection import reset_connection_cache
|
280
|
+
except ImportError as exc: # pragma: no cover - optional dependency
|
281
|
+
logger.debug("skipping data connection cleanup", error=str(exc))
|
282
|
+
else:
|
283
|
+
await reset_connection_cache()
|
284
|
+
|
277
285
|
await self.db_manager.disconnect()
|
278
286
|
|
279
287
|
if self.storage:
|
planar/cli.py
CHANGED
@@ -2,6 +2,7 @@ import os
|
|
2
2
|
import subprocess
|
3
3
|
import sys
|
4
4
|
from pathlib import Path
|
5
|
+
from typing import Annotated
|
5
6
|
|
6
7
|
import typer
|
7
8
|
import uvicorn
|
@@ -9,10 +10,35 @@ from jinja2 import Environment as JinjaEnvironment
|
|
9
10
|
from jinja2 import FileSystemLoader
|
10
11
|
|
11
12
|
from planar.config import Environment
|
13
|
+
from planar.version import get_version
|
12
14
|
|
13
15
|
app = typer.Typer(help="Planar CLI tool")
|
14
16
|
|
15
17
|
|
18
|
+
def version_callback(value: bool) -> bool:
|
19
|
+
if value:
|
20
|
+
typer.echo(f"planar {get_version()}")
|
21
|
+
raise typer.Exit()
|
22
|
+
return value
|
23
|
+
|
24
|
+
|
25
|
+
@app.callback()
|
26
|
+
def root_callback(
|
27
|
+
version: Annotated[
|
28
|
+
bool | None,
|
29
|
+
typer.Option(
|
30
|
+
"--version",
|
31
|
+
"-v",
|
32
|
+
help="Show Planar version and exit.",
|
33
|
+
callback=version_callback,
|
34
|
+
is_flag=True,
|
35
|
+
is_eager=True,
|
36
|
+
),
|
37
|
+
] = None,
|
38
|
+
) -> None:
|
39
|
+
"""Entry point for Planar CLI with shared options."""
|
40
|
+
|
41
|
+
|
16
42
|
def find_default_app_path() -> Path:
|
17
43
|
"""Checks for default app file paths (app.py, then main.py)."""
|
18
44
|
for filename in ["app.py", "main.py"]:
|
@@ -236,6 +262,11 @@ def run_command(
|
|
236
262
|
def scaffold_project(
|
237
263
|
name: str = typer.Option(None, "--name", help="Name of the new project"),
|
238
264
|
directory: Path = typer.Option(Path("."), "--directory", help="Target directory"),
|
265
|
+
use_local_source: bool = typer.Option(
|
266
|
+
False,
|
267
|
+
"--use-local-source",
|
268
|
+
help="Use local planar source instead of published package (for development)",
|
269
|
+
),
|
239
270
|
):
|
240
271
|
"""
|
241
272
|
Creates a new Planar project with a basic structure and example workflow.
|
@@ -252,8 +283,19 @@ def scaffold_project(
|
|
252
283
|
template_dir = Path(__file__).parent / "scaffold_templates"
|
253
284
|
jinja_env = JinjaEnvironment(loader=FileSystemLoader(template_dir))
|
254
285
|
|
286
|
+
planar_source_path = None
|
287
|
+
if use_local_source:
|
288
|
+
# Assume we're running from the planar package itself
|
289
|
+
# Go up from planar/cli.py -> planar/ -> planar_repo/
|
290
|
+
planar_source_path = Path(__file__).parent.parent.resolve()
|
291
|
+
typer.echo(f"Using local planar source: {planar_source_path}")
|
292
|
+
|
255
293
|
# Template context
|
256
|
-
context = {
|
294
|
+
context = {
|
295
|
+
"name": name,
|
296
|
+
"local_source": use_local_source,
|
297
|
+
"planar_source_path": planar_source_path,
|
298
|
+
}
|
257
299
|
|
258
300
|
# Create project structure
|
259
301
|
try:
|
planar/config.py
CHANGED
@@ -3,6 +3,7 @@ import logging
|
|
3
3
|
import logging.config
|
4
4
|
import os
|
5
5
|
import sys
|
6
|
+
from contextlib import contextmanager
|
6
7
|
from enum import Enum
|
7
8
|
from pathlib import Path
|
8
9
|
from typing import Annotated, Any, Dict, Literal
|
@@ -24,6 +25,7 @@ from sqlalchemy import URL, make_url
|
|
24
25
|
from planar.data.config import DataConfig
|
25
26
|
from planar.files.storage.config import LocalDirectoryConfig, StorageConfig
|
26
27
|
from planar.logging import get_logger
|
28
|
+
from planar.logging.formatter import StructuredFormatter
|
27
29
|
|
28
30
|
logger = get_logger(__name__)
|
29
31
|
|
@@ -144,12 +146,12 @@ class CorsConfig(BaseModel):
|
|
144
146
|
allow_headers: list[str]
|
145
147
|
|
146
148
|
@model_validator(mode="after")
|
147
|
-
def validate_allow_origins(
|
148
|
-
if
|
149
|
+
def validate_allow_origins(self):
|
150
|
+
if self.allow_credentials and "*" in self.allow_origins:
|
149
151
|
raise ValueError(
|
150
152
|
"allow_credentials cannot be True if allow_origins includes '*'. Must explicitly specify allowed origins."
|
151
153
|
)
|
152
|
-
return
|
154
|
+
return self
|
153
155
|
|
154
156
|
|
155
157
|
LOCAL_CORS_CONFIG = CorsConfig(
|
@@ -173,10 +175,10 @@ class JWTConfig(BaseModel):
|
|
173
175
|
additional_exclusion_paths: list[str] | None = Field(default_factory=list)
|
174
176
|
|
175
177
|
@model_validator(mode="after")
|
176
|
-
def validate_client_id(
|
177
|
-
if not
|
178
|
+
def validate_client_id(self):
|
179
|
+
if not self.client_id or not self.org_id:
|
178
180
|
raise ValueError("Both client_id and org_id required to enable JWT")
|
179
|
-
return
|
181
|
+
return self
|
180
182
|
|
181
183
|
|
182
184
|
# Coplane ORG JWT config
|
@@ -235,12 +237,12 @@ class PlanarConfig(BaseModel):
|
|
235
237
|
model_config = ConfigDict(extra="forbid")
|
236
238
|
|
237
239
|
@model_validator(mode="after")
|
238
|
-
def validate_db_connection_reference(
|
239
|
-
if
|
240
|
+
def validate_db_connection_reference(self):
|
241
|
+
if self.app.db_connection not in self.db_connections:
|
240
242
|
raise ValueError(
|
241
|
-
f"Invalid db_connection reference: {
|
243
|
+
f"Invalid db_connection reference: {self.app.db_connection}"
|
242
244
|
)
|
243
|
-
return
|
245
|
+
return self
|
244
246
|
|
245
247
|
def connection_url(self) -> URL:
|
246
248
|
connection = self.db_connections[self.app.db_connection]
|
@@ -460,6 +462,28 @@ def load_environment_aware_env_vars() -> None:
|
|
460
462
|
return
|
461
463
|
|
462
464
|
|
465
|
+
@contextmanager
|
466
|
+
def _temporary_config_logging():
|
467
|
+
"""Install a console handler so config loading logs are visible before configuration."""
|
468
|
+
root_logger = logging.getLogger()
|
469
|
+
handler = logging.StreamHandler(sys.stderr)
|
470
|
+
handler.setFormatter(StructuredFormatter(use_colors=sys.stderr.isatty()))
|
471
|
+
handler.setLevel(logging.NOTSET)
|
472
|
+
|
473
|
+
previous_level = root_logger.level
|
474
|
+
if root_logger.getEffectiveLevel() > logging.INFO:
|
475
|
+
root_logger.setLevel(logging.INFO)
|
476
|
+
|
477
|
+
root_logger.addHandler(handler)
|
478
|
+
|
479
|
+
try:
|
480
|
+
yield
|
481
|
+
finally:
|
482
|
+
root_logger.removeHandler(handler)
|
483
|
+
handler.close()
|
484
|
+
root_logger.setLevel(previous_level)
|
485
|
+
|
486
|
+
|
463
487
|
def load_environment_aware_config[ConfigClass]() -> PlanarConfig:
|
464
488
|
"""
|
465
489
|
Load configuration based on environment settings, using environment variables
|
@@ -476,83 +500,87 @@ def load_environment_aware_config[ConfigClass]() -> PlanarConfig:
|
|
476
500
|
Raises:
|
477
501
|
InvalidConfigurationError: If configuration loading or validation fails.
|
478
502
|
"""
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
503
|
+
with _temporary_config_logging():
|
504
|
+
load_environment_aware_env_vars()
|
505
|
+
env = get_environment()
|
506
|
+
|
507
|
+
if env == "dev":
|
508
|
+
base_config = sqlite_config(db_path="planar_dev.db")
|
509
|
+
base_config.security = SecurityConfig(cors=LOCAL_CORS_CONFIG)
|
510
|
+
base_config.environment = Environment.DEV
|
511
|
+
else:
|
512
|
+
base_config = sqlite_config(db_path="planar.db")
|
513
|
+
base_config.environment = Environment.PROD
|
514
|
+
base_config.security = SecurityConfig(
|
515
|
+
cors=PROD_CORS_CONFIG, jwt=JWT_COPLANE_CONFIG
|
516
|
+
)
|
492
517
|
|
493
|
-
|
494
|
-
|
495
|
-
|
518
|
+
# Convert base config to dict for merging
|
519
|
+
# Use by_alias=False to work with Python field names before validation
|
520
|
+
base_dict = base_config.model_dump(mode="python", by_alias=False)
|
496
521
|
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
522
|
+
override_config_path = get_config_path()
|
523
|
+
if override_config_path:
|
524
|
+
if not override_config_path.exists():
|
525
|
+
raise InvalidConfigurationError(
|
526
|
+
f"Configuration file not found: {override_config_path}"
|
527
|
+
)
|
528
|
+
else:
|
529
|
+
paths_to_check = []
|
530
|
+
if os.environ.get("PLANAR_ENTRY_POINT"):
|
531
|
+
# Extract the directory from the entry point path
|
532
|
+
entry_point_dir = Path(os.environ["PLANAR_ENTRY_POINT"]).parent
|
533
|
+
paths_to_check = [
|
534
|
+
entry_point_dir / f"planar.{env}.yaml",
|
535
|
+
entry_point_dir / "planar.yaml",
|
536
|
+
]
|
537
|
+
paths_to_check.append(Path(f"planar.{env}.yaml"))
|
538
|
+
paths_to_check.append(Path("planar.yaml"))
|
539
|
+
|
540
|
+
override_config_path = next(
|
541
|
+
(path for path in paths_to_check if path.exists()), None
|
502
542
|
)
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
override_config_path = next(
|
516
|
-
(path for path in paths_to_check if path.exists()), None
|
517
|
-
)
|
518
|
-
if override_config_path is None:
|
519
|
-
logger.warning(
|
520
|
-
"no override config file found, using default config",
|
521
|
-
search_paths=[str(p) for p in paths_to_check],
|
522
|
-
env=env,
|
543
|
+
if override_config_path is None:
|
544
|
+
logger.warning(
|
545
|
+
"no override config file found, using default config",
|
546
|
+
search_paths=[str(p) for p in paths_to_check],
|
547
|
+
env=env,
|
548
|
+
)
|
549
|
+
|
550
|
+
merged_dict = base_dict
|
551
|
+
if override_config_path and override_config_path.exists():
|
552
|
+
logger.info(
|
553
|
+
"using override config file", override_config_path=override_config_path
|
523
554
|
)
|
555
|
+
try:
|
556
|
+
# We can't use load_config_from_file here because we expect
|
557
|
+
# the override config to not be a fully validated PlanarConfig object,
|
558
|
+
# and we need to merge it onto the base default config.
|
559
|
+
with open(override_config_path, "r") as f:
|
560
|
+
override_yaml_str = f.read()
|
561
|
+
|
562
|
+
# Expand environment variables in the YAML string
|
563
|
+
processed_yaml_str = os.path.expandvars(override_yaml_str)
|
564
|
+
logger.debug(
|
565
|
+
"processed override yaml string",
|
566
|
+
processed_yaml_str=processed_yaml_str,
|
567
|
+
)
|
568
|
+
|
569
|
+
override_dict = yaml.safe_load(processed_yaml_str) or {}
|
570
|
+
logger.debug("loaded override config", override_dict=override_dict)
|
571
|
+
|
572
|
+
# Deep merge the override onto the base dictionary
|
573
|
+
merged_dict = deep_merge_dicts(override_dict, base_dict)
|
574
|
+
logger.debug("merged config dict", merged_dict=merged_dict)
|
575
|
+
except yaml.YAMLError as e:
|
576
|
+
raise InvalidConfigurationError(
|
577
|
+
f"Error parsing override configuration file {override_config_path}: {e}"
|
578
|
+
) from e
|
524
579
|
|
525
|
-
merged_dict = base_dict
|
526
|
-
if override_config_path and override_config_path.exists():
|
527
|
-
logger.info(
|
528
|
-
"using override config file", override_config_path=override_config_path
|
529
|
-
)
|
530
580
|
try:
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
with open(override_config_path, "r") as f:
|
535
|
-
override_yaml_str = f.read()
|
536
|
-
|
537
|
-
# Expand environment variables in the YAML string
|
538
|
-
processed_yaml_str = os.path.expandvars(override_yaml_str)
|
539
|
-
logger.debug(
|
540
|
-
"processed override yaml string", processed_yaml_str=processed_yaml_str
|
541
|
-
)
|
542
|
-
|
543
|
-
override_dict = yaml.safe_load(processed_yaml_str) or {}
|
544
|
-
logger.debug("loaded override config", override_dict=override_dict)
|
545
|
-
|
546
|
-
# Deep merge the override onto the base dictionary
|
547
|
-
merged_dict = deep_merge_dicts(override_dict, base_dict)
|
548
|
-
logger.debug("merged config dict", merged_dict=merged_dict)
|
549
|
-
except yaml.YAMLError as e:
|
581
|
+
final_config = PlanarConfig.model_validate(merged_dict)
|
582
|
+
return final_config
|
583
|
+
except ValidationError as e:
|
550
584
|
raise InvalidConfigurationError(
|
551
|
-
f"
|
585
|
+
f"Configuration validation error: {e}"
|
552
586
|
) from e
|
553
|
-
|
554
|
-
try:
|
555
|
-
final_config = PlanarConfig.model_validate(merged_dict)
|
556
|
-
return final_config
|
557
|
-
except ValidationError as e:
|
558
|
-
raise InvalidConfigurationError(f"Configuration validation error: {e}") from e
|
planar/data/__init__.py
CHANGED
planar/data/config.py
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
from typing import Annotated, Literal
|
4
4
|
|
5
|
-
from pydantic import BaseModel, Field
|
5
|
+
from pydantic import BaseModel, ConfigDict, Field
|
6
6
|
|
7
7
|
from planar.files.storage.config import StorageConfig
|
8
8
|
|
@@ -13,6 +13,8 @@ class DuckDBCatalogConfig(BaseModel):
|
|
13
13
|
type: Literal["duckdb"]
|
14
14
|
path: str # Path to .ducklake file
|
15
15
|
|
16
|
+
model_config = ConfigDict(frozen=True)
|
17
|
+
|
16
18
|
|
17
19
|
class PostgresCatalogConfig(BaseModel):
|
18
20
|
"""Configuration for PostgreSQL catalog backend."""
|
@@ -24,6 +26,8 @@ class PostgresCatalogConfig(BaseModel):
|
|
24
26
|
password: str | None = None
|
25
27
|
db: str
|
26
28
|
|
29
|
+
model_config = ConfigDict(frozen=True)
|
30
|
+
|
27
31
|
|
28
32
|
class SQLiteCatalogConfig(BaseModel):
|
29
33
|
"""Configuration for SQLite catalog backend."""
|
@@ -31,6 +35,8 @@ class SQLiteCatalogConfig(BaseModel):
|
|
31
35
|
type: Literal["sqlite"]
|
32
36
|
path: str # Path to .sqlite file
|
33
37
|
|
38
|
+
model_config = ConfigDict(frozen=True)
|
39
|
+
|
34
40
|
|
35
41
|
# Discriminated union for catalog configurations
|
36
42
|
CatalogConfig = Annotated[
|
@@ -47,3 +53,8 @@ class DataConfig(BaseModel):
|
|
47
53
|
|
48
54
|
# Optional settings
|
49
55
|
catalog_name: str = "planar_data" # Default catalog name in Ducklake
|
56
|
+
|
57
|
+
model_config = ConfigDict(frozen=True)
|
58
|
+
|
59
|
+
def is_sqlite_catalog(self) -> bool:
|
60
|
+
return self.catalog.type == "sqlite"
|