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,289 @@
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 dataclasses import asdict
9
+ from cachetools import TTLCache
10
+ import time
11
+
12
+ from .. import _utils as u, _constants as c
13
+ from .._schemas import response_models as rm
14
+ from .._parameter_configs import APIParamFieldInfo
15
+ from .._parameter_sets import ParameterSet
16
+ from .._exceptions import ConfigurationError, InvalidInputError
17
+ from .._manifest import PermissionScope, AuthType, AuthStrategy
18
+ from .._schemas.query_param_models import get_query_models_for_parameters
19
+ from .._schemas.auth_models import AbstractUser
20
+ from .base import RouteBase
21
+
22
+
23
+ class ProjectRoutes(RouteBase):
24
+ """Project metadata and data catalog routes"""
25
+
26
+ def __init__(self, get_bearer_token: HTTPBearer, project, no_cache: bool = False):
27
+ super().__init__(get_bearer_token, project, no_cache)
28
+
29
+ # Setup caches
30
+ self.parameters_cache = TTLCache(
31
+ maxsize=self.env_vars.parameters_cache_size,
32
+ ttl=self.env_vars.parameters_cache_ttl_minutes*60
33
+ )
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: AbstractUser, 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: AbstractUser, 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, param_fields: dict[str, APIParamFieldInfo]
70
+ ):
71
+ """Setup project metadata routes"""
72
+ project_name = self.manifest_cfg.project_variables.name
73
+ project_version = str(self.manifest_cfg.project_variables.major_version)
74
+ label = self.manifest_cfg.project_variables.label
75
+ description = self.manifest_cfg.project_variables.description
76
+ auth_type = self.manifest_cfg.project_variables.auth_type.value
77
+
78
+ elevated_access_level = self.env_vars.elevated_access_level
79
+ if elevated_access_level != "admin":
80
+ self.logger.warning(f"{c.SQRL_PERMISSIONS_ELEVATED_ACCESS_LEVEL} has been set to a non-admin access level. For security reasons, DO NOT expose the APIs for this app publicly!")
81
+
82
+ # Project metadata endpoint
83
+ @app.get("/", summary="Get project metadata and API endpoint paths", tags=["Project Metadata"], response_class=JSONResponse)
84
+ async def get_project_metadata(request: Request) -> rm.ProjectMetadataModel:
85
+ project_name_for_api = u.normalize_name_for_api(project_name)
86
+ auth_strategy = self.manifest_cfg.project_variables.auth_strategy
87
+ is_external = (auth_strategy == AuthStrategy.EXTERNAL)
88
+
89
+ _, root_path = self._get_base_url_for_current_app(request)
90
+ api_routes = rm.ApiRoutesModel(
91
+ get_data_catalog_url = root_path + "/data-catalog",
92
+ get_parameters_url = root_path + "/parameters",
93
+ get_dataset_parameters_url = root_path + "/datasets/{dataset_name}/parameters",
94
+ get_dataset_results_url = root_path + "/datasets/{dataset_name}",
95
+ get_dashboard_parameters_url = root_path + "/dashboards/{dashboard_name}/parameters",
96
+ get_dashboard_results_url = root_path + "/dashboards/{dashboard_name}",
97
+ trigger_build_url = root_path + "/build",
98
+ get_query_result_url = root_path + "/query-result",
99
+ get_compiled_model_url = root_path + "/compiled-models/{model_name}",
100
+ get_user_session_url = root_path + "/auth/user-session",
101
+ list_providers_url = root_path + "/auth/providers",
102
+ login_url = None if is_external else root_path + "/auth/login",
103
+ logout_url = root_path + "/auth/logout",
104
+ change_password_url = None if is_external else root_path + "/auth/password",
105
+ list_api_keys_url = None if is_external else root_path + "/auth/api-keys",
106
+ create_api_key_url = None if is_external else root_path + "/auth/api-keys",
107
+ revoke_api_key_url = None if is_external else root_path + "/auth/api-keys/{key_id}",
108
+ list_user_fields_url = None if is_external else root_path + "/auth/user-management/user-fields",
109
+ list_users_url = None if is_external else root_path + "/auth/user-management/users",
110
+ add_user_url = None if is_external else root_path + "/auth/user-management/users",
111
+ update_user_url = None if is_external else root_path + "/auth/user-management/users/{username}",
112
+ delete_user_url = None if is_external else root_path + "/auth/user-management/users/{username}",
113
+ )
114
+
115
+ return rm.ProjectMetadataModel(
116
+ name = project_name,
117
+ name_for_api = project_name_for_api,
118
+ version = project_version,
119
+ label = label,
120
+ description = description,
121
+ auth_strategy = auth_strategy.value,
122
+ auth_type = auth_type,
123
+ password_requirements = None if is_external else self.authenticator.password_requirements,
124
+ elevated_access_level = elevated_access_level,
125
+ api_routes = api_routes,
126
+ )
127
+
128
+ # Data catalog endpoint
129
+ data_catalog_path = '/data-catalog'
130
+
131
+ async def get_data_catalog0(user: AbstractUser) -> rm.CatalogModel:
132
+ parameters = self.param_cfg_set.apply_selections(None, {}, user)
133
+ parameters_model = parameters.to_api_response_model0()
134
+ full_parameters_list = [p.name for p in parameters_model.parameters]
135
+ user_has_elevated_privileges = u.user_has_elevated_privileges(user.access_level, elevated_access_level)
136
+
137
+ dataset_items: list[rm.DatasetItemModel] = []
138
+ for name, config in self.manifest_cfg.datasets.items():
139
+ if self.authenticator.can_user_access_scope(user, config.scope):
140
+ name_for_api = u.normalize_name_for_api(name)
141
+ metadata = self.project.dataset_metadata(name).to_json()
142
+ parameters = config.parameters if config.parameters is not None else full_parameters_list
143
+
144
+ # Build dataset-specific configurables list
145
+ if user_has_elevated_privileges:
146
+ dataset_configurables_defaults = self.manifest_cfg.get_default_configurables(overrides=config.configurables)
147
+ dataset_configurables_list = [
148
+ rm.ConfigurableOverrideModel(name=name, default=default)
149
+ for name, default in dataset_configurables_defaults.items()
150
+ ]
151
+ else:
152
+ dataset_configurables_list = []
153
+
154
+ dataset_items.append(rm.DatasetItemModel(
155
+ name=name, name_for_api=name_for_api,
156
+ label=config.label,
157
+ description=config.description,
158
+ schema=metadata["schema"], # type: ignore
159
+ configurables=dataset_configurables_list,
160
+ parameters=parameters
161
+ ))
162
+
163
+ dashboard_items: list[rm.DashboardItemModel] = []
164
+ for name, dashboard in self.project._dashboards.items():
165
+ config = dashboard.config
166
+ if self.authenticator.can_user_access_scope(user, config.scope):
167
+ name_for_api = u.normalize_name_for_api(name)
168
+
169
+ try:
170
+ dashboard_format = self.project._dashboards[name].get_dashboard_format()
171
+ except KeyError:
172
+ raise ConfigurationError(f"No dashboard file found for: {name}")
173
+
174
+ if user_has_elevated_privileges:
175
+ dashboard_configurables_defaults = self.manifest_cfg.get_default_configurables(overrides=config.configurables)
176
+ dashboard_configurables_list = [
177
+ rm.ConfigurableOverrideModel(name=name, default=default)
178
+ for name, default in dashboard_configurables_defaults.items()
179
+ ]
180
+ else:
181
+ dashboard_configurables_list = []
182
+
183
+ parameters = config.parameters if config.parameters is not None else full_parameters_list
184
+ dashboard_items.append(rm.DashboardItemModel(
185
+ name=name, name_for_api=name_for_api,
186
+ label=config.label,
187
+ description=config.description,
188
+ result_format=dashboard_format,
189
+ configurables=dashboard_configurables_list,
190
+ parameters=parameters
191
+ ))
192
+
193
+ if user_has_elevated_privileges:
194
+ compiled_dag = await self.project._get_compiled_dag(user)
195
+ connections_items = self.project._get_all_connections()
196
+ data_models = self.project._get_all_data_models(compiled_dag)
197
+ lineage_items = self.project._get_all_data_lineage(compiled_dag)
198
+ configurables_list = [
199
+ rm.ConfigurableItemModel(name=name, label=cfg.label, default=cfg.default, description=cfg.description)
200
+ for name, cfg in self.manifest_cfg.configurables.items()
201
+ ]
202
+ else:
203
+ connections_items = []
204
+ data_models = []
205
+ lineage_items = []
206
+ configurables_list = []
207
+
208
+ return rm.CatalogModel(
209
+ parameters=parameters_model.parameters,
210
+ datasets=dataset_items,
211
+ dashboards=dashboard_items,
212
+ connections=connections_items,
213
+ models=data_models,
214
+ lineage=lineage_items,
215
+ configurables=configurables_list,
216
+ )
217
+
218
+ @app.get(data_catalog_path, tags=["Project Metadata"], summary="Get catalog of datasets and dashboards available for user")
219
+ async def get_data_catalog(
220
+ user: AbstractUser = Depends(self.get_current_user)
221
+ ) -> rm.CatalogModel:
222
+ """
223
+ Get catalog of datasets and dashboards available for the authenticated user.
224
+
225
+ For admin users, this endpoint will also return detailed information about all models and their lineage in the project.
226
+ """
227
+ start = time.time()
228
+
229
+ # If authentication is required, require user to be authenticated to access catalog
230
+ if self.manifest_cfg.project_variables.auth_type == AuthType.REQUIRED and user.access_level == "guest":
231
+ raise InvalidInputError(401, "user_required", "Authentication is required to access the data catalog")
232
+ data_catalog = await get_data_catalog0(user)
233
+
234
+ self.logger.log_activity_time("GET REQUEST for DATA CATALOG", start)
235
+ return data_catalog
236
+
237
+ async def get_data_catalog_for_mcp(user: AbstractUser) -> rm.CatalogModelForMcp:
238
+ """Get data catalog for MCP tools/resources. Takes user object."""
239
+ data_catalog = await get_data_catalog0(user)
240
+ return rm.CatalogModelForMcp(parameters=data_catalog.parameters, datasets=data_catalog.datasets)
241
+
242
+ # Store the MCP function as an instance attribute for access by McpServerBuilder
243
+ self._get_data_catalog_for_mcp = get_data_catalog_for_mcp
244
+
245
+ # Project-level parameters endpoints
246
+ project_level_parameters_path = '/parameters'
247
+ parameters_description = "Selections of one parameter may cascade the available options in another parameter. " \
248
+ "For example, if the dataset has parameters for 'country' and 'city', available options for 'city' would " \
249
+ "depend on the selected option 'country'. If a parameter has 'trigger_refresh' as true, provide the parameter " \
250
+ "selection to this endpoint whenever it changes to refresh the parameter options of children parameters."
251
+
252
+ QueryModelForGetProjectParams, QueryModelForPostProjectParams = get_query_models_for_parameters(param_fields)
253
+
254
+ async def get_parameters_definition(
255
+ parameters_list: list[str] | None, entity_type: str, entity_name: str, entity_scope: PermissionScope,
256
+ user: AbstractUser, params: dict
257
+ ) -> rm.ParametersModel:
258
+ # self._validate_request_params(all_request_params, params, headers)
259
+
260
+ get_parameters_function = self._get_parameters_helper if self.no_cache else self._get_parameters_cachable
261
+ selections = self.get_selections_as_immutable(params, uncached_keys=set())
262
+ parameters_tuple = tuple(parameters_list) if parameters_list is not None else None
263
+ result = await get_parameters_function(parameters_tuple, entity_type, entity_name, entity_scope, user, selections)
264
+ return result.to_api_response_model0()
265
+
266
+ @app.get(project_level_parameters_path, tags=["Project Metadata"], description=parameters_description)
267
+ async def get_project_parameters(
268
+ params: QueryModelForGetProjectParams, user=Depends(self.get_current_user)
269
+ ) -> rm.ParametersModel:
270
+ start = time.time()
271
+ result = await get_parameters_definition(
272
+ None, "project", "", PermissionScope.PUBLIC, user, asdict(params)
273
+ )
274
+ self.logger.log_activity_time("GET REQUEST for PROJECT PARAMETERS", start)
275
+ return result
276
+
277
+ @app.post(project_level_parameters_path, tags=["Project Metadata"], description=parameters_description)
278
+ async def get_project_parameters_with_post(
279
+ params: QueryModelForPostProjectParams, user=Depends(self.get_current_user)
280
+ ) -> rm.ParametersModel:
281
+ start = time.time()
282
+ result = await get_parameters_definition(
283
+ None, "project", "", PermissionScope.PUBLIC, user, params.model_dump()
284
+ )
285
+ self.logger.log_activity_time("POST REQUEST for PROJECT PARAMETERS", start)
286
+ return result
287
+
288
+ return get_parameters_definition
289
+