squirrels 0.5.0b2__py3-none-any.whl → 0.5.0b4__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.
Potentially problematic release.
This version of squirrels might be problematic. Click here for more details.
- dateutils/__init__.py +6 -460
- dateutils/_enums.py +25 -0
- dateutils/_implementation.py +409 -0
- dateutils/types.py +6 -0
- squirrels/__init__.py +9 -13
- squirrels/_api_routes/__init__.py +5 -0
- squirrels/_api_routes/auth.py +262 -0
- squirrels/_api_routes/base.py +154 -0
- squirrels/_api_routes/dashboards.py +142 -0
- squirrels/_api_routes/data_management.py +103 -0
- squirrels/_api_routes/datasets.py +242 -0
- squirrels/_api_routes/oauth2.py +300 -0
- squirrels/_api_routes/project.py +214 -0
- squirrels/_api_server.py +145 -748
- squirrels/_arguments/__init__.py +0 -0
- squirrels/{arguments → _arguments}/init_time_args.py +7 -2
- squirrels/{arguments → _arguments}/run_time_args.py +4 -26
- squirrels/_auth.py +646 -93
- squirrels/_connection_set.py +5 -5
- squirrels/_constants.py +7 -1
- squirrels/{_dashboards_io.py → _dashboards.py} +87 -6
- squirrels/_data_sources.py +564 -0
- squirrels/_exceptions.py +9 -37
- squirrels/_initializer.py +31 -26
- squirrels/_manifest.py +5 -5
- squirrels/_model_builder.py +1 -1
- squirrels/_model_configs.py +2 -2
- squirrels/_model_queries.py +1 -1
- squirrels/_models.py +40 -27
- squirrels/{package_data → _package_data}/base_project/.env +1 -0
- squirrels/{package_data → _package_data}/base_project/.env.example +1 -0
- squirrels/{package_data → _package_data}/base_project/dashboards/dashboard_example.py +4 -4
- squirrels/{package_data → _package_data}/base_project/dashboards/dashboard_example.yml +2 -2
- squirrels/_package_data/base_project/macros/macros_example.sql +17 -0
- squirrels/{package_data → _package_data}/base_project/models/builds/build_example.py +2 -2
- squirrels/{package_data → _package_data}/base_project/models/builds/build_example.sql +1 -1
- squirrels/{package_data → _package_data}/base_project/models/dbviews/dbview_example.sql +1 -1
- squirrels/_package_data/base_project/models/federates/federate_example.py +41 -0
- squirrels/_package_data/base_project/models/federates/federate_example.sql +25 -0
- squirrels/{package_data → _package_data}/base_project/models/federates/federate_example.yml +6 -6
- squirrels/{package_data → _package_data}/base_project/parameters.yml +9 -8
- squirrels/_package_data/base_project/pyconfigs/connections.py +14 -0
- squirrels/{package_data → _package_data}/base_project/pyconfigs/context.py +14 -16
- squirrels/_package_data/base_project/pyconfigs/parameters.py +106 -0
- squirrels/_package_data/base_project/pyconfigs/user.py +51 -0
- squirrels/_package_data/templates/dataset_results.html +112 -0
- squirrels/_package_data/templates/oauth_login.html +271 -0
- squirrels/_parameter_configs.py +35 -35
- squirrels/_parameter_options.py +348 -0
- squirrels/_parameter_sets.py +47 -37
- squirrels/_parameters.py +1664 -0
- squirrels/_project.py +76 -32
- squirrels/_py_module.py +3 -2
- squirrels/_schemas/__init__.py +0 -0
- squirrels/_schemas/auth_models.py +144 -0
- squirrels/_schemas/query_param_models.py +67 -0
- squirrels/{_api_response_models.py → _schemas/response_models.py} +12 -8
- squirrels/_utils.py +38 -4
- squirrels/arguments.py +2 -0
- squirrels/auth.py +1 -0
- squirrels/connections.py +1 -0
- squirrels/dashboards.py +1 -82
- squirrels/data_sources.py +8 -563
- squirrels/parameter_options.py +8 -348
- squirrels/parameters.py +9 -1266
- squirrels/types.py +11 -0
- {squirrels-0.5.0b2.dist-info → squirrels-0.5.0b4.dist-info}/METADATA +4 -1
- squirrels-0.5.0b4.dist-info/RECORD +94 -0
- squirrels/package_data/base_project/macros/macros_example.sql +0 -15
- squirrels/package_data/base_project/models/federates/federate_example.py +0 -44
- squirrels/package_data/base_project/models/federates/federate_example.sql +0 -17
- squirrels/package_data/base_project/pyconfigs/connections.py +0 -14
- squirrels/package_data/base_project/pyconfigs/parameters.py +0 -93
- squirrels/package_data/base_project/pyconfigs/user.py +0 -23
- squirrels-0.5.0b2.dist-info/RECORD +0 -70
- /squirrels/{dataset_result.py → _dataset_types.py} +0 -0
- /squirrels/{package_data → _package_data}/base_project/assets/expenses.db +0 -0
- /squirrels/{package_data → _package_data}/base_project/assets/weather.db +0 -0
- /squirrels/{package_data → _package_data}/base_project/connections.yml +0 -0
- /squirrels/{package_data → _package_data}/base_project/docker/.dockerignore +0 -0
- /squirrels/{package_data → _package_data}/base_project/docker/Dockerfile +0 -0
- /squirrels/{package_data → _package_data}/base_project/docker/compose.yml +0 -0
- /squirrels/{package_data → _package_data}/base_project/duckdb_init.sql +0 -0
- /squirrels/{package_data/base_project/.gitignore → _package_data/base_project/gitignore} +0 -0
- /squirrels/{package_data → _package_data}/base_project/models/builds/build_example.yml +0 -0
- /squirrels/{package_data → _package_data}/base_project/models/dbviews/dbview_example.yml +0 -0
- /squirrels/{package_data → _package_data}/base_project/models/sources.yml +0 -0
- /squirrels/{package_data → _package_data}/base_project/seeds/seed_categories.csv +0 -0
- /squirrels/{package_data → _package_data}/base_project/seeds/seed_categories.yml +0 -0
- /squirrels/{package_data → _package_data}/base_project/seeds/seed_subcategories.csv +0 -0
- /squirrels/{package_data → _package_data}/base_project/seeds/seed_subcategories.yml +0 -0
- /squirrels/{package_data → _package_data}/base_project/squirrels.yml.j2 +0 -0
- /squirrels/{package_data → _package_data}/base_project/tmp/.gitignore +0 -0
- {squirrels-0.5.0b2.dist-info → squirrels-0.5.0b4.dist-info}/WHEEL +0 -0
- {squirrels-0.5.0b2.dist-info → squirrels-0.5.0b4.dist-info}/entry_points.txt +0 -0
- {squirrels-0.5.0b2.dist-info → squirrels-0.5.0b4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project metadata routes
|
|
3
|
+
"""
|
|
4
|
+
from typing import Any
|
|
5
|
+
from fastapi import FastAPI, Depends, Request
|
|
6
|
+
from fastapi.responses import JSONResponse
|
|
7
|
+
from fastapi.security import HTTPBearer
|
|
8
|
+
from mcp.server.fastmcp import FastMCP, Context
|
|
9
|
+
from dataclasses import asdict
|
|
10
|
+
from cachetools import TTLCache
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
from .. import _utils as u, _constants as c
|
|
14
|
+
from .._schemas import response_models as rm
|
|
15
|
+
from .._parameter_sets import ParameterSet
|
|
16
|
+
from .._exceptions import ConfigurationError, InvalidInputError
|
|
17
|
+
from .._manifest import PermissionScope
|
|
18
|
+
from .._version import __version__
|
|
19
|
+
from .._schemas.query_param_models import get_query_models_for_parameters
|
|
20
|
+
from .._auth import BaseUser
|
|
21
|
+
from .base import RouteBase
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ProjectRoutes(RouteBase):
|
|
25
|
+
"""Project metadata and data catalog routes"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, get_bearer_token: HTTPBearer, project, no_cache: bool = False):
|
|
28
|
+
super().__init__(get_bearer_token, project, no_cache)
|
|
29
|
+
|
|
30
|
+
# Setup caches
|
|
31
|
+
parameters_cache_size = int(self.env_vars.get(c.SQRL_PARAMETERS_CACHE_SIZE, 1024))
|
|
32
|
+
parameters_cache_ttl = int(self.env_vars.get(c.SQRL_PARAMETERS_CACHE_TTL_MINUTES, 60))
|
|
33
|
+
self.parameters_cache = TTLCache(maxsize=parameters_cache_size, ttl=parameters_cache_ttl*60)
|
|
34
|
+
|
|
35
|
+
async def _get_parameters_helper(
|
|
36
|
+
self, parameters_tuple: tuple[str, ...] | None, entity_type: str, entity_name: str, entity_scope: PermissionScope,
|
|
37
|
+
user: BaseUser | None, selections: tuple[tuple[str, Any], ...]
|
|
38
|
+
) -> ParameterSet:
|
|
39
|
+
"""Helper for getting parameters"""
|
|
40
|
+
selections_dict = dict(selections)
|
|
41
|
+
if "x_parent_param" not in selections_dict:
|
|
42
|
+
if len(selections_dict) > 1:
|
|
43
|
+
raise InvalidInputError(400, "Invalid input for cascading parameters", f"The parameters endpoint takes at most 1 widget parameter selection (unless x_parent_param is provided). Got {selections_dict}")
|
|
44
|
+
elif len(selections_dict) == 1:
|
|
45
|
+
parent_param = next(iter(selections_dict))
|
|
46
|
+
selections_dict["x_parent_param"] = parent_param
|
|
47
|
+
|
|
48
|
+
parent_param = selections_dict.get("x_parent_param")
|
|
49
|
+
if parent_param is not None and parent_param not in selections_dict:
|
|
50
|
+
# this condition is possible for multi-select parameters with empty selection
|
|
51
|
+
selections_dict[parent_param] = list()
|
|
52
|
+
|
|
53
|
+
if not self.authenticator.can_user_access_scope(user, entity_scope):
|
|
54
|
+
raise self.project._permission_error(user, entity_type, entity_name, entity_scope.name)
|
|
55
|
+
|
|
56
|
+
param_set = self.param_cfg_set.apply_selections(parameters_tuple, selections_dict, user, parent_param=parent_param)
|
|
57
|
+
return param_set
|
|
58
|
+
|
|
59
|
+
async def _get_parameters_cachable(
|
|
60
|
+
self, parameters_tuple: tuple[str, ...] | None, entity_type: str, entity_name: str, entity_scope: PermissionScope,
|
|
61
|
+
user: BaseUser | None, selections: tuple[tuple[str, Any], ...]
|
|
62
|
+
) -> ParameterSet:
|
|
63
|
+
"""Cachable version of parameters helper"""
|
|
64
|
+
return await self.do_cachable_action(
|
|
65
|
+
self.parameters_cache, self._get_parameters_helper, parameters_tuple, entity_type, entity_name, entity_scope, user, selections
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def setup_routes(
|
|
69
|
+
self, app: FastAPI, mcp: FastMCP, project_metadata_path: str, project_name: str, project_version: str, param_fields: dict
|
|
70
|
+
):
|
|
71
|
+
"""Setup project metadata routes"""
|
|
72
|
+
|
|
73
|
+
# Project metadata endpoint
|
|
74
|
+
@app.get(project_metadata_path, tags=["Project Metadata"], response_class=JSONResponse)
|
|
75
|
+
async def get_project_metadata(request: Request) -> rm.ProjectModel:
|
|
76
|
+
return rm.ProjectModel(
|
|
77
|
+
name=project_name,
|
|
78
|
+
version=project_version,
|
|
79
|
+
label=self.manifest_cfg.project_variables.label,
|
|
80
|
+
description=self.manifest_cfg.project_variables.description,
|
|
81
|
+
squirrels_version=__version__
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Data catalog endpoint
|
|
85
|
+
data_catalog_path = project_metadata_path + '/data-catalog'
|
|
86
|
+
|
|
87
|
+
async def get_data_catalog0(user: BaseUser | None) -> rm.CatalogModel:
|
|
88
|
+
parameters = self.param_cfg_set.apply_selections(None, {}, user)
|
|
89
|
+
parameters_model = parameters.to_api_response_model0()
|
|
90
|
+
full_parameters_list = [p.name for p in parameters_model.parameters]
|
|
91
|
+
|
|
92
|
+
dataset_items: list[rm.DatasetItemModel] = []
|
|
93
|
+
for name, config in self.manifest_cfg.datasets.items():
|
|
94
|
+
if self.authenticator.can_user_access_scope(user, config.scope):
|
|
95
|
+
name_normalized = u.normalize_name_for_api(name)
|
|
96
|
+
metadata = self.project.dataset_metadata(name).to_json()
|
|
97
|
+
parameters = config.parameters if config.parameters is not None else full_parameters_list
|
|
98
|
+
dataset_items.append(rm.DatasetItemModel(
|
|
99
|
+
name=name_normalized, label=config.label,
|
|
100
|
+
description=config.description,
|
|
101
|
+
schema=metadata["schema"], # type: ignore
|
|
102
|
+
parameters=parameters,
|
|
103
|
+
parameters_path=f"{project_metadata_path}/dataset/{name_normalized}/parameters",
|
|
104
|
+
result_path=f"{project_metadata_path}/dataset/{name_normalized}"
|
|
105
|
+
))
|
|
106
|
+
|
|
107
|
+
dashboard_items: list[rm.DashboardItemModel] = []
|
|
108
|
+
for name, dashboard in self.project._dashboards.items():
|
|
109
|
+
config = dashboard.config
|
|
110
|
+
if self.authenticator.can_user_access_scope(user, config.scope):
|
|
111
|
+
name_normalized = u.normalize_name_for_api(name)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
dashboard_format = self.project._dashboards[name].get_dashboard_format()
|
|
115
|
+
except KeyError:
|
|
116
|
+
raise ConfigurationError(f"No dashboard file found for: {name}")
|
|
117
|
+
|
|
118
|
+
parameters = config.parameters if config.parameters is not None else full_parameters_list
|
|
119
|
+
dashboard_items.append(rm.DashboardItemModel(
|
|
120
|
+
name=name, label=config.label,
|
|
121
|
+
description=config.description,
|
|
122
|
+
result_format=dashboard_format,
|
|
123
|
+
parameters=parameters,
|
|
124
|
+
parameters_path=f"{project_metadata_path}/dashboard/{name_normalized}/parameters",
|
|
125
|
+
result_path=f"{project_metadata_path}/dashboard/{name_normalized}"
|
|
126
|
+
))
|
|
127
|
+
|
|
128
|
+
if user and user.is_admin:
|
|
129
|
+
compiled_dag = await self.project._get_compiled_dag(user=user)
|
|
130
|
+
connections_items = self.project._get_all_connections()
|
|
131
|
+
data_models = self.project._get_all_data_models(compiled_dag)
|
|
132
|
+
lineage_items = self.project._get_all_data_lineage(compiled_dag)
|
|
133
|
+
else:
|
|
134
|
+
connections_items = []
|
|
135
|
+
data_models = []
|
|
136
|
+
lineage_items = []
|
|
137
|
+
|
|
138
|
+
return rm.CatalogModel(
|
|
139
|
+
parameters=parameters_model.parameters,
|
|
140
|
+
datasets=dataset_items,
|
|
141
|
+
dashboards=dashboard_items,
|
|
142
|
+
connections=connections_items,
|
|
143
|
+
models=data_models,
|
|
144
|
+
lineage=lineage_items,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
@app.get(data_catalog_path, tags=["Project Metadata"], summary="Get catalog of datasets and dashboards available for user")
|
|
148
|
+
async def get_data_catalog(request: Request, user: BaseUser | None = Depends(self.get_current_user)) -> rm.CatalogModel:
|
|
149
|
+
"""
|
|
150
|
+
Get catalog of datasets and dashboards available for the authenticated user.
|
|
151
|
+
|
|
152
|
+
For admin users, this endpoint will also return detailed information about all models and their lineage in the project.
|
|
153
|
+
"""
|
|
154
|
+
return await get_data_catalog0(user)
|
|
155
|
+
|
|
156
|
+
@mcp.tool(
|
|
157
|
+
name=f"get_data_catalog_for_{project_name}_{project_version}",
|
|
158
|
+
description=f"Use this tool to get the details of all datasets and parameters you can access in the Squirrels project '{project_name}'."
|
|
159
|
+
)
|
|
160
|
+
async def get_data_catalog_tool(ctx: Context):
|
|
161
|
+
user = self.get_user_from_tool_ctx(ctx)
|
|
162
|
+
data_catalog = await get_data_catalog0(user)
|
|
163
|
+
restricted_data_catalog = {
|
|
164
|
+
"parameters": data_catalog.parameters,
|
|
165
|
+
"datasets": data_catalog.datasets,
|
|
166
|
+
}
|
|
167
|
+
return restricted_data_catalog
|
|
168
|
+
|
|
169
|
+
# Project-level parameters endpoints
|
|
170
|
+
project_level_parameters_path = project_metadata_path + '/parameters'
|
|
171
|
+
parameters_description = "Selections of one parameter may cascade the available options in another parameter. " \
|
|
172
|
+
"For example, if the dataset has parameters for 'country' and 'city', available options for 'city' would " \
|
|
173
|
+
"depend on the selected option 'country'. If a parameter has 'trigger_refresh' as true, provide the parameter " \
|
|
174
|
+
"selection to this endpoint whenever it changes to refresh the parameter options of children parameters."
|
|
175
|
+
|
|
176
|
+
QueryModelForGetProjectParams, QueryModelForPostProjectParams = get_query_models_for_parameters(None, param_fields)
|
|
177
|
+
|
|
178
|
+
async def get_parameters_definition(
|
|
179
|
+
parameters_list: list[str] | None, entity_type: str, entity_name: str, entity_scope: PermissionScope,
|
|
180
|
+
user, all_request_params: dict, params: dict
|
|
181
|
+
) -> rm.ParametersModel:
|
|
182
|
+
self._validate_request_params(all_request_params, params)
|
|
183
|
+
|
|
184
|
+
get_parameters_function = self._get_parameters_helper if self.no_cache else self._get_parameters_cachable
|
|
185
|
+
selections = self.get_selections_as_immutable(params, uncached_keys={"x_verify_params"})
|
|
186
|
+
parameters_tuple = tuple(parameters_list) if parameters_list is not None else None
|
|
187
|
+
result = await get_parameters_function(parameters_tuple, entity_type, entity_name, entity_scope, user, selections)
|
|
188
|
+
return result.to_api_response_model0()
|
|
189
|
+
|
|
190
|
+
@app.get(project_level_parameters_path, tags=["Project Metadata"], description=parameters_description)
|
|
191
|
+
async def get_project_parameters(
|
|
192
|
+
request: Request, params: QueryModelForGetProjectParams, user=Depends(self.get_current_user) # type: ignore
|
|
193
|
+
) -> rm.ParametersModel:
|
|
194
|
+
start = time.time()
|
|
195
|
+
result = await get_parameters_definition(
|
|
196
|
+
None, "project", "", PermissionScope.PUBLIC, user, dict(request.query_params), asdict(params)
|
|
197
|
+
)
|
|
198
|
+
self.log_activity_time("GET REQUEST for PROJECT PARAMETERS", start, request)
|
|
199
|
+
return result
|
|
200
|
+
|
|
201
|
+
@app.post(project_level_parameters_path, tags=["Project Metadata"], description=parameters_description)
|
|
202
|
+
async def get_project_parameters_with_post(
|
|
203
|
+
request: Request, params: QueryModelForPostProjectParams, user=Depends(self.get_current_user) # type: ignore
|
|
204
|
+
) -> rm.ParametersModel:
|
|
205
|
+
start = time.time()
|
|
206
|
+
payload: dict = await request.json()
|
|
207
|
+
result = await get_parameters_definition(
|
|
208
|
+
None, "project", "", PermissionScope.PUBLIC, user, payload, params.model_dump()
|
|
209
|
+
)
|
|
210
|
+
self.log_activity_time("POST REQUEST for PROJECT PARAMETERS", start, request)
|
|
211
|
+
return result
|
|
212
|
+
|
|
213
|
+
return get_parameters_definition
|
|
214
|
+
|