squirrels 0.1.0__py3-none-any.whl → 0.6.0.post0__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 (127) hide show
  1. dateutils/__init__.py +6 -0
  2. dateutils/_enums.py +25 -0
  3. squirrels/dateutils.py → dateutils/_implementation.py +409 -380
  4. dateutils/types.py +6 -0
  5. squirrels/__init__.py +21 -18
  6. squirrels/_api_routes/__init__.py +5 -0
  7. squirrels/_api_routes/auth.py +337 -0
  8. squirrels/_api_routes/base.py +196 -0
  9. squirrels/_api_routes/dashboards.py +156 -0
  10. squirrels/_api_routes/data_management.py +148 -0
  11. squirrels/_api_routes/datasets.py +220 -0
  12. squirrels/_api_routes/project.py +289 -0
  13. squirrels/_api_server.py +552 -134
  14. squirrels/_arguments/__init__.py +0 -0
  15. squirrels/_arguments/init_time_args.py +83 -0
  16. squirrels/_arguments/run_time_args.py +111 -0
  17. squirrels/_auth.py +777 -0
  18. squirrels/_command_line.py +239 -107
  19. squirrels/_compile_prompts.py +147 -0
  20. squirrels/_connection_set.py +94 -0
  21. squirrels/_constants.py +141 -64
  22. squirrels/_dashboards.py +179 -0
  23. squirrels/_data_sources.py +570 -0
  24. squirrels/_dataset_types.py +91 -0
  25. squirrels/_env_vars.py +209 -0
  26. squirrels/_exceptions.py +29 -0
  27. squirrels/_http_error_responses.py +52 -0
  28. squirrels/_initializer.py +319 -110
  29. squirrels/_logging.py +121 -0
  30. squirrels/_manifest.py +357 -187
  31. squirrels/_mcp_server.py +578 -0
  32. squirrels/_model_builder.py +69 -0
  33. squirrels/_model_configs.py +74 -0
  34. squirrels/_model_queries.py +52 -0
  35. squirrels/_models.py +1201 -0
  36. squirrels/_package_data/base_project/.env +7 -0
  37. squirrels/_package_data/base_project/.env.example +44 -0
  38. squirrels/_package_data/base_project/connections.yml +16 -0
  39. squirrels/_package_data/base_project/dashboards/dashboard_example.py +40 -0
  40. squirrels/_package_data/base_project/dashboards/dashboard_example.yml +22 -0
  41. squirrels/_package_data/base_project/docker/.dockerignore +16 -0
  42. squirrels/_package_data/base_project/docker/Dockerfile +16 -0
  43. squirrels/_package_data/base_project/docker/compose.yml +7 -0
  44. squirrels/_package_data/base_project/duckdb_init.sql +10 -0
  45. squirrels/_package_data/base_project/gitignore +13 -0
  46. squirrels/_package_data/base_project/macros/macros_example.sql +17 -0
  47. squirrels/_package_data/base_project/models/builds/build_example.py +26 -0
  48. squirrels/_package_data/base_project/models/builds/build_example.sql +16 -0
  49. squirrels/_package_data/base_project/models/builds/build_example.yml +57 -0
  50. squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +17 -0
  51. squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +32 -0
  52. squirrels/_package_data/base_project/models/federates/federate_example.py +51 -0
  53. squirrels/_package_data/base_project/models/federates/federate_example.sql +21 -0
  54. squirrels/_package_data/base_project/models/federates/federate_example.yml +65 -0
  55. squirrels/_package_data/base_project/models/sources.yml +38 -0
  56. squirrels/_package_data/base_project/parameters.yml +142 -0
  57. squirrels/_package_data/base_project/pyconfigs/connections.py +19 -0
  58. squirrels/_package_data/base_project/pyconfigs/context.py +96 -0
  59. squirrels/_package_data/base_project/pyconfigs/parameters.py +141 -0
  60. squirrels/_package_data/base_project/pyconfigs/user.py +56 -0
  61. squirrels/_package_data/base_project/resources/expenses.db +0 -0
  62. squirrels/_package_data/base_project/resources/public/.gitkeep +0 -0
  63. squirrels/_package_data/base_project/resources/weather.db +0 -0
  64. squirrels/_package_data/base_project/seeds/seed_categories.csv +6 -0
  65. squirrels/_package_data/base_project/seeds/seed_categories.yml +15 -0
  66. squirrels/_package_data/base_project/seeds/seed_subcategories.csv +15 -0
  67. squirrels/_package_data/base_project/seeds/seed_subcategories.yml +21 -0
  68. squirrels/_package_data/base_project/squirrels.yml.j2 +61 -0
  69. squirrels/_package_data/base_project/tmp/.gitignore +2 -0
  70. squirrels/_package_data/templates/login_successful.html +53 -0
  71. squirrels/_package_data/templates/squirrels_studio.html +22 -0
  72. squirrels/_package_loader.py +29 -0
  73. squirrels/_parameter_configs.py +592 -0
  74. squirrels/_parameter_options.py +348 -0
  75. squirrels/_parameter_sets.py +207 -0
  76. squirrels/_parameters.py +1703 -0
  77. squirrels/_project.py +796 -0
  78. squirrels/_py_module.py +122 -0
  79. squirrels/_request_context.py +33 -0
  80. squirrels/_schemas/__init__.py +0 -0
  81. squirrels/_schemas/auth_models.py +83 -0
  82. squirrels/_schemas/query_param_models.py +70 -0
  83. squirrels/_schemas/request_models.py +26 -0
  84. squirrels/_schemas/response_models.py +286 -0
  85. squirrels/_seeds.py +97 -0
  86. squirrels/_sources.py +112 -0
  87. squirrels/_utils.py +540 -149
  88. squirrels/_version.py +1 -3
  89. squirrels/arguments.py +7 -0
  90. squirrels/auth.py +4 -0
  91. squirrels/connections.py +3 -0
  92. squirrels/dashboards.py +3 -0
  93. squirrels/data_sources.py +14 -282
  94. squirrels/parameter_options.py +13 -189
  95. squirrels/parameters.py +14 -801
  96. squirrels/types.py +18 -0
  97. squirrels-0.6.0.post0.dist-info/METADATA +148 -0
  98. squirrels-0.6.0.post0.dist-info/RECORD +101 -0
  99. {squirrels-0.1.0.dist-info → squirrels-0.6.0.post0.dist-info}/WHEEL +1 -2
  100. {squirrels-0.1.0.dist-info → squirrels-0.6.0.post0.dist-info}/entry_points.txt +1 -0
  101. squirrels-0.6.0.post0.dist-info/licenses/LICENSE +201 -0
  102. squirrels/_credentials_manager.py +0 -87
  103. squirrels/_module_loader.py +0 -37
  104. squirrels/_parameter_set.py +0 -151
  105. squirrels/_renderer.py +0 -286
  106. squirrels/_timed_imports.py +0 -37
  107. squirrels/connection_set.py +0 -126
  108. squirrels/package_data/base_project/.gitignore +0 -4
  109. squirrels/package_data/base_project/connections.py +0 -21
  110. squirrels/package_data/base_project/database/sample_database.db +0 -0
  111. squirrels/package_data/base_project/database/seattle_weather.db +0 -0
  112. squirrels/package_data/base_project/datasets/sample_dataset/context.py +0 -8
  113. squirrels/package_data/base_project/datasets/sample_dataset/database_view1.py +0 -23
  114. squirrels/package_data/base_project/datasets/sample_dataset/database_view1.sql.j2 +0 -7
  115. squirrels/package_data/base_project/datasets/sample_dataset/final_view.py +0 -10
  116. squirrels/package_data/base_project/datasets/sample_dataset/final_view.sql.j2 +0 -2
  117. squirrels/package_data/base_project/datasets/sample_dataset/parameters.py +0 -30
  118. squirrels/package_data/base_project/datasets/sample_dataset/selections.cfg +0 -6
  119. squirrels/package_data/base_project/squirrels.yaml +0 -26
  120. squirrels/package_data/static/favicon.ico +0 -0
  121. squirrels/package_data/static/script.js +0 -234
  122. squirrels/package_data/static/style.css +0 -110
  123. squirrels/package_data/templates/index.html +0 -32
  124. squirrels-0.1.0.dist-info/LICENSE +0 -22
  125. squirrels-0.1.0.dist-info/METADATA +0 -67
  126. squirrels-0.1.0.dist-info/RECORD +0 -40
  127. squirrels-0.1.0.dist-info/top_level.txt +0 -1
@@ -0,0 +1,122 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Type, Optional, Any, Iterator
4
+ import importlib.util
5
+ from contextlib import contextmanager
6
+ from pathlib import Path
7
+ import sys
8
+
9
+ from . import _constants as c, _utils as u
10
+ from ._exceptions import ConfigurationError, FileExecutionError
11
+
12
+
13
+ @contextmanager
14
+ def _temporary_sys_path(path: str) -> Iterator[None]:
15
+ """
16
+ Temporarily prepend `path` to sys.path for the duration of the context.
17
+ """
18
+ resolved = str(Path(path).resolve())
19
+ prior = list(sys.path)
20
+ try:
21
+ if resolved in sys.path:
22
+ # Ensure it is first, so imports resolve to this project before anything else.
23
+ sys.path.remove(resolved)
24
+ sys.path.insert(0, resolved)
25
+ yield
26
+ finally:
27
+ sys.path[:] = prior
28
+
29
+
30
+ @contextmanager
31
+ def _temporary_sys_modules(prefixes: tuple[str, ...]) -> Iterator[None]:
32
+ """
33
+ Temporarily isolate sys.modules entries for certain prefixes.
34
+
35
+ This prevents cross-project pollution when multiple SquirrelsProject instances
36
+ in the same process import identically named packages (e.g. "pyconfigs").
37
+ """
38
+ def _matches(name: str) -> bool:
39
+ return any(name == p or name.startswith(p + ".") for p in prefixes)
40
+
41
+ saved: dict[str, Any] = {k: v for k, v in sys.modules.items() if _matches(k)}
42
+ try:
43
+ # Remove matching modules so imports during this block re-resolve.
44
+ for k in list(sys.modules.keys()):
45
+ if _matches(k):
46
+ sys.modules.pop(k, None)
47
+ yield
48
+ finally:
49
+ # Remove any modules added during this block for the prefixes.
50
+ for k in list(sys.modules.keys()):
51
+ if _matches(k):
52
+ sys.modules.pop(k, None)
53
+ # Restore prior state.
54
+ sys.modules.update(saved)
55
+
56
+
57
+ class PyModule:
58
+ def __init__(
59
+ self,
60
+ filepath: u.FilePath,
61
+ project_path: str,
62
+ *,
63
+ default_class: Optional[Type] = None,
64
+ is_required: bool = False,
65
+ ) -> None:
66
+ """
67
+ Constructor for PyModule, an abstract module for a file that may or may not exist
68
+
69
+ Arguments:
70
+ filepath (str | pathlib.Path): The file path to the python module
71
+ project_path: The root folder of the Squirrels project. If provided, it is temporarily
72
+ added to sys.path while executing the module so imports like `from pyconfigs import user`
73
+ can work without globally mutating sys.path.
74
+ is_required: If true, throw an error if the file path doesn't exist
75
+ """
76
+ self.filepath = str(filepath)
77
+ try:
78
+ with _temporary_sys_path(project_path), _temporary_sys_modules((c.PYCONFIGS_FOLDER, c.PACKAGES_FOLDER)):
79
+ spec = importlib.util.spec_from_file_location(self.filepath, self.filepath)
80
+ assert spec is not None and spec.loader is not None
81
+ self.module = importlib.util.module_from_spec(spec)
82
+ spec.loader.exec_module(self.module)
83
+ except FileNotFoundError as e:
84
+ if is_required:
85
+ raise ConfigurationError(f"Required file not found: '{self.filepath}'") from e
86
+ self.module = default_class
87
+
88
+ def get_func_or_class(self, attr_name: str, *, default_attr: Any = None, is_required: bool = True) -> Any:
89
+ """
90
+ Get an attribute of the module. Usually a python function or class.
91
+
92
+ Arguments:
93
+ attr_name: The attribute name
94
+ default_attr: The default function or class to use if the attribute cannot be found
95
+ is_required: If true, throw an error if the attribute cannot be found, unless default_attr is not None
96
+
97
+ Returns:
98
+ The attribute of the module
99
+ """
100
+ func_or_class = getattr(self.module, attr_name, default_attr)
101
+ if func_or_class is None and is_required:
102
+ raise ConfigurationError(f"Module '{self.filepath}' missing required attribute '{attr_name}'")
103
+ return func_or_class
104
+
105
+
106
+ def run_pyconfig_main(project_path: str, filename: str, kwargs: dict[str, Any] = {}) -> Any | None:
107
+ """
108
+ Given a python file in the 'pyconfigs' folder, run its main function
109
+
110
+ Arguments:
111
+ project_path: The base path of the project
112
+ filename: The name of the file to run main function
113
+ kwargs: Dictionary of the main function arguments
114
+ """
115
+ filepath = u.Path(project_path, c.PYCONFIGS_FOLDER, filename)
116
+ module = PyModule(filepath, project_path)
117
+ main_function = module.get_func_or_class(c.MAIN_FUNC, is_required=False)
118
+ if main_function:
119
+ try:
120
+ return main_function(**kwargs)
121
+ except Exception as e:
122
+ raise FileExecutionError(f'Failed to run python file "{filepath}"', e) from e
@@ -0,0 +1,33 @@
1
+ """
2
+ Request context management using ContextVars for request-scoped data.
3
+ Provides thread-safe and async-safe access to request IDs throughout the request lifecycle.
4
+ """
5
+ from contextvars import ContextVar
6
+ import uuid
7
+ import base64
8
+
9
+ # ContextVar for storing the current request ID
10
+ _request_id: ContextVar[str | None] = ContextVar("request_id", default=None)
11
+
12
+
13
+ def get_request_id() -> str | None:
14
+ """
15
+ Get the current request ID from the context.
16
+
17
+ Returns:
18
+ The request ID string if available, None otherwise (e.g., in background tasks).
19
+ """
20
+ return _request_id.get()
21
+
22
+
23
+ def set_request_id() -> str:
24
+ """
25
+ Set a new request ID in the context.
26
+ Uses base64 URL-safe encoding of UUID bytes to create a shorter ID (22 chars vs 36).
27
+
28
+ Returns:
29
+ The request ID that was set.
30
+ """
31
+ request_id = base64.urlsafe_b64encode(uuid.uuid4().bytes).decode().rstrip('=')
32
+ _request_id.set(request_id)
33
+ return request_id
File without changes
@@ -0,0 +1,83 @@
1
+ from typing import Callable, Any, Literal
2
+ from datetime import datetime
3
+ from pydantic import BaseModel, ConfigDict, Field, field_serializer
4
+
5
+
6
+ class CustomUserFields(BaseModel):
7
+ """
8
+ Extend this class to add custom user fields.
9
+ - Only the following types are supported: [str, int, float, bool, typing.Literal]
10
+ - Add "| None" after the type to make it nullable.
11
+ - Always set a default value for the column (use None if default is null).
12
+ """
13
+ model_config = ConfigDict(extra='allow')
14
+
15
+
16
+ class AbstractUser(BaseModel):
17
+ model_config = ConfigDict(from_attributes=True)
18
+ username: str
19
+ access_level: Literal["admin", "member", "guest"]
20
+ custom_fields: CustomUserFields
21
+
22
+ def __hash__(self):
23
+ return hash(self.username)
24
+
25
+ def __str__(self):
26
+ return self.username
27
+
28
+
29
+ class GuestUser(AbstractUser):
30
+ access_level: Literal["guest"] = "guest"
31
+
32
+
33
+ class RegisteredUser(AbstractUser):
34
+ access_level: Literal["admin", "member"] = "member"
35
+
36
+
37
+ class ApiKey(BaseModel):
38
+ model_config = ConfigDict(from_attributes=True)
39
+ id: str
40
+ last_four: str
41
+ title: str
42
+ username: str
43
+ created_at: datetime
44
+ expires_at: datetime
45
+
46
+ @field_serializer('created_at', 'expires_at')
47
+ def serialize_datetime(self, dt: datetime) -> str:
48
+ return dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
49
+
50
+
51
+ class UserField(BaseModel):
52
+ name: str = Field(description="The name of the field")
53
+ label: str = Field(description="The human-friendly display name for the field")
54
+ type: str = Field(description="The type of the field")
55
+ nullable: bool = Field(description="Whether the field is nullable")
56
+ enum: list[str] | None = Field(description="The possible values of the field (or None if not applicable)")
57
+ default: Any | None = Field(description="The default value of the field (or None if field is required)")
58
+
59
+
60
+ class UserFieldsModel(BaseModel):
61
+ username: UserField = Field(description="The username field metadata")
62
+ access_level: UserField = Field(description="The access level field metadata")
63
+ custom_fields: list[UserField] = Field(description="The list of custom user fields metadata for the current Squirrels project")
64
+
65
+
66
+ class ProviderConfigs(BaseModel):
67
+ client_id: str
68
+ client_secret: str
69
+ server_url: str
70
+ server_metadata_path: str = Field(default="/.well-known/oauth-authorization-server")
71
+ client_kwargs: dict = Field(default_factory=dict)
72
+ get_user: Callable[[dict], RegisteredUser]
73
+
74
+ @property
75
+ def server_metadata_url(self) -> str:
76
+ return f"{self.server_url}{self.server_metadata_path}"
77
+
78
+
79
+ class AuthProvider(BaseModel):
80
+ name: str = Field(description="The name of the provider")
81
+ label: str = Field(description="The human-friendly display name for the provider")
82
+ icon: str = Field(description="The URL of the provider's icon. Can also start with '/public/' to indicate a file in the '/resources/public/' directory.")
83
+ provider_configs: ProviderConfigs
@@ -0,0 +1,70 @@
1
+ """
2
+ Query model generation utilities for API routes
3
+ """
4
+ from typing import Annotated
5
+ from dataclasses import make_dataclass
6
+ from fastapi import Depends
7
+ from pydantic import create_model
8
+
9
+ from .._parameter_configs import APIParamFieldInfo
10
+
11
+
12
+ def _get_query_models_helper(
13
+ predefined_params: list[APIParamFieldInfo], param_fields: dict[str, APIParamFieldInfo], scoped_parameters: list[str] | None = None
14
+ ):
15
+ """Helper function to generate query models"""
16
+ if scoped_parameters is None:
17
+ scoped_parameters = list(param_fields.keys())
18
+
19
+ QueryModelForGetRaw = make_dataclass("QueryParams", [
20
+ param_fields[param].as_query_info() for param in scoped_parameters
21
+ ] + [param.as_query_info() for param in predefined_params])
22
+ QueryModelForGet = Annotated[QueryModelForGetRaw, Depends()]
23
+
24
+ field_definitions = {param: param_fields[param].as_body_info() for param in scoped_parameters}
25
+ for param in predefined_params:
26
+ field_definitions[param.name] = param.as_body_info()
27
+ QueryModelForPost = create_model("RequestBodyParams", **field_definitions) # type: ignore
28
+ return QueryModelForGet, QueryModelForPost
29
+
30
+
31
+ def get_query_models_for_parameters(param_fields: dict[str, APIParamFieldInfo], scoped_parameters: list[str] | None = None):
32
+ """Generate query models for parameter endpoints"""
33
+ predefined_params = [
34
+ APIParamFieldInfo("x_parent_param", str, description="The parent parameter name used for parameter updates. If no query parameter name matches the parent parameter, then an empty list is used (which is a valid selection for multi-select parameters)"),
35
+ ]
36
+ return _get_query_models_helper(predefined_params, param_fields, scoped_parameters)
37
+
38
+
39
+ def get_query_models_for_dataset(param_fields: dict[str, APIParamFieldInfo], scoped_parameters: list[str] | None = None):
40
+ """Generate query models for dataset endpoints"""
41
+ predefined_params = [
42
+ APIParamFieldInfo("x_sql_query", str, description="Optional Polars SQL to transform the final dataset. Use table name 'result' to reference the dataset."),
43
+ APIParamFieldInfo("x_orientation", str, default="records", description="Controls the orientation of the result data. Options: 'records' (default), 'rows', 'columns'"),
44
+ APIParamFieldInfo("x_offset", int, default=0, description="The number of rows to skip before returning data (applied after data caching)"),
45
+ APIParamFieldInfo("x_limit", int, default=1000, description="The maximum number of rows to return (applied after data caching and offset)"),
46
+ ]
47
+ return _get_query_models_helper(predefined_params, param_fields, scoped_parameters)
48
+
49
+
50
+ def get_query_models_for_dashboard(param_fields: dict[str, APIParamFieldInfo], scoped_parameters: list[str] | None = None):
51
+ """Generate query models for dashboard endpoints"""
52
+ predefined_params = []
53
+ return _get_query_models_helper(predefined_params, param_fields, scoped_parameters)
54
+
55
+
56
+ def get_query_models_for_querying_models(param_fields: dict[str, APIParamFieldInfo]):
57
+ """Generate query models for querying data models"""
58
+ predefined_params = [
59
+ APIParamFieldInfo("x_sql_query", str, description="The SQL query to execute on the data models"),
60
+ APIParamFieldInfo("x_orientation", str, default="records", description="Controls the orientation of the result data. Options: 'records' (default), 'rows', 'columns'"),
61
+ APIParamFieldInfo("x_offset", int, default=0, description="The number of rows to skip before returning data (applied after data caching)"),
62
+ APIParamFieldInfo("x_limit", int, default=1000, description="The maximum number of rows to return (applied after data caching and offset)"),
63
+ ]
64
+ return _get_query_models_helper(predefined_params, param_fields)
65
+
66
+
67
+ def get_query_models_for_compiled_models(param_fields: dict[str, APIParamFieldInfo]):
68
+ """Generate query models for fetching compiled model SQL"""
69
+ predefined_params = []
70
+ return _get_query_models_helper(predefined_params, param_fields)
@@ -0,0 +1,26 @@
1
+ from pydantic import BaseModel, Field, field_validator
2
+
3
+
4
+ class McpRequestHeaders(BaseModel):
5
+ raw_headers: dict[str, str] = Field(default_factory=dict, description="The raw HTTP headers")
6
+
7
+ @field_validator("raw_headers")
8
+ @classmethod
9
+ def lowercase_keys(cls, v: dict[str, str]) -> dict[str, str]:
10
+ return {key.lower(): val for key, val in v.items()}
11
+
12
+ @property
13
+ def bearer_token(self) -> str | None:
14
+ auth_header = self.raw_headers.get("authorization", "")
15
+ if auth_header.lower().startswith("bearer "):
16
+ return auth_header[7:].strip()
17
+ return None
18
+
19
+ @property
20
+ def api_key(self) -> str | None:
21
+ return self.raw_headers.get("x-api-key")
22
+
23
+ @property
24
+ def feature_flags(self) -> set[str]:
25
+ feature_flags_str = self.raw_headers.get("x-feature-flags", "")
26
+ return set(x.strip() for x in feature_flags_str.split(",") if x.strip())
@@ -0,0 +1,286 @@
1
+ from typing import Annotated, Literal, Any
2
+ from textwrap import dedent
3
+ from pydantic import BaseModel, Field
4
+ from datetime import date
5
+
6
+ from .. import _model_configs as mc, _sources as s, _constants as c
7
+
8
+
9
+ ## Simple Auth Response Models
10
+
11
+ class ApiKeyResponse(BaseModel):
12
+ api_key: Annotated[str, Field(examples=["sqrl-12345678"], description="The API key to use subsequent API requests")]
13
+
14
+ class ProviderResponse(BaseModel):
15
+ name: Annotated[str, Field(examples=["my_provider"], description="The name of the provider")]
16
+ label: Annotated[str, Field(examples=["My Provider"], description="The human-friendly display name for the provider")]
17
+ icon: Annotated[str, Field(examples=["https://example.com/my_provider_icon.png"], description="The URL of the provider's icon. If the path starts with '/public/', it is assumed to be a static file in the project's 'resources/public/' directory.")]
18
+ login_url: Annotated[str, Field(examples=["https://example.com/my_provider_login"], description="The URL to redirect to for provider login")]
19
+
20
+
21
+ ## Parameters Response Models
22
+
23
+ class ParameterOptionModel(BaseModel):
24
+ id: Annotated[str, Field(description="The unique identifier for the option")]
25
+ label: Annotated[str, Field(description="The human-friendly display name for the option")]
26
+
27
+ class ParameterModelBase(BaseModel):
28
+ widget_type: Annotated[str, Field(description="The parameter type")]
29
+ name: Annotated[str, Field(description="The name of the parameter. Use this as the key when providing the API request parameters")]
30
+ label: Annotated[str, Field(description="The human-friendly display name for the parameter")]
31
+ description: Annotated[str, Field(description="The description of the parameter")]
32
+
33
+ class NoneParameterModel(ParameterModelBase):
34
+ pass
35
+
36
+ class SelectParameterModel(ParameterModelBase):
37
+ options: Annotated[list[ParameterOptionModel], Field(description="The list of dropdown options as JSON objects containing 'id' and 'label' fields")]
38
+ trigger_refresh: Annotated[bool, Field(description="A boolean that's set to true for parent parameters that require a new parameters API call when the selection changes")]
39
+
40
+ class SingleSelectParameterModel(SelectParameterModel):
41
+ widget_type: Annotated[Literal["single_select"], Field(description="The parameter type")]
42
+ selected_id: Annotated[str | None, Field(description="The ID of the selected / default option")]
43
+
44
+ class MultiSelectParameterModel(SelectParameterModel):
45
+ widget_type: Annotated[Literal["multi_select"], Field(description="The parameter type")]
46
+ show_select_all: Annotated[bool, Field(description="A boolean for whether there should be a toggle to select all options")]
47
+ order_matters: Annotated[bool, Field(description="A boolean for whether the ordering of the input selections would affect the result of the dataset")]
48
+ selected_ids: Annotated[list[str], Field(description="A list of ids of the selected / default options")]
49
+
50
+ class _DateTypeParameterModel(ParameterModelBase):
51
+ min_date: Annotated[date | None, Field(description='A string in "yyyy-MM-dd" format for the minimum date')]
52
+ max_date: Annotated[date | None, Field(description='A string in "yyyy-MM-dd" format for the maximum date')]
53
+
54
+ class DateParameterModel(_DateTypeParameterModel):
55
+ widget_type: Annotated[Literal["date"], Field(description="The parameter type")]
56
+ selected_date: Annotated[date, Field(description='A string in "yyyy-MM-dd" format for the selected / default date')]
57
+
58
+ class DateRangeParameterModel(_DateTypeParameterModel):
59
+ widget_type: Annotated[Literal["date_range"], Field(description="The parameter type")]
60
+ selected_start_date: Annotated[date, Field(description='A string in "yyyy-MM-dd" format for the selected / default start date')]
61
+ selected_end_date: Annotated[date, Field(description='A string in "yyyy-MM-dd" format for the selected / default end date')]
62
+
63
+ class _NumericParameterModel(ParameterModelBase):
64
+ min_value: Annotated[float, Field(description="A number for the lower bound of the selectable number")]
65
+ max_value: Annotated[float, Field(description="A number for the upper bound of the selectable number")]
66
+ increment: Annotated[float, Field(description="A number for the selectable increments between the lower bound and upper bound")]
67
+
68
+ class NumberParameterModel(_NumericParameterModel):
69
+ widget_type: Annotated[Literal["number"], Field(description="The parameter type")]
70
+ selected_value: Annotated[float, Field(description="A number for the selected / default number")]
71
+
72
+ class NumberRangeParameterModel(_NumericParameterModel):
73
+ widget_type: Annotated[Literal["number_range"], Field(description="The parameter type")]
74
+ selected_lower_value: Annotated[float, Field(description="A number for the selected / default lower number")]
75
+ selected_upper_value: Annotated[float, Field(description="A number for the selected / default upper number")]
76
+
77
+ class TextParameterModel(ParameterModelBase):
78
+ widget_type: Annotated[Literal["text"], Field(description="The parameter type")]
79
+ entered_text: Annotated[str, Field(description="A string for the default entered text")]
80
+ input_type: Annotated[str, Field(
81
+ description='A string for the input type (one of "text", "textarea", "number", "date", "datetime-local", "month", "time", "color", or "password")'
82
+ )]
83
+
84
+ ParametersListType = list[
85
+ NoneParameterModel | SingleSelectParameterModel | MultiSelectParameterModel | DateParameterModel | DateRangeParameterModel |
86
+ NumberParameterModel | NumberRangeParameterModel | TextParameterModel
87
+ ]
88
+
89
+ class ParametersModel(BaseModel):
90
+ parameters: Annotated[ParametersListType, Field(description="The list of parameters for the dataset / dashboard")]
91
+
92
+
93
+ ## Datasets / Dashboards Catalog Response Models
94
+
95
+ name_description = "The name of the dataset / dashboard (usually in snake case with underscores)"
96
+ name_for_api_description = "The name of the dataset / dashboard for the API (with dashes instead of underscores)"
97
+ label_description = "The human-friendly display name for the dataset / dashboard"
98
+ description_description = "The description of the dataset / dashboard"
99
+ parameters_path_description = "The API path to the parameters for the dataset / dashboard"
100
+ metadata_path_description = "The API path to the metadata (i.e., description and schema) for the dataset"
101
+ result_path_description = "The API path to the results for the dataset / dashboard"
102
+
103
+ class ConfigurableOverrideModel(BaseModel):
104
+ name: str
105
+ default: str
106
+
107
+ class ConfigurableItemModel(ConfigurableOverrideModel):
108
+ label: str
109
+ description: str
110
+
111
+ class ColumnModel(BaseModel):
112
+ name: Annotated[str, Field(examples=["mycol"], description="Name of column")]
113
+ type: Annotated[str, Field(examples=["string", "integer", "boolean", "datetime"], description='Column type (such as "string", "integer", "boolean", "datetime", etc.)')]
114
+ description: Annotated[str, Field(examples=["My column description"], description="The description of the column")]
115
+ category: Annotated[str, Field(examples=["dimension", "measure", "misc"], description="The category of the column (such as 'dimension', 'measure', or 'misc')")]
116
+
117
+ class ColumnWithConditionModel(ColumnModel):
118
+ condition: Annotated[list[str] | None, Field(default=None, examples=[["My condition"]], description="The condition(s) of when the field is included (such as based on a parameter selection)")]
119
+
120
+ class SchemaModel(BaseModel):
121
+ fields: Annotated[list[ColumnModel], Field(description="A list of JSON objects containing the 'name' and 'type' for each of the columns in the result")]
122
+
123
+ class SchemaWithConditionModel(BaseModel):
124
+ fields: Annotated[list[ColumnWithConditionModel], Field(description="A list of JSON objects containing the 'name' and 'type' for each of the columns in the result")]
125
+
126
+ class DatasetItemModelForMcp(BaseModel):
127
+ name: Annotated[str, Field(examples=["my_dataset"], description=name_description)]
128
+ label: Annotated[str, Field(examples=["My Dataset"], description=label_description)]
129
+ description: Annotated[str, Field(examples=[""], description=description_description)]
130
+ parameters: Annotated[list[str], Field(examples=[["myparam1", "myparam2"]], description="The list of parameter names used by the dataset. If the list is empty, the dataset does not accept any parameters.")]
131
+ data_schema: Annotated[SchemaWithConditionModel, Field(alias="schema", description="JSON object describing the schema of the dataset")]
132
+
133
+ class DatasetItemModel(DatasetItemModelForMcp):
134
+ name_for_api: Annotated[str, Field(examples=["my-dataset"], description=name_for_api_description)]
135
+ configurables: Annotated[list[ConfigurableOverrideModel], Field(default_factory=list, description="The list of configurables with their default values")]
136
+
137
+ class DashboardItemModel(ParametersModel):
138
+ name: Annotated[str, Field(examples=["mydashboard"], description=name_description)]
139
+ name_for_api: Annotated[str, Field(examples=["my-dashboard"], description=name_for_api_description)]
140
+ label: Annotated[str, Field(examples=["My Dashboard"], description=label_description)]
141
+ description: Annotated[str, Field(examples=[""], description=description_description)]
142
+ configurables: Annotated[list[ConfigurableOverrideModel], Field(default_factory=list, description="The list of configurables with their default values")]
143
+ parameters: Annotated[list[str], Field(examples=[["myparam1", "myparam2"]], description="The list of parameter names used by the dashboard")]
144
+ result_format: Annotated[str, Field(examples=["png", "html"], description="The format of the dashboard's result API response (one of 'png' or 'html')")]
145
+
146
+ ModelConfigType = mc.ModelConfig | s.Source | mc.SeedConfig | mc.BuildModelConfig | mc.DbviewModelConfig | mc.FederateModelConfig
147
+
148
+ class ConnectionItemModel(BaseModel):
149
+ name: Annotated[str, Field(examples=["myconnection"], description="The name of the connection")]
150
+ label: Annotated[str, Field(examples=["My Connection"], description="The human-friendly display name for the connection")]
151
+
152
+ class DataModelItem(BaseModel):
153
+ name: Annotated[str, Field(examples=["model_name"], description="The name of the model")]
154
+ model_type: Annotated[Literal["source", "dbview", "federate", "seed", "build"], Field(
155
+ examples=["source", "dbview", "federate", "seed", "build"], description="The type of the model"
156
+ )]
157
+ config: Annotated[ModelConfigType, Field(description="The configuration of the model")]
158
+ is_queryable: Annotated[bool, Field(examples=[True], description="Whether the model is queryable")]
159
+
160
+ class LineageNode(BaseModel):
161
+ name: str
162
+ type: Literal["model", "dataset", "dashboard"]
163
+
164
+ class LineageRelation(BaseModel):
165
+ type: Literal["buildtime", "runtime"]
166
+ source: LineageNode
167
+ target: LineageNode
168
+
169
+ class CatalogModelForMcp(BaseModel):
170
+ parameters: Annotated[ParametersListType, Field(description="The list of all parameters in the project. It is possible that not all parameters are used by a dataset.")]
171
+ datasets: Annotated[list[DatasetItemModelForMcp], Field(description="The list of accessible datasets")]
172
+
173
+ class CatalogModel(CatalogModelForMcp):
174
+ datasets: Annotated[list[DatasetItemModel], Field(description="The list of accessible datasets")]
175
+ dashboards: Annotated[list[DashboardItemModel], Field(description="The list of accessible dashboards")]
176
+ connections: Annotated[list[ConnectionItemModel], Field(description="The list of connections in the project (only provided for admin users)")]
177
+ models: Annotated[list[DataModelItem], Field(description="The list of data models in the project (only provided for admin users)")]
178
+ lineage: Annotated[list[LineageRelation], Field(description="The lineage information between data assets (only provided for admin users)")]
179
+ configurables: Annotated[list[ConfigurableItemModel], Field(description="The list of configurables (only provided for admin users)")]
180
+
181
+
182
+ ## Dataset Results Response Models
183
+
184
+ class DataDetailsModel(BaseModel):
185
+ num_rows: Annotated[int, Field(description="The number of rows in the data field")]
186
+ orientation: Annotated[Literal["records", "rows", "columns"], Field(description="The orientation of the data field")]
187
+
188
+ class DatasetResultModel(BaseModel):
189
+ data_schema: Annotated[SchemaModel, Field(alias="schema", description="JSON object describing the schema of the dataset")]
190
+ total_num_rows: Annotated[int, Field(description="The total number of rows for the dataset")]
191
+ data_details: Annotated[DataDetailsModel, Field(description="A JSON object containing the details of the data field")]
192
+ data: Annotated[list[dict] | list[list] | dict[str, list], Field(
193
+ description=dedent("""
194
+ The data payload.
195
+ - If orientation is 'records', this is a list of JSON objects.
196
+ - If orientation is 'rows', this is a list of rows. Each row is a list of values in the same order as the columns in `schema`.
197
+ - If orientation is 'columns', this is a JSON object where keys are column names and values are columns. Each column is a list of values from the first to last row.
198
+ """).strip()
199
+ )]
200
+
201
+
202
+ ## Compiled Query Response Model
203
+
204
+ class CompiledQueryModel(BaseModel):
205
+ language: Annotated[Literal["sql", "python"], Field(description="The language of the data model query: 'sql' or 'python'")]
206
+ definition: Annotated[str, Field("", description="The compiled SQL or Python definition of the data model.")]
207
+ placeholders: Annotated[dict[str, Any], Field({}, description="The placeholders for the data model.")]
208
+
209
+
210
+ ## Project Metadata Response Models
211
+
212
+ SAMPLE_BASE_URL = "https://example.com/analytics/project/v1"
213
+ SAMPLE_BASE_URL_API = f"{SAMPLE_BASE_URL}{c.LATEST_API_VERSION_MOUNT_PATH}"
214
+
215
+ class PasswordRequirements(BaseModel):
216
+ min_length: Annotated[int, Field(default=8, description="The minimum length of the password")]
217
+ max_length: Annotated[int, Field(default=64, description="The maximum length of the password")] # For bcrypt, the max length must be 72 or less
218
+
219
+ class ApiRoutesModel(BaseModel):
220
+ # Data catalog and assets routes
221
+ get_data_catalog_url: Annotated[str, Field(examples=[f"{SAMPLE_BASE_URL_API}/data-catalog"])]
222
+ get_parameters_url: Annotated[str, Field(examples=[f"{SAMPLE_BASE_URL_API}/parameters"])]
223
+ get_dataset_parameters_url: Annotated[str, Field(examples=[f"{SAMPLE_BASE_URL_API}/datasets/{{dataset_name}}/parameters"])]
224
+ get_dataset_results_url: Annotated[str, Field(examples=[f"{SAMPLE_BASE_URL_API}/datasets/{{dataset_name}}"])]
225
+ get_dashboard_parameters_url: Annotated[str, Field(examples=[f"{SAMPLE_BASE_URL_API}/dashboards/{{dashboard_name}}/parameters"])]
226
+ get_dashboard_results_url: Annotated[str, Field(examples=[f"{SAMPLE_BASE_URL_API}/dashboards/{{dashboard_name}}"])]
227
+
228
+ # Data management routes
229
+ trigger_build_url: Annotated[str, Field(examples=[f"{SAMPLE_BASE_URL_API}/build"])]
230
+ get_query_result_url: Annotated[str, Field(examples=[f"{SAMPLE_BASE_URL_API}/query-result"])]
231
+ get_compiled_model_url: Annotated[str, Field(examples=[f"{SAMPLE_BASE_URL_API}/compiled-models/{{model_name}}"])]
232
+
233
+ # Authentication routes
234
+ get_user_session_url: Annotated[str | None, Field(default=None, examples=[f"{SAMPLE_BASE_URL_API}/auth/user-session"])]
235
+ list_providers_url: Annotated[str | None, Field(default=None, examples=[f"{SAMPLE_BASE_URL_API}/auth/providers"])]
236
+ login_url: Annotated[str | None, Field(default=None, examples=[f"{SAMPLE_BASE_URL_API}/auth/login"])]
237
+ logout_url: Annotated[str | None, Field(default=None, examples=[f"{SAMPLE_BASE_URL_API}/auth/logout"])]
238
+ change_password_url: Annotated[str | None, Field(default=None, examples=[f"{SAMPLE_BASE_URL_API}/auth/password"])]
239
+ list_api_keys_url: Annotated[str | None, Field(default=None, examples=[f"{SAMPLE_BASE_URL_API}/auth/api-keys"])]
240
+ create_api_key_url: Annotated[str | None, Field(default=None, examples=[f"{SAMPLE_BASE_URL_API}/auth/api-keys"])]
241
+ revoke_api_key_url: Annotated[str | None, Field(default=None, examples=[f"{SAMPLE_BASE_URL_API}/auth/api-keys/{{key_id}}"])]
242
+
243
+ # User management routes
244
+ list_user_fields_url: Annotated[str | None, Field(default=None, examples=[f"{SAMPLE_BASE_URL_API}/auth/user-management/user-fields"])]
245
+ list_users_url: Annotated[str | None, Field(default=None, examples=[f"{SAMPLE_BASE_URL_API}/auth/user-management/users"])]
246
+ add_user_url: Annotated[str | None, Field(default=None, examples=[f"{SAMPLE_BASE_URL_API}/auth/user-management/users"])]
247
+ update_user_url: Annotated[str | None, Field(default=None, examples=[f"{SAMPLE_BASE_URL_API}/auth/user-management/users/{{username}}"])]
248
+ delete_user_url: Annotated[str | None, Field(default=None, examples=[f"{SAMPLE_BASE_URL_API}/auth/user-management/users/{{username}}"])]
249
+
250
+ class ProjectMetadataModel(BaseModel):
251
+ name: Annotated[str, Field(examples=["my_project"], description="The name of the project (usually in snake case with underscores)")]
252
+ name_for_api: Annotated[str, Field(examples=["my-project"], description="The name of the project for the API (with dashes instead of underscores)")]
253
+ version: Annotated[str, Field(examples=["1"], description="The version of the project. Should be a stringified integer.")]
254
+ label: Annotated[str, Field(examples=["My Project"], description="The human-friendly display name for the project")]
255
+ description: Annotated[str, Field(examples=["My project description"], description="The description of the project")]
256
+ auth_strategy: Annotated[Literal["managed", "external"], Field(examples=["managed"], description="The authentication strategy for the project")]
257
+ auth_type: Annotated[Literal["optional", "required"], Field(examples=["optional"], description="The authentication type for the project")]
258
+ password_requirements: Annotated[PasswordRequirements | None, Field(description="The password requirements for the project")]
259
+ elevated_access_level: Annotated[Literal["admin", "member", "guest"], Field(
260
+ examples=["admin"], description="The access level required to access elevated features (such as configurables and data lineage)"
261
+ )]
262
+ api_routes: Annotated[ApiRoutesModel, Field(description="The API routes for the project")]
263
+
264
+
265
+ class DocumentationRoutesModel(BaseModel):
266
+ swagger_url: Annotated[str, Field(examples=[f"{SAMPLE_BASE_URL}/docs"], description="The URL to the Swagger UI")]
267
+ redoc_url: Annotated[str, Field(examples=[f"{SAMPLE_BASE_URL}/redoc"], description="The URL to the ReDoc UI")]
268
+ openapi_url: Annotated[str, Field(examples=[f"{SAMPLE_BASE_URL}/openapi.json"], description="The URL to the OpenAPI specification")]
269
+
270
+ class APIVersionMetadataModel(BaseModel):
271
+ project_metadata_url: Annotated[str, Field(examples=[SAMPLE_BASE_URL_API], description="The URL to the project metadata endpoint")]
272
+ documentation_routes: Annotated[DocumentationRoutesModel, Field(description="The API documentation for this version of the API contract")]
273
+
274
+ class ExploreEndpointsModel(BaseModel):
275
+ health_url: Annotated[str, Field(examples=[f"{SAMPLE_BASE_URL}/health"], description="The URL to the health check endpoint")]
276
+ api_versions: Annotated[dict[str, APIVersionMetadataModel], Field(description="The set of API versions and their metadata paths")]
277
+ documentation_routes: Annotated[DocumentationRoutesModel, Field(description="The API documentation for the base API application")]
278
+ mcp_server_url: Annotated[str, Field(examples=[f"{SAMPLE_BASE_URL}/mcp"], description="The URL to the MCP server")]
279
+ studio_url: Annotated[str, Field(examples=[f"{SAMPLE_BASE_URL}/studio"], description="The URL to the Squirrels Studio UI")]
280
+
281
+
282
+ class OAuthProtectedResourceMetadata(BaseModel):
283
+ """OAuth 2.1 Protected Resource Metadata (RFC 9728)"""
284
+ resource: str = Field(description="The resource identifier URL")
285
+ authorization_servers: list[str] = Field(description="List of authorization server issuer identifier URLs")
286
+ scopes_supported: list[str] = Field(description="List of OAuth 2.1 scope values supported")