squirrels 0.5.0b3__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.

Files changed (47) hide show
  1. squirrels/__init__.py +2 -0
  2. squirrels/_api_routes/__init__.py +5 -0
  3. squirrels/_api_routes/auth.py +262 -0
  4. squirrels/_api_routes/base.py +154 -0
  5. squirrels/_api_routes/dashboards.py +142 -0
  6. squirrels/_api_routes/data_management.py +103 -0
  7. squirrels/_api_routes/datasets.py +242 -0
  8. squirrels/_api_routes/oauth2.py +300 -0
  9. squirrels/_api_routes/project.py +214 -0
  10. squirrels/_api_server.py +142 -745
  11. squirrels/_arguments/__init__.py +0 -0
  12. squirrels/_arguments/{_init_time_args.py → init_time_args.py} +5 -0
  13. squirrels/_arguments/{_run_time_args.py → run_time_args.py} +1 -1
  14. squirrels/_auth.py +645 -92
  15. squirrels/_connection_set.py +1 -1
  16. squirrels/_constants.py +6 -0
  17. squirrels/{_dashboards_io.py → _dashboards.py} +87 -6
  18. squirrels/_exceptions.py +9 -37
  19. squirrels/_model_builder.py +1 -1
  20. squirrels/_model_queries.py +1 -1
  21. squirrels/_models.py +13 -12
  22. squirrels/_package_data/base_project/.env +1 -0
  23. squirrels/_package_data/base_project/.env.example +1 -0
  24. squirrels/_package_data/base_project/pyconfigs/parameters.py +84 -76
  25. squirrels/_package_data/base_project/pyconfigs/user.py +30 -2
  26. squirrels/_package_data/templates/dataset_results.html +112 -0
  27. squirrels/_package_data/templates/oauth_login.html +271 -0
  28. squirrels/_parameter_configs.py +1 -1
  29. squirrels/_parameter_sets.py +31 -21
  30. squirrels/_parameters.py +521 -123
  31. squirrels/_project.py +43 -24
  32. squirrels/_py_module.py +3 -2
  33. squirrels/_schemas/__init__.py +0 -0
  34. squirrels/_schemas/auth_models.py +144 -0
  35. squirrels/_schemas/query_param_models.py +67 -0
  36. squirrels/{_api_response_models.py → _schemas/response_models.py} +12 -8
  37. squirrels/_utils.py +34 -2
  38. squirrels/arguments.py +2 -2
  39. squirrels/auth.py +1 -0
  40. squirrels/dashboards.py +1 -1
  41. squirrels/types.py +3 -3
  42. {squirrels-0.5.0b3.dist-info → squirrels-0.5.0b4.dist-info}/METADATA +4 -1
  43. {squirrels-0.5.0b3.dist-info → squirrels-0.5.0b4.dist-info}/RECORD +46 -32
  44. squirrels/_dashboard_types.py +0 -82
  45. {squirrels-0.5.0b3.dist-info → squirrels-0.5.0b4.dist-info}/WHEEL +0 -0
  46. {squirrels-0.5.0b3.dist-info → squirrels-0.5.0b4.dist-info}/entry_points.txt +0 -0
  47. {squirrels-0.5.0b3.dist-info → squirrels-0.5.0b4.dist-info}/licenses/LICENSE +0 -0
squirrels/_project.py CHANGED
@@ -5,14 +5,16 @@ import asyncio, typing as t, functools as ft, shutil, json, os
5
5
  import logging as l, matplotlib.pyplot as plt, networkx as nx, polars as pl
6
6
  import sqlglot, sqlglot.expressions
7
7
 
8
- from ._auth import Authenticator, BaseUser
8
+ from ._auth import Authenticator, BaseUser, AuthProviderArgs, ProviderFunctionType
9
+ from ._schemas import response_models as rm
9
10
  from ._model_builder import ModelBuilder
10
11
  from ._exceptions import InvalidInputError, ConfigurationError
11
- from . import _utils as u, _constants as c, _manifest as mf, _connection_set as cs, _api_response_models as arm
12
+ from ._py_module import PyModule
13
+ from . import _dashboards as d, _utils as u, _constants as c, _manifest as mf, _connection_set as cs
12
14
  from . import _seeds as s, _models as m, _model_configs as mc, _model_queries as mq, _sources as so
13
- from . import _parameter_sets as ps, _dashboards_io as d, _dashboard_types as dash, _dataset_types as dr
15
+ from . import _parameter_sets as ps, _dataset_types as dr
14
16
 
15
- T = t.TypeVar("T", bound=dash.Dashboard)
17
+ T = t.TypeVar("T", bound=d.Dashboard)
16
18
  M = t.TypeVar("M", bound=m.DataModel)
17
19
 
18
20
 
@@ -129,16 +131,33 @@ class SquirrelsProject:
129
131
  return cs.ConnectionSetIO.load_from_file(self._logger, self._filepath, self._manifest_cfg, self._conn_args)
130
132
 
131
133
  @ft.cached_property
132
- def _auth(self) -> Authenticator:
133
- return Authenticator(self._logger, self._filepath, self._env_vars)
134
+ def _user_cls_and_provider_functions(self) -> tuple[type[BaseUser], list[ProviderFunctionType]]:
135
+ user_module_path = u.Path(self._filepath, c.PYCONFIGS_FOLDER, c.USER_FILE)
136
+ user_module = PyModule(user_module_path)
137
+
138
+ User = user_module.get_func_or_class("User", default_attr=BaseUser) # adds to Authenticator.providers as side effect
139
+ provider_functions = Authenticator.providers
140
+ Authenticator.providers = []
141
+
142
+ if not issubclass(User, BaseUser):
143
+ raise ConfigurationError(f"User class in '{c.USER_FILE}' must inherit from BaseUser")
144
+
145
+ return User, provider_functions
146
+
147
+ @ft.cached_property
148
+ def _auth_args(self) -> AuthProviderArgs:
149
+ conn_args = self._conn_args
150
+ return AuthProviderArgs(conn_args.project_path, conn_args.proj_vars, conn_args.env_vars)
134
151
 
135
152
  @ft.cached_property
136
- def User(self) -> t.Type[BaseUser]:
137
- return self._auth.User
153
+ def _auth(self) -> Authenticator[BaseUser]:
154
+ User, provider_functions = self._user_cls_and_provider_functions
155
+ return Authenticator(self._logger, self._filepath, self._auth_args, provider_functions, user_cls=User)
138
156
 
139
157
  @ft.cached_property
140
158
  def _param_args(self) -> ps.ParametersArgs:
141
- return ps.ParameterConfigsSetIO.get_param_args(self._conn_args)
159
+ conn_args = self._conn_args
160
+ return ps.ParametersArgs(conn_args.project_path, conn_args.proj_vars, conn_args.env_vars)
142
161
 
143
162
  @ft.cached_property
144
163
  def _param_cfg_set(self) -> ps.ParameterConfigsSet:
@@ -261,7 +280,7 @@ class SquirrelsProject:
261
280
  for model_name in dependencies:
262
281
  model = models_dict[model_name]
263
282
  if isinstance(model, m.SourceModel) and not model.model_config.load_to_duckdb:
264
- raise InvalidInputError(203, f"Source model '{model_name}' cannot be queried with DuckDB")
283
+ raise InvalidInputError(400, "Unqueryable source model", f"Source model '{model_name}' cannot be queried with DuckDB")
265
284
  if isinstance(model, (m.SourceModel, m.BuildModel)):
266
285
  substitutions[model_name] = f"venv.{model_name}"
267
286
 
@@ -310,18 +329,18 @@ class SquirrelsProject:
310
329
  await dag.execute(self._param_args, self._param_cfg_set, self._context_func, user, selections, runquery=False, default_traits=default_traits)
311
330
  return dag
312
331
 
313
- def _get_all_connections(self) -> list[arm.ConnectionItemModel]:
332
+ def _get_all_connections(self) -> list[rm.ConnectionItemModel]:
314
333
  connections = []
315
334
  for conn_name, conn_props in self._conn_set.get_connections_as_dict().items():
316
335
  if isinstance(conn_props, mf.ConnectionProperties):
317
336
  label = conn_props.label if conn_props.label is not None else conn_name
318
- connections.append(arm.ConnectionItemModel(name=conn_name, label=label))
337
+ connections.append(rm.ConnectionItemModel(name=conn_name, label=label))
319
338
  return connections
320
339
 
321
- def _get_all_data_models(self, compiled_dag: m.DAG) -> list[arm.DataModelItem]:
340
+ def _get_all_data_models(self, compiled_dag: m.DAG) -> list[rm.DataModelItem]:
322
341
  return compiled_dag.get_all_data_models()
323
342
 
324
- async def get_all_data_models(self) -> list[arm.DataModelItem]:
343
+ async def get_all_data_models(self) -> list[rm.DataModelItem]:
325
344
  """
326
345
  Get all data models in the project
327
346
 
@@ -331,26 +350,26 @@ class SquirrelsProject:
331
350
  compiled_dag = await self._get_compiled_dag()
332
351
  return self._get_all_data_models(compiled_dag)
333
352
 
334
- def _get_all_data_lineage(self, compiled_dag: m.DAG) -> list[arm.LineageRelation]:
353
+ def _get_all_data_lineage(self, compiled_dag: m.DAG) -> list[rm.LineageRelation]:
335
354
  all_lineage = compiled_dag.get_all_model_lineage()
336
355
 
337
356
  # Add dataset nodes to the lineage
338
357
  for dataset in self._manifest_cfg.datasets.values():
339
- target_dataset = arm.LineageNode(name=dataset.name, type="dataset")
340
- source_model = arm.LineageNode(name=dataset.model, type="model")
341
- all_lineage.append(arm.LineageRelation(type="runtime", source=source_model, target=target_dataset))
358
+ target_dataset = rm.LineageNode(name=dataset.name, type="dataset")
359
+ source_model = rm.LineageNode(name=dataset.model, type="model")
360
+ all_lineage.append(rm.LineageRelation(type="runtime", source=source_model, target=target_dataset))
342
361
 
343
362
  # Add dashboard nodes to the lineage
344
363
  for dashboard in self._dashboards.values():
345
- target_dashboard = arm.LineageNode(name=dashboard.dashboard_name, type="dashboard")
364
+ target_dashboard = rm.LineageNode(name=dashboard.dashboard_name, type="dashboard")
346
365
  datasets = set(x.dataset for x in dashboard.config.depends_on)
347
366
  for dataset in datasets:
348
- source_dataset = arm.LineageNode(name=dataset, type="dataset")
349
- all_lineage.append(arm.LineageRelation(type="runtime", source=source_dataset, target=target_dashboard))
367
+ source_dataset = rm.LineageNode(name=dataset, type="dataset")
368
+ all_lineage.append(rm.LineageRelation(type="runtime", source=source_dataset, target=target_dashboard))
350
369
 
351
370
  return all_lineage
352
371
 
353
- async def get_all_data_lineage(self) -> list[arm.LineageRelation]:
372
+ async def get_all_data_lineage(self) -> list[rm.LineageRelation]:
354
373
  """
355
374
  Get all data lineage in the project
356
375
 
@@ -480,7 +499,7 @@ class SquirrelsProject:
480
499
 
481
500
  def _permission_error(self, user: BaseUser | None, data_type: str, data_name: str, scope: str) -> InvalidInputError:
482
501
  username = "" if user is None else f" '{user.username}'"
483
- return InvalidInputError(25, f"User{username} does not have permission to access {scope} {data_type}: {data_name}")
502
+ return InvalidInputError(403, f"Unauthorized access to {data_type}", f"User{username} does not have permission to access {scope} {data_type}: {data_name}")
484
503
 
485
504
  def seed(self, name: str) -> pl.LazyFrame:
486
505
  """
@@ -545,7 +564,7 @@ class SquirrelsProject:
545
564
  )
546
565
 
547
566
  async def dashboard(
548
- self, name: str, *, selections: dict[str, t.Any] = {}, user: BaseUser | None = None, dashboard_type: t.Type[T] = dash.PngDashboard
567
+ self, name: str, *, selections: dict[str, t.Any] = {}, user: BaseUser | None = None, dashboard_type: t.Type[T] = d.PngDashboard
549
568
  ) -> T:
550
569
  """
551
570
  Async method to retrieve a dashboard given parameter selections.
squirrels/_py_module.py CHANGED
@@ -43,11 +43,12 @@ class PyModule:
43
43
  return func_or_class
44
44
 
45
45
 
46
- def run_pyconfig_main(base_path: str, filename: str, kwargs: dict[str, Any] = {}) -> None:
46
+ def run_pyconfig_main(base_path: str, filename: str, kwargs: dict[str, Any] = {}) -> Any | None:
47
47
  """
48
48
  Given a python file in the 'pyconfigs' folder, run its main function
49
49
 
50
50
  Arguments:
51
+ base_path: The base path of the project
51
52
  filename: The name of the file to run main function
52
53
  kwargs: Dictionary of the main function arguments
53
54
  """
@@ -56,6 +57,6 @@ def run_pyconfig_main(base_path: str, filename: str, kwargs: dict[str, Any] = {}
56
57
  main_function = module.get_func_or_class(c.MAIN_FUNC, is_required=False)
57
58
  if main_function:
58
59
  try:
59
- main_function(**kwargs)
60
+ return main_function(**kwargs)
60
61
  except Exception as e:
61
62
  raise FileExecutionError(f'Failed to run python file "{filepath}"', e) from e
File without changes
@@ -0,0 +1,144 @@
1
+ from typing import Callable, Any
2
+ from datetime import datetime
3
+ from pydantic import BaseModel, ConfigDict, Field, field_serializer
4
+
5
+
6
+ class BaseUser(BaseModel):
7
+ model_config = ConfigDict(from_attributes=True)
8
+ username: str
9
+ is_admin: bool = False
10
+
11
+ @classmethod
12
+ def dropped_columns(cls):
13
+ return []
14
+
15
+ def __hash__(self):
16
+ return hash(self.username)
17
+
18
+
19
+ class ApiKey(BaseModel):
20
+ model_config = ConfigDict(from_attributes=True)
21
+ id: str
22
+ title: str
23
+ username: str
24
+ created_at: datetime
25
+ expires_at: datetime
26
+
27
+ @field_serializer('created_at', 'expires_at')
28
+ def serialize_datetime(self, dt: datetime) -> str:
29
+ return dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
30
+
31
+
32
+ class UserField(BaseModel):
33
+ name: str
34
+ type: str
35
+ nullable: bool
36
+ enum: list[str] | None
37
+ default: Any | None
38
+
39
+
40
+ class ProviderConfigs(BaseModel):
41
+ client_id: str
42
+ client_secret: str
43
+ server_metadata_url: str
44
+ client_kwargs: dict = Field(default_factory=dict)
45
+ get_user: Callable[[dict], BaseUser]
46
+
47
+
48
+ class AuthProvider(BaseModel):
49
+ name: str
50
+ label: str
51
+ icon: str
52
+ provider_configs: ProviderConfigs
53
+
54
+
55
+ # OAuth 2.1 Models
56
+
57
+ class OAuthClientModel(BaseModel):
58
+ """OAuth client details"""
59
+ model_config = ConfigDict(from_attributes=True)
60
+ client_id: str
61
+ client_name: str
62
+ redirect_uris: list[str]
63
+ scope: str
64
+ grant_types: list[str]
65
+ response_types: list[str]
66
+ created_at: datetime
67
+ is_active: bool
68
+
69
+ @field_serializer('created_at')
70
+ def serialize_datetime(self, dt: datetime) -> str:
71
+ return dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
72
+
73
+
74
+ class ClientRegistrationRequest(BaseModel):
75
+ """Request model for OAuth client registration"""
76
+ client_name: str = Field(description="Human-readable name for the OAuth client")
77
+ redirect_uris: list[str] = Field(description="List of allowed redirect URIs for the client")
78
+ scope: str = Field(default="read", description="Default scope for the client")
79
+ grant_types: list[str] = Field(default=["authorization_code", "refresh_token"], description="Allowed grant types")
80
+ response_types: list[str] = Field(default=["code"], description="Allowed response types")
81
+
82
+
83
+ class ClientUpdateRequest(BaseModel):
84
+ """Request model for OAuth client update"""
85
+ client_name: str | None = Field(default=None, description="Human-readable name for the OAuth client")
86
+ redirect_uris: list[str] | None = Field(default=None, description="List of allowed redirect URIs for the client")
87
+ scope: str | None = Field(default=None, description="Default scope for the client")
88
+ grant_types: list[str] | None = Field(default=None, description="Allowed grant types")
89
+ response_types: list[str] | None = Field(default=None, description="Allowed response types")
90
+ is_active: bool | None = Field(default=None, description="Whether the client is active")
91
+
92
+
93
+ class ClientDetailsResponse(BaseModel):
94
+ """Response model for OAuth client details (without client_secret)"""
95
+ client_id: str = Field(description="Client ID")
96
+ client_name: str = Field(description="Client name")
97
+ redirect_uris: list[str] = Field(description="Registered redirect URIs")
98
+ scope: str = Field(description="Default scope")
99
+ grant_types: list[str] = Field(description="Allowed grant types")
100
+ response_types: list[str] = Field(description="Allowed response types")
101
+ created_at: datetime = Field(description="Registration timestamp")
102
+ is_active: bool = Field(description="Whether the client is active")
103
+
104
+ @field_serializer('created_at')
105
+ def serialize_datetime(self, dt: datetime) -> str:
106
+ return dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
107
+
108
+
109
+ class ClientUpdateResponse(ClientDetailsResponse):
110
+ """Response model for OAuth client update"""
111
+ registration_access_token: str | None = Field(default=None, description="Token for managing this client registration (store securely)")
112
+
113
+
114
+ class ClientRegistrationResponse(ClientUpdateResponse):
115
+ """Response model for OAuth client registration"""
116
+ client_secret: str = Field(description="Generated client secret (store securely)")
117
+ registration_client_uri: str | None = Field(default=None, description="URI for managing this client registration")
118
+
119
+
120
+ class TokenResponse(BaseModel):
121
+ access_token: str
122
+ token_type: str = "bearer"
123
+ expires_in: int
124
+ refresh_token: str | None = None
125
+
126
+
127
+ class OAuthServerMetadata(BaseModel):
128
+ """OAuth 2.1 Authorization Server Metadata (RFC 8414)"""
129
+ issuer: str = Field(description="Authorization server's issuer identifier URL")
130
+ authorization_endpoint: str = Field(description="URL of the authorization endpoint")
131
+ token_endpoint: str = Field(description="URL of the token endpoint")
132
+ revocation_endpoint: str = Field(description="URL of the token revocation endpoint")
133
+ registration_endpoint: str = Field(description="URL of the client registration endpoint")
134
+ scopes_supported: list[str] = Field(description="List of OAuth 2.1 scope values supported")
135
+ response_types_supported: list[str] = Field(description="List of OAuth 2.1 response_type values supported")
136
+ grant_types_supported: list[str] = Field(description="List of OAuth 2.1 grant type values supported")
137
+ token_endpoint_auth_methods_supported: list[str] = Field(
138
+ default=["client_secret_basic", "client_secret_post"],
139
+ description="List of client authentication methods supported by the token endpoint"
140
+ )
141
+ code_challenge_methods_supported: list[str] = Field(
142
+ default=["S256"],
143
+ description="List of PKCE code challenge methods supported"
144
+ )
@@ -0,0 +1,67 @@
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(widget_parameters: list[str] | None, predefined_params: list[APIParamFieldInfo], param_fields: dict):
13
+ """Helper function to generate query models"""
14
+ if widget_parameters is None:
15
+ widget_parameters = list(param_fields.keys())
16
+
17
+ QueryModelForGetRaw = make_dataclass("QueryParams", [
18
+ param_fields[param].as_query_info() for param in widget_parameters
19
+ ] + [param.as_query_info() for param in predefined_params])
20
+ QueryModelForGet = Annotated[QueryModelForGetRaw, Depends()]
21
+
22
+ field_definitions = {param: param_fields[param].as_body_info() for param in widget_parameters}
23
+ for param in predefined_params:
24
+ field_definitions[param.name] = param.as_body_info()
25
+ QueryModelForPost = create_model("RequestBodyParams", **field_definitions) # type: ignore
26
+ return QueryModelForGet, QueryModelForPost
27
+
28
+
29
+ def get_query_models_for_parameters(widget_parameters: list[str] | None, param_fields: dict):
30
+ """Generate query models for parameter endpoints"""
31
+ predefined_params = [
32
+ APIParamFieldInfo("x_verify_params", bool, default=False, description="If true, the query parameters are verified to be valid for the dataset"),
33
+ APIParamFieldInfo("x_parent_param", str, description="The parameter name used for parameter updates. If not provided, then all parameters are retrieved"),
34
+ ]
35
+ return _get_query_models_helper(widget_parameters, predefined_params, param_fields)
36
+
37
+
38
+ def get_query_models_for_dataset(widget_parameters: list[str] | None, param_fields: dict):
39
+ """Generate query models for dataset endpoints"""
40
+ predefined_params = [
41
+ APIParamFieldInfo("x_verify_params", bool, default=False, description="If true, the query parameters are verified to be valid for the dataset"),
42
+ APIParamFieldInfo("x_orientation", str, default="records", description="The orientation of the data to return, one of: 'records', 'rows', or 'columns'"),
43
+ APIParamFieldInfo("x_select", list[str], examples=[[]], description="The columns to select from the dataset. All are returned if not specified"),
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(widget_parameters, predefined_params, param_fields)
48
+
49
+
50
+ def get_query_models_for_dashboard(widget_parameters: list[str] | None, param_fields: dict):
51
+ """Generate query models for dashboard endpoints"""
52
+ predefined_params = [
53
+ APIParamFieldInfo("x_verify_params", bool, default=False, description="If true, the query parameters are verified to be valid for the dashboard"),
54
+ ]
55
+ return _get_query_models_helper(widget_parameters, predefined_params, param_fields)
56
+
57
+
58
+ def get_query_models_for_querying_models(param_fields: dict):
59
+ """Generate query models for querying data models"""
60
+ predefined_params = [
61
+ APIParamFieldInfo("x_verify_params", bool, default=False, description="If true, the query parameters are verified to be valid"),
62
+ APIParamFieldInfo("x_orientation", str, default="records", description="The orientation of the data to return, one of: 'records', 'rows', or 'columns'"),
63
+ APIParamFieldInfo("x_offset", int, default=0, description="The number of rows to skip before returning data (applied after data caching)"),
64
+ APIParamFieldInfo("x_limit", int, default=1000, description="The maximum number of rows to return (applied after data caching and offset)"),
65
+ APIParamFieldInfo("x_sql_query", str, description="The SQL query to execute on the data models"),
66
+ ]
67
+ return _get_query_models_helper(None, predefined_params, param_fields)
@@ -1,16 +1,20 @@
1
1
  from typing import Annotated, Literal
2
2
  from pydantic import BaseModel, Field
3
- from datetime import datetime, date
3
+ from datetime import date
4
4
 
5
- from . import _model_configs as mc, _sources as s
5
+ from .. import _model_configs as mc, _sources as s
6
6
 
7
7
 
8
- class LoginReponse(BaseModel):
9
- access_token: Annotated[str, Field(examples=["encoded_jwt_token"], description="An encoded JSON web token to use subsequent API requests")]
10
- token_type: Annotated[str, Field(examples=["bearer"], description='Always "bearer" for Bearer token')]
11
- username: Annotated[str, Field(examples=["johndoe"], description='The username authenticated with from the form data')]
12
- is_admin: Annotated[bool, Field(examples=[False], description="A boolean for whether the user is an admin")]
13
- expiry_time: Annotated[datetime, Field(examples=["2023-08-01T12:00:00.000000Z"], description="The expiry time of the access token in yyyy-MM-dd'T'hh:mm:ss.SSSSSS'Z' format")]
8
+ ## Simple Auth Response Models
9
+
10
+ class ApiKeyResponse(BaseModel):
11
+ api_key: Annotated[str, Field(examples=["sqrl-12345678"], description="The API key to use subsequent API requests")]
12
+
13
+ class ProviderResponse(BaseModel):
14
+ name: Annotated[str, Field(examples=["my_provider"], description="The name of the provider")]
15
+ label: Annotated[str, Field(examples=["My Provider"], description="The human-friendly display name for the provider")]
16
+ icon: Annotated[str, Field(examples=["https://example.com/my_provider_icon.png"], description="The URL of the provider's icon")]
17
+ login_url: Annotated[str, Field(examples=["https://example.com/my_provider_login"], description="The URL to redirect to for provider login")]
14
18
 
15
19
 
16
20
  ## Parameters Response Models
squirrels/_utils.py CHANGED
@@ -2,10 +2,9 @@ from typing import Sequence, Optional, Union, TypeVar, Callable, Any, Iterable
2
2
  from datetime import datetime
3
3
  from pathlib import Path
4
4
  from functools import lru_cache
5
- from pydantic import BaseModel
6
5
  import os, time, logging, json, duckdb, polars as pl, yaml
7
6
  import jinja2 as j2, jinja2.nodes as j2_nodes
8
- import sqlglot, sqlglot.expressions, asyncio
7
+ import sqlglot, sqlglot.expressions, asyncio, hashlib, inspect, base64
9
8
 
10
9
  from . import _constants as c
11
10
  from ._exceptions import ConfigurationError
@@ -359,3 +358,36 @@ async def asyncio_gather(coroutines: list):
359
358
  # Wait for tasks to be cancelled
360
359
  await asyncio.gather(*tasks, return_exceptions=True)
361
360
  raise
361
+
362
+
363
+ def hash_string(input_str: str, salt: str) -> str:
364
+ """
365
+ Hashes a string using SHA-256
366
+ """
367
+ return hashlib.sha256((input_str + salt).encode()).hexdigest()
368
+
369
+
370
+ T = TypeVar('T')
371
+ def call_func(func: Callable[..., T], **kwargs) -> T:
372
+ """
373
+ Calls a function with the given arguments if func expects arguments, otherwise calls func without arguments
374
+ """
375
+ sig = inspect.signature(func)
376
+ # Filter kwargs to only include parameters that the function accepts
377
+ filtered_kwargs = {k: v for k, v in kwargs.items() if k in sig.parameters}
378
+ return func(**filtered_kwargs)
379
+
380
+
381
+ def generate_pkce_challenge(code_verifier: str) -> str:
382
+ """Generate PKCE code challenge from code verifier"""
383
+ # Generate SHA256 hash of code_verifier
384
+ verifier_hash = hashlib.sha256(code_verifier.encode('utf-8')).digest()
385
+ # Base64 URL encode (without padding)
386
+ expected_challenge = base64.urlsafe_b64encode(verifier_hash).decode('utf-8').rstrip('=')
387
+ return expected_challenge
388
+
389
+ def validate_pkce_challenge(code_verifier: str, code_challenge: str) -> bool:
390
+ """Validate PKCE code verifier against code challenge"""
391
+ # Generate expected challenge
392
+ expected_challenge = generate_pkce_challenge(code_verifier)
393
+ return expected_challenge == code_challenge
squirrels/arguments.py CHANGED
@@ -1,2 +1,2 @@
1
- from ._arguments._init_time_args import ConnectionsArgs, ParametersArgs, BuildModelArgs
2
- from ._arguments._run_time_args import ContextArgs, ModelArgs, DashboardArgs
1
+ from ._arguments.init_time_args import ConnectionsArgs, AuthProviderArgs, ParametersArgs, BuildModelArgs
2
+ from ._arguments.run_time_args import ContextArgs, ModelArgs, DashboardArgs
squirrels/auth.py ADDED
@@ -0,0 +1 @@
1
+ from ._auth import BaseUser, ProviderConfigs, provider
squirrels/dashboards.py CHANGED
@@ -1 +1 @@
1
- from ._dashboard_types import PngDashboard, HtmlDashboard
1
+ from ._dashboards import PngDashboard, HtmlDashboard
squirrels/types.py CHANGED
@@ -4,8 +4,8 @@ from ._parameter_options import ParameterOption
4
4
 
5
5
  from ._parameters import Parameter, TextValue
6
6
 
7
- from ._auth import BaseUser
8
-
9
7
  from ._dataset_types import DatasetMetadata, DatasetResult
10
8
 
11
- from ._dashboard_types import Dashboard
9
+ from ._dashboards import Dashboard
10
+
11
+ from ._parameter_configs import ParameterConfigBase
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: squirrels
3
- Version: 0.5.0b3
3
+ Version: 0.5.0b4
4
4
  Summary: Squirrels - API Framework for Data Analytics
5
5
  Project-URL: Homepage, https://squirrels-analytics.github.io
6
6
  Project-URL: Repository, https://github.com/squirrels-analytics/squirrels
@@ -13,15 +13,18 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
13
13
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
14
  Classifier: Typing :: Typed
15
15
  Requires-Python: ~=3.10
16
+ Requires-Dist: authlib<2,>=1.5.2
16
17
  Requires-Dist: bcrypt<5,>=4.0.1
17
18
  Requires-Dist: cachetools<6,>=5.3.2
18
19
  Requires-Dist: duckdb<2,>=1.1.3
19
20
  Requires-Dist: fastapi<1,>=0.112.1
20
21
  Requires-Dist: gitpython<4,>=3.1.41
21
22
  Requires-Dist: inquirer<4,>=3.2.1
23
+ Requires-Dist: itsdangerous<3,>=2.2.0
22
24
  Requires-Dist: jinja2<4,>=3.1.3
23
25
  Requires-Dist: libpass<2,>=1.9.0
24
26
  Requires-Dist: matplotlib<4,>=3.8.3
27
+ Requires-Dist: mcp>=1.9.2
25
28
  Requires-Dist: networkx<4,>=3.2.1
26
29
  Requires-Dist: pandas<3,>=2.1.4
27
30
  Requires-Dist: polars<2,>=1.14.0
@@ -2,46 +2,54 @@ dateutils/__init__.py,sha256=dq4VSlJ5ztaDPdvYBRAvXSyanT_CZif3I4O0YVmWfa8,277
2
2
  dateutils/_enums.py,sha256=WBrnLqta_iMMhGMEn24cCO1Vlr7bST0E8oEfAL3z0P8,373
3
3
  dateutils/_implementation.py,sha256=PVJAdNolDdTpCZXwvokKoMIZzTSNpCUvZlVLWpMeSho,15173
4
4
  dateutils/types.py,sha256=xcRwBoftQi-uHM1tlVTW5xgN84qdrqCo5tQT7A34S8Y,124
5
- squirrels/__init__.py,sha256=7n-Wci_BSeE7SEY703WYqsBhJ2Ix49AKIcRq3rXb4QI,267
6
- squirrels/_api_response_models.py,sha256=fQWjEBGAyy8KbkaY4jjOKvxhEcvQPU1bF2dJRTVTRc4,13601
7
- squirrels/_api_server.py,sha256=iODEImWZo7NPekubYYRFXtVxMHobry0JACLDMFPTJAk,51208
8
- squirrels/_auth.py,sha256=u3ISed2yavktOseN9rf5uXcPupfS4pK6rlC60RvyrgI,19305
5
+ squirrels/__init__.py,sha256=O9QBS0WHdmWM7SUCTnEk1VU8ElnJKKrFHGAEd8W5CkM,288
6
+ squirrels/_api_server.py,sha256=y-GW7e7vKgeZsL50Md9dwsc7WR5Oe3Mu-yAZOK96_wE,14071
7
+ squirrels/_auth.py,sha256=3VWbB4-mMEqUp3YL1-T6R-kOswZ4xx29H0SSSlp2ezM,45592
9
8
  squirrels/_command_line.py,sha256=ZB7cTJabWiYEV6tFi1l87M-w468LJ-DV7TNgQ_bqFbY,11151
10
- squirrels/_connection_set.py,sha256=170NqnDgAisseTY4Iqzw79CZeT3oMIdUNuz3HLHZSGM,4009
11
- squirrels/_constants.py,sha256=ddKMtnBAzUNuXHT4zT63oo1OpWy9LPXakDeLduikM6Q,3127
12
- squirrels/_dashboard_types.py,sha256=g27ET5CnM_4IvxdhtGBjaRUM4MeK1kDsIfcp78AIYFE,1950
13
- squirrels/_dashboards_io.py,sha256=bWgXdqTNV57x3zlptmFnG_RX5s1zO-TV2CvrQpcCTs8,3001
9
+ squirrels/_connection_set.py,sha256=-krmn3RJkPOmh6oP3t8PhGhInyQ2pLu5HulYDi-aUrI,4008
10
+ squirrels/_constants.py,sha256=6sBvx5ijWgLz4KJGZo-VZ8NPZvw1aVROsYxA6ac2pZ8,3337
11
+ squirrels/_dashboards.py,sha256=FF6KG-GXohR5Hp-BMYRwEZ2hUQ_jbkFpcK5epwoD64w,4891
14
12
  squirrels/_data_sources.py,sha256=W1aUAHkkjPpBRjZGBrc9RJIxEwICfnzB8HAyknCoZ3E,25988
15
13
  squirrels/_dataset_types.py,sha256=wZvvRs4U1Hva_iyoFosAAu76S1yfv1Cd5SX3UMIw2PA,2838
16
- squirrels/_exceptions.py,sha256=ny_-lS-V-G4ss7XT98cg83kl2KWZfP-K1plkgK2ElSg,2505
14
+ squirrels/_exceptions.py,sha256=-hfDZoV-JhvuNRAERTFg-5NFjr0uNk4i9ogNgJfAyyc,1171
17
15
  squirrels/_initializer.py,sha256=bJ0AeFIHYIYVT6o_kw5oWQJ9Qm4-n1RVNk3iYi2qbH0,14270
18
16
  squirrels/_manifest.py,sha256=ag902b0SNbra6XrBzkaKizmJGwRgFKPjEjtnkJgfFio,10232
19
- squirrels/_model_builder.py,sha256=fdoBcbbDpQaSTApV0mHKbq3yFe5xrr-TggdehEx8xE0,5063
17
+ squirrels/_model_builder.py,sha256=5aLL_DxOv_wgJc3ml26q3hZSpmzkbniBC34Dh-f2ML4,5096
20
18
  squirrels/_model_configs.py,sha256=eJne5L-QLv42s7uodUOOASMRn1NbzGbiBmwMllkO5C4,3303
21
- squirrels/_model_queries.py,sha256=mvx3r0nps8rBGjmxP_SaGjDmKsjh5CBWQ6uAWxX1LnU,1088
22
- squirrels/_models.py,sha256=pP_NuYtBj8pmC--3BpNp6cVTxfkCUkZf19sB2v817Bg,49721
19
+ squirrels/_model_queries.py,sha256=2fl07feHtzddBpiXUWGfNBYeoN_EZ7K5mAz2Jc-vCvs,1087
20
+ squirrels/_models.py,sha256=_oudu_szkcBSAXYloxAd2LV2-gbTY6d8-E1kQuc89c0,50017
23
21
  squirrels/_package_loader.py,sha256=xcIur5Z38OWd-OVjsueFstVV567zlkK9UBnLS4NawJY,1158
24
- squirrels/_parameter_configs.py,sha256=GwKtuZJIMIsGx2d1bWlluqw3FkwIRv7nzekYzMQN4zg,23950
22
+ squirrels/_parameter_configs.py,sha256=wxF51hMOewtf5SZgo6dEHa3vzU81vXN4LNmIK0UK9po,23981
25
23
  squirrels/_parameter_options.py,sha256=cWYKNoBUopHq6VfaeBu-nN2V0_IY3OgYpmYhKODNCew,16956
26
- squirrels/_parameter_sets.py,sha256=K_4HKu2keC1-EvaOSBS9PRDRjooicqfTz_NMk1ZqqRs,9823
27
- squirrels/_parameters.py,sha256=Bg3MqgkBb-QjtEIgBwA35730cOM3KCvq0_qFz-HZaZw,55942
28
- squirrels/_project.py,sha256=3RnBZVRJtZasuZ-KqIqCM5vOmBXtQr4_FTqrvFp5n5w,29307
29
- squirrels/_py_module.py,sha256=LgILTjMx3jyb92o8Y35ezpntuk6u6ezYVGAICKhUSUM,2622
24
+ squirrels/_parameter_sets.py,sha256=xTiwDBT3LONILHlitA12J2b9CS4NU6-S1IQSILT2v8A,10315
25
+ squirrels/_parameters.py,sha256=Ta929_a-b8r1miDKXT_SZtHT_d8jJ2W5ABvkXL7k074,76372
26
+ squirrels/_project.py,sha256=y6r_ppujlSbxboS52zSIKfyzRo1kC1eQ9nkdYcHJc30,30391
27
+ squirrels/_py_module.py,sha256=NW86sv1oYWh8oMqgGh2oIUwuFcLXoq3cwEuKRxPfEwY,2683
30
28
  squirrels/_seeds.py,sha256=yyIYp4bn9Sg6lhgvsOYIJQHIpPbvLNsyGHVfswEyVd8,2225
31
29
  squirrels/_sources.py,sha256=j5mY_EtA5cxoHwtk8RwTVHO74hleik2lS7mF9gVnG_A,4840
32
- squirrels/_utils.py,sha256=A2_zBxdKKf-HmwlI6vdsS6VMrefDF1km9TBuwnOG-ME,12279
30
+ squirrels/_utils.py,sha256=yZ7pyWUZxCkAtOZO00QAN9kRM9KNpIUwERnnaiXCpxg,13552
33
31
  squirrels/_version.py,sha256=M8aFbJ4vlAi3Sk9b7leRuEfkNBjkkX5S_F9lA4h8GK4,105
34
- squirrels/arguments.py,sha256=Sg8m5_LYrL4IMb7fzZGmyNceA2vQMD7fiu0cXNir_V4,165
32
+ squirrels/arguments.py,sha256=s8Ud0ahdgyMO9jO_qV9uYzJ3rCApAeRKpNKCz_EfQjI,181
33
+ squirrels/auth.py,sha256=GNalDnQo4I1ONSYUGWwAxhJmgEwLYy688Fby-ksvnHk,54
35
34
  squirrels/connections.py,sha256=dpjR00DjLiPfF0iz9HfL9PyjWYtYQq67zk2LyT2bnhg,64
36
- squirrels/dashboards.py,sha256=hPpuG4xJIZdY7OQFcO_CdO630KCSgLzHdMSsGjlpt5U,57
35
+ squirrels/dashboards.py,sha256=UA-mksRXtL5dyzewoGTnR5bz26KzvrVgKsIpNIPkLe0,52
37
36
  squirrels/data_sources.py,sha256=6a4E1m-Zcx-o2SERwKJiz8n6LFsI2CQ5MbmDwKhVq1o,170
38
37
  squirrels/parameter_options.py,sha256=TmvoESS1BSPx-73bNzo6W0bSztj7-ye0_vssmii0cBk,205
39
38
  squirrels/parameters.py,sha256=7Go5jetD3J7NDrkk2a_7ExxJv0mT4rwo1B9sAkuxPPg,195
40
- squirrels/types.py,sha256=xhCOlIxXOERG1Th_jTSE6yYJvFMNeATdZbqRFVsEhBg,263
41
- squirrels/_arguments/_init_time_args.py,sha256=_XfZjsqP9GWhJYppnokRAAFXO1mSqf1mUcKG0KgWk7o,3448
42
- squirrels/_arguments/_run_time_args.py,sha256=ppakg7XXnhgIkycingldxcaDqXtBkgpfyMpiFhAYK9Y,5017
43
- squirrels/_package_data/base_project/.env,sha256=3Tuk3SJl7RNMebm8P0jocqB-Ox91aW09Zreq8QjyUJ4,1026
44
- squirrels/_package_data/base_project/.env.example,sha256=dS5ZO_kyMT3GdZbj6nfiK9PzkvwGfse9-UiYkvrh7ys,1078
39
+ squirrels/types.py,sha256=snP-ZLW_YY1TCCPVQTcDQMQvU6159yMRL-Hou9L7QJU,282
40
+ squirrels/_api_routes/__init__.py,sha256=-oGMDfM5Qo7NDiwpjHt8BKVZNNZ4798-NZCJzYCUdbc,106
41
+ squirrels/_api_routes/auth.py,sha256=BzThmdxugavFDW_RF2LNpPf8Z4fQcxrHL52TLPrsj54,13917
42
+ squirrels/_api_routes/base.py,sha256=7H4SzhBJ-yWtC70kjzZneW_ZkejVzwTSnMlunSBt8ig,7067
43
+ squirrels/_api_routes/dashboards.py,sha256=tasnHUmgkm-ZcpEviRoCiNtR9TgY6xjBZac7SkyEJBg,8097
44
+ squirrels/_api_routes/data_management.py,sha256=Gr6JVFpMaFhlQCUICGxuDNWrz4BGPydrMou0elBg5tU,5347
45
+ squirrels/_api_routes/datasets.py,sha256=Xcf-sTI8Noe06Tc-Nm5tJt4Y94ljl8ZXSxVrTOrN9gw,13592
46
+ squirrels/_api_routes/oauth2.py,sha256=1Q0mgbOw39iOGKy_Wwuuxi_Iw5rRCXhZPKc_CXYsBH4,14855
47
+ squirrels/_api_routes/project.py,sha256=gEtfDa1RyjxGEcUZTnNZSgQbLeIb5wUv2n65fZd6d40,11546
48
+ squirrels/_arguments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
+ squirrels/_arguments/init_time_args.py,sha256=Y5gVbrU_yd1r5JtAChcL1xTpfz0i8gwNtZMe5QBT2mc,3511
50
+ squirrels/_arguments/run_time_args.py,sha256=Izh7nxifrzEmMCY41vzz8TWBJ5cSAHuXkjPRZRx6mHM,5016
51
+ squirrels/_package_data/base_project/.env,sha256=CX13teP-pc3ymHIg9oERWcSGvxvMWRDOylLmI1Oiqpg,1105
52
+ squirrels/_package_data/base_project/.env.example,sha256=eN9fIwZF0CbK5mz0zJ4xsZ2I2MwY7Tj6f1ZsudfJKkA,1157
45
53
  squirrels/_package_data/base_project/connections.yml,sha256=qZxh7OuI2xqf2cFKwpMo5TONrJXGVzQ7YfcWh4Go7Oo,1011
46
54
  squirrels/_package_data/base_project/duckdb_init.sql,sha256=iwKDoHbKhOEMe-Pu_sX5a9OauCgqxfZLD70S7RduBrE,196
47
55
  squirrels/_package_data/base_project/gitignore,sha256=B9OEkQ_j9fZGA2IAyVUvXeylxpya-AUwzLzqzMN4Bfc,155
@@ -66,15 +74,21 @@ squirrels/_package_data/base_project/models/federates/federate_example.sql,sha25
66
74
  squirrels/_package_data/base_project/models/federates/federate_example.yml,sha256=FpSVZV7xNSH5iUAXMkyd_jynFxWryutz5Izlgsq3aP0,2279
67
75
  squirrels/_package_data/base_project/pyconfigs/connections.py,sha256=IRL9D2HG9pdwp12S896b0Q-hKnFfVck8we1q5sifX10,601
68
76
  squirrels/_package_data/base_project/pyconfigs/context.py,sha256=8zsQ0tf0EhfeRDPwK7ANNaJWAubT3AYVCzcdaOz0xwY,3670
69
- squirrels/_package_data/base_project/pyconfigs/parameters.py,sha256=B3AdnzJzpK5BC_fADl7og_hsmejAYNqgjpQO-4eFYXk,5355
70
- squirrels/_package_data/base_project/pyconfigs/user.py,sha256=qP0dpptza-xBBZKxN3-KBfaDwaD5ZUGhUBqU8WqBNXk,855
77
+ squirrels/_package_data/base_project/pyconfigs/parameters.py,sha256=VEHGP8UsoV7KQQAQB-K_6ciTOLZwO6h7OgW2Q4QJk5I,4496
78
+ squirrels/_package_data/base_project/pyconfigs/user.py,sha256=5gRP_I5C2-z-gTaNt0F9yqk0yLzu6xrS0pdGvb8ZET4,2030
71
79
  squirrels/_package_data/base_project/seeds/seed_categories.csv,sha256=jppjf1nOIxy7-bi5lJn5CVqmnLfJHHq0ABgp6UqbXnw,104
72
80
  squirrels/_package_data/base_project/seeds/seed_categories.yml,sha256=NZ4BVvYYCEq6OnjRLrE_WOMhYsW0BQhRPWOgUchzdp4,435
73
81
  squirrels/_package_data/base_project/seeds/seed_subcategories.csv,sha256=Tta1oIgnc2nukNMDlUkIErRKNH_8YT5EPp1A2kQKcow,327
74
82
  squirrels/_package_data/base_project/seeds/seed_subcategories.yml,sha256=QTgw8Eld-p6Kntf53FyXyn7-7vKYI7IOJVu-Lr-FHCY,583
75
83
  squirrels/_package_data/base_project/tmp/.gitignore,sha256=XImoqcWvJY0C0L_TWCx1ljvqU7qh9fUTJmK4ACCmNFI,13
76
- squirrels-0.5.0b3.dist-info/METADATA,sha256=PIrRC6cNC2UbwcUSK7eVbiiK9LM8l43_zX9qI5M66n4,4399
77
- squirrels-0.5.0b3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
78
- squirrels-0.5.0b3.dist-info/entry_points.txt,sha256=i6vgjhJ3o_cdSFYofFcNY9DFMPr4MIcuwnkskSTXfJc,95
79
- squirrels-0.5.0b3.dist-info/licenses/LICENSE,sha256=qqERuumQtQVsMrEXvJHuecJvV2sLxbleEubd_Zk8dY8,11338
80
- squirrels-0.5.0b3.dist-info/RECORD,,
84
+ squirrels/_package_data/templates/dataset_results.html,sha256=Y-1xtw4ZzHcoW1vfWR1amnFJqFUMvwJDY3eeY7bfKJw,2793
85
+ squirrels/_package_data/templates/oauth_login.html,sha256=LmR7zbjFUYxWIhB9nhIkoGDmqUgEtSXCqo0pKrWqwXw,7108
86
+ squirrels/_schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
87
+ squirrels/_schemas/auth_models.py,sha256=3EpMtGip5iPfA4ixWBa5ipOy9LrQafC047nc1Mhjz3w,5625
88
+ squirrels/_schemas/query_param_models.py,sha256=zPfvr2kHEhGqbFDDs4oGsy8zYt-acwO0mfq1ELIpEI8,4094
89
+ squirrels/_schemas/response_models.py,sha256=lvz37EPPDkj6ZNb8ZprnCgpmFHrZA4Xg-04MVxBqfLg,13616
90
+ squirrels-0.5.0b4.dist-info/METADATA,sha256=QCKmfVXmjXUPl2qya_8KIyolw072cgjrWt61LYOXZYc,4496
91
+ squirrels-0.5.0b4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
92
+ squirrels-0.5.0b4.dist-info/entry_points.txt,sha256=i6vgjhJ3o_cdSFYofFcNY9DFMPr4MIcuwnkskSTXfJc,95
93
+ squirrels-0.5.0b4.dist-info/licenses/LICENSE,sha256=qqERuumQtQVsMrEXvJHuecJvV2sLxbleEubd_Zk8dY8,11338
94
+ squirrels-0.5.0b4.dist-info/RECORD,,