squirrels 0.4.0__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of squirrels might be problematic. Click here for more details.

Files changed (125) hide show
  1. dateutils/__init__.py +6 -0
  2. dateutils/_enums.py +25 -0
  3. squirrels/dateutils.py → dateutils/_implementation.py +58 -111
  4. dateutils/types.py +6 -0
  5. squirrels/__init__.py +13 -11
  6. squirrels/_api_routes/__init__.py +5 -0
  7. squirrels/_api_routes/auth.py +271 -0
  8. squirrels/_api_routes/base.py +165 -0
  9. squirrels/_api_routes/dashboards.py +150 -0
  10. squirrels/_api_routes/data_management.py +145 -0
  11. squirrels/_api_routes/datasets.py +257 -0
  12. squirrels/_api_routes/oauth2.py +298 -0
  13. squirrels/_api_routes/project.py +252 -0
  14. squirrels/_api_server.py +256 -450
  15. squirrels/_arguments/__init__.py +0 -0
  16. squirrels/_arguments/init_time_args.py +108 -0
  17. squirrels/_arguments/run_time_args.py +147 -0
  18. squirrels/_auth.py +960 -0
  19. squirrels/_command_line.py +126 -45
  20. squirrels/_compile_prompts.py +147 -0
  21. squirrels/_connection_set.py +48 -26
  22. squirrels/_constants.py +68 -38
  23. squirrels/_dashboards.py +160 -0
  24. squirrels/_data_sources.py +570 -0
  25. squirrels/_dataset_types.py +84 -0
  26. squirrels/_exceptions.py +29 -0
  27. squirrels/_initializer.py +177 -80
  28. squirrels/_logging.py +115 -0
  29. squirrels/_manifest.py +208 -79
  30. squirrels/_model_builder.py +69 -0
  31. squirrels/_model_configs.py +74 -0
  32. squirrels/_model_queries.py +52 -0
  33. squirrels/_models.py +926 -367
  34. squirrels/_package_data/base_project/.env +42 -0
  35. squirrels/_package_data/base_project/.env.example +42 -0
  36. squirrels/_package_data/base_project/assets/expenses.db +0 -0
  37. squirrels/_package_data/base_project/connections.yml +16 -0
  38. squirrels/_package_data/base_project/dashboards/dashboard_example.py +34 -0
  39. squirrels/_package_data/base_project/dashboards/dashboard_example.yml +22 -0
  40. squirrels/{package_data → _package_data}/base_project/docker/.dockerignore +5 -2
  41. squirrels/{package_data → _package_data}/base_project/docker/Dockerfile +3 -3
  42. squirrels/{package_data → _package_data}/base_project/docker/compose.yml +1 -1
  43. squirrels/_package_data/base_project/duckdb_init.sql +10 -0
  44. squirrels/{package_data/base_project/.gitignore → _package_data/base_project/gitignore} +3 -2
  45. squirrels/_package_data/base_project/macros/macros_example.sql +17 -0
  46. squirrels/_package_data/base_project/models/builds/build_example.py +26 -0
  47. squirrels/_package_data/base_project/models/builds/build_example.sql +16 -0
  48. squirrels/_package_data/base_project/models/builds/build_example.yml +57 -0
  49. squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +12 -0
  50. squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +26 -0
  51. squirrels/_package_data/base_project/models/federates/federate_example.py +37 -0
  52. squirrels/_package_data/base_project/models/federates/federate_example.sql +19 -0
  53. squirrels/_package_data/base_project/models/federates/federate_example.yml +65 -0
  54. squirrels/_package_data/base_project/models/sources.yml +38 -0
  55. squirrels/{package_data → _package_data}/base_project/parameters.yml +56 -40
  56. squirrels/_package_data/base_project/pyconfigs/connections.py +14 -0
  57. squirrels/{package_data → _package_data}/base_project/pyconfigs/context.py +21 -40
  58. squirrels/_package_data/base_project/pyconfigs/parameters.py +141 -0
  59. squirrels/_package_data/base_project/pyconfigs/user.py +44 -0
  60. squirrels/_package_data/base_project/seeds/seed_categories.yml +15 -0
  61. squirrels/_package_data/base_project/seeds/seed_subcategories.csv +15 -0
  62. squirrels/_package_data/base_project/seeds/seed_subcategories.yml +21 -0
  63. squirrels/_package_data/base_project/squirrels.yml.j2 +61 -0
  64. squirrels/_package_data/templates/dataset_results.html +112 -0
  65. squirrels/_package_data/templates/oauth_login.html +271 -0
  66. squirrels/_package_data/templates/squirrels_studio.html +20 -0
  67. squirrels/_package_loader.py +8 -4
  68. squirrels/_parameter_configs.py +104 -103
  69. squirrels/_parameter_options.py +348 -0
  70. squirrels/_parameter_sets.py +57 -47
  71. squirrels/_parameters.py +1664 -0
  72. squirrels/_project.py +721 -0
  73. squirrels/_py_module.py +7 -5
  74. squirrels/_schemas/__init__.py +0 -0
  75. squirrels/_schemas/auth_models.py +167 -0
  76. squirrels/_schemas/query_param_models.py +75 -0
  77. squirrels/{_api_response_models.py → _schemas/response_models.py} +126 -47
  78. squirrels/_seeds.py +35 -16
  79. squirrels/_sources.py +110 -0
  80. squirrels/_utils.py +248 -73
  81. squirrels/_version.py +1 -1
  82. squirrels/arguments.py +7 -0
  83. squirrels/auth.py +4 -0
  84. squirrels/connections.py +3 -0
  85. squirrels/dashboards.py +2 -81
  86. squirrels/data_sources.py +14 -631
  87. squirrels/parameter_options.py +13 -348
  88. squirrels/parameters.py +14 -1266
  89. squirrels/types.py +16 -0
  90. squirrels-0.5.0.dist-info/METADATA +113 -0
  91. squirrels-0.5.0.dist-info/RECORD +97 -0
  92. {squirrels-0.4.0.dist-info → squirrels-0.5.0.dist-info}/WHEEL +1 -1
  93. squirrels-0.5.0.dist-info/entry_points.txt +3 -0
  94. {squirrels-0.4.0.dist-info → squirrels-0.5.0.dist-info/licenses}/LICENSE +1 -1
  95. squirrels/_authenticator.py +0 -85
  96. squirrels/_dashboards_io.py +0 -61
  97. squirrels/_environcfg.py +0 -84
  98. squirrels/arguments/init_time_args.py +0 -40
  99. squirrels/arguments/run_time_args.py +0 -208
  100. squirrels/package_data/assets/favicon.ico +0 -0
  101. squirrels/package_data/assets/index.css +0 -1
  102. squirrels/package_data/assets/index.js +0 -58
  103. squirrels/package_data/base_project/assets/expenses.db +0 -0
  104. squirrels/package_data/base_project/connections.yml +0 -7
  105. squirrels/package_data/base_project/dashboards/dashboard_example.py +0 -32
  106. squirrels/package_data/base_project/dashboards.yml +0 -10
  107. squirrels/package_data/base_project/env.yml +0 -29
  108. squirrels/package_data/base_project/models/dbviews/dbview_example.py +0 -47
  109. squirrels/package_data/base_project/models/dbviews/dbview_example.sql +0 -22
  110. squirrels/package_data/base_project/models/federates/federate_example.py +0 -21
  111. squirrels/package_data/base_project/models/federates/federate_example.sql +0 -3
  112. squirrels/package_data/base_project/pyconfigs/auth.py +0 -45
  113. squirrels/package_data/base_project/pyconfigs/connections.py +0 -19
  114. squirrels/package_data/base_project/pyconfigs/parameters.py +0 -95
  115. squirrels/package_data/base_project/seeds/seed_subcategories.csv +0 -15
  116. squirrels/package_data/base_project/squirrels.yml.j2 +0 -94
  117. squirrels/package_data/templates/index.html +0 -18
  118. squirrels/project.py +0 -378
  119. squirrels/user_base.py +0 -55
  120. squirrels-0.4.0.dist-info/METADATA +0 -117
  121. squirrels-0.4.0.dist-info/RECORD +0 -60
  122. squirrels-0.4.0.dist-info/entry_points.txt +0 -4
  123. /squirrels/{package_data → _package_data}/base_project/assets/weather.db +0 -0
  124. /squirrels/{package_data → _package_data}/base_project/seeds/seed_categories.csv +0 -0
  125. /squirrels/{package_data → _package_data}/base_project/tmp/.gitignore +0 -0
squirrels/_py_module.py CHANGED
@@ -2,6 +2,7 @@ from typing import Type, Optional, Any
2
2
  import importlib.util
3
3
 
4
4
  from . import _constants as c, _utils as u
5
+ from ._exceptions import ConfigurationError, FileExecutionError
5
6
 
6
7
 
7
8
  class PyModule:
@@ -21,7 +22,7 @@ class PyModule:
21
22
  spec.loader.exec_module(self.module)
22
23
  except FileNotFoundError as e:
23
24
  if is_required:
24
- raise u.ConfigurationError(f"Required file not found: '{self.filepath}'") from e
25
+ raise ConfigurationError(f"Required file not found: '{self.filepath}'") from e
25
26
  self.module = default_class
26
27
 
27
28
  def get_func_or_class(self, attr_name: str, *, default_attr: Any = None, is_required: bool = True) -> Any:
@@ -38,15 +39,16 @@ class PyModule:
38
39
  """
39
40
  func_or_class = getattr(self.module, attr_name, default_attr)
40
41
  if func_or_class is None and is_required:
41
- raise u.ConfigurationError(f"Module '{self.filepath}' missing required attribute '{attr_name}'")
42
+ raise ConfigurationError(f"Module '{self.filepath}' missing required attribute '{attr_name}'")
42
43
  return func_or_class
43
44
 
44
45
 
45
- 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:
46
47
  """
47
48
  Given a python file in the 'pyconfigs' folder, run its main function
48
49
 
49
50
  Arguments:
51
+ base_path: The base path of the project
50
52
  filename: The name of the file to run main function
51
53
  kwargs: Dictionary of the main function arguments
52
54
  """
@@ -55,6 +57,6 @@ def run_pyconfig_main(base_path: str, filename: str, kwargs: dict[str, Any] = {}
55
57
  main_function = module.get_func_or_class(c.MAIN_FUNC, is_required=False)
56
58
  if main_function:
57
59
  try:
58
- main_function(**kwargs)
60
+ return main_function(**kwargs)
59
61
  except Exception as e:
60
- raise u.FileExecutionError(f'Failed to run python file "{filepath}"', e) from e
62
+ raise FileExecutionError(f'Failed to run python file "{filepath}"', e) from e
File without changes
@@ -0,0 +1,167 @@
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
+ pass
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
+ title: str
41
+ username: str
42
+ created_at: datetime
43
+ expires_at: datetime
44
+
45
+ @field_serializer('created_at', 'expires_at')
46
+ def serialize_datetime(self, dt: datetime) -> str:
47
+ return dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
48
+
49
+
50
+ class UserField(BaseModel):
51
+ name: str
52
+ type: str
53
+ nullable: bool
54
+ enum: list[str] | None
55
+ default: Any | None
56
+
57
+
58
+ class ProviderConfigs(BaseModel):
59
+ client_id: str
60
+ client_secret: str
61
+ server_url: str
62
+ server_metadata_path: str = Field(default="/.well-known/openid-configuration")
63
+ client_kwargs: dict = Field(default_factory=dict)
64
+ get_user: Callable[[dict], RegisteredUser]
65
+
66
+ @property
67
+ def server_metadata_url(self) -> str:
68
+ return f"{self.server_url}{self.server_metadata_path}"
69
+
70
+
71
+ class AuthProvider(BaseModel):
72
+ name: str
73
+ label: str
74
+ icon: str
75
+ provider_configs: ProviderConfigs
76
+
77
+
78
+ # OAuth 2.1 Models
79
+
80
+ class OAuthClientModel(BaseModel):
81
+ """OAuth client details"""
82
+ model_config = ConfigDict(from_attributes=True)
83
+ client_id: str
84
+ client_name: str
85
+ redirect_uris: list[str]
86
+ scope: str
87
+ grant_types: list[str]
88
+ response_types: list[str]
89
+ created_at: datetime
90
+ is_active: bool
91
+
92
+ @field_serializer('created_at')
93
+ def serialize_datetime(self, dt: datetime) -> str:
94
+ return dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
95
+
96
+
97
+ class ClientRegistrationRequest(BaseModel):
98
+ """Request model for OAuth client registration"""
99
+ client_name: str = Field(description="Human-readable name for the OAuth client")
100
+ redirect_uris: list[str] = Field(description="List of allowed redirect URIs for the client")
101
+ scope: str = Field(default="read", description="Default scope for the client")
102
+ grant_types: list[str] = Field(default=["authorization_code", "refresh_token"], description="Allowed grant types")
103
+ response_types: list[str] = Field(default=["code"], description="Allowed response types")
104
+
105
+
106
+ class ClientUpdateRequest(BaseModel):
107
+ """Request model for OAuth client update"""
108
+ client_name: str | None = Field(default=None, description="Human-readable name for the OAuth client")
109
+ redirect_uris: list[str] | None = Field(default=None, description="List of allowed redirect URIs for the client")
110
+ scope: str | None = Field(default=None, description="Default scope for the client")
111
+ grant_types: list[str] | None = Field(default=None, description="Allowed grant types")
112
+ response_types: list[str] | None = Field(default=None, description="Allowed response types")
113
+ is_active: bool | None = Field(default=None, description="Whether the client is active")
114
+
115
+
116
+ class ClientDetailsResponse(BaseModel):
117
+ """Response model for OAuth client details (without client_secret)"""
118
+ client_id: str = Field(description="Client ID")
119
+ client_name: str = Field(description="Client name")
120
+ redirect_uris: list[str] = Field(description="Registered redirect URIs")
121
+ scope: str = Field(description="Default scope")
122
+ grant_types: list[str] = Field(description="Allowed grant types")
123
+ response_types: list[str] = Field(description="Allowed response types")
124
+ created_at: datetime = Field(description="Registration timestamp")
125
+ is_active: bool = Field(description="Whether the client is active")
126
+
127
+ @field_serializer('created_at')
128
+ def serialize_datetime(self, dt: datetime) -> str:
129
+ return dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
130
+
131
+
132
+ class ClientUpdateResponse(ClientDetailsResponse):
133
+ """Response model for OAuth client update"""
134
+ registration_access_token: str | None = Field(default=None, description="Token for managing this client registration (store securely)")
135
+
136
+
137
+ class ClientRegistrationResponse(ClientUpdateResponse):
138
+ """Response model for OAuth client registration"""
139
+ client_secret: str = Field(description="Generated client secret (store securely)")
140
+ registration_client_uri: str | None = Field(default=None, description="URI for managing this client registration")
141
+
142
+
143
+ class TokenResponse(BaseModel):
144
+ access_token: str
145
+ token_type: str = "bearer"
146
+ expires_in: int
147
+ refresh_token: str | None = None
148
+
149
+
150
+ class OAuthServerMetadata(BaseModel):
151
+ """OAuth 2.1 Authorization Server Metadata (RFC 8414)"""
152
+ issuer: str = Field(description="Authorization server's issuer identifier URL")
153
+ authorization_endpoint: str = Field(description="URL of the authorization endpoint")
154
+ token_endpoint: str = Field(description="URL of the token endpoint")
155
+ revocation_endpoint: str = Field(description="URL of the token revocation endpoint")
156
+ registration_endpoint: str = Field(description="URL of the client registration endpoint")
157
+ scopes_supported: list[str] = Field(description="List of OAuth 2.1 scope values supported")
158
+ response_types_supported: list[str] = Field(description="List of OAuth 2.1 response_type values supported")
159
+ grant_types_supported: list[str] = Field(description="List of OAuth 2.1 grant type values supported")
160
+ token_endpoint_auth_methods_supported: list[str] = Field(
161
+ default=["client_secret_basic", "client_secret_post"],
162
+ description="List of client authentication methods supported by the token endpoint"
163
+ )
164
+ code_challenge_methods_supported: list[str] = Field(
165
+ default=["S256"],
166
+ description="List of PKCE code challenge methods supported"
167
+ )
@@ -0,0 +1,75 @@
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_sql_query", str, description="Optional DuckDB SQL to transform the final dataset. Use table name 'result' to reference the dataset."),
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)
68
+
69
+
70
+ def get_query_models_for_compiled_models(param_fields: dict):
71
+ """Generate query models for fetching compiled model SQL"""
72
+ predefined_params = [
73
+ APIParamFieldInfo("x_verify_params", bool, default=False, description="If true, the query parameters are verified to be valid for the model"),
74
+ ]
75
+ return _get_query_models_helper(None, predefined_params, param_fields)
@@ -1,41 +1,20 @@
1
- from typing import Annotated
1
+ from typing import Annotated, Literal, Any
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
6
 
6
- class LoginReponse(BaseModel):
7
- access_token: Annotated[str, Field(examples=["encoded_jwt_token"], description="An encoded JSON web token to use subsequent API requests")]
8
- token_type: Annotated[str, Field(examples=["bearer"], description='Always "bearer" for Bearer token')]
9
- username: Annotated[str, Field(examples=["johndoe"], description='The username authenticated with from the form data')]
10
- 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")]
11
7
 
8
+ ## Simple Auth Response Models
12
9
 
13
- ## Datasets / Dashboards Catalog Response Models
10
+ class ApiKeyResponse(BaseModel):
11
+ api_key: Annotated[str, Field(examples=["sqrl-12345678"], description="The API key to use subsequent API requests")]
14
12
 
15
- name_description = "The name of the dataset / dashboard (usually in snake case)"
16
- label_description = "The human-friendly display name for the dataset / dashboard"
17
- description_description = "The description of the dataset / dashboard"
18
- parameters_path_description = "The API path to the parameters for the dataset / dashboard"
19
- result_path_description = "The API path to the results for the dataset / dashboard"
20
-
21
- class DatasetItemModel(BaseModel):
22
- name: Annotated[str, Field(examples=["mydataset"], description=name_description)]
23
- label: Annotated[str, Field(examples=["My Dataset"], description=label_description)]
24
- description: Annotated[str, Field(examples=[""], description=description_description)]
25
- parameters_path: Annotated[str, Field(examples=["/squirrels-v0/myproject/v1/dataset/mydataset/parameters"], description=parameters_path_description)]
26
- result_path: Annotated[str, Field(examples=["/squirrels-v0/myproject/v1/dataset/mydataset"], description=result_path_description)]
27
-
28
- class DashboardItemModel(BaseModel):
29
- name: Annotated[str, Field(examples=["mydashboard"], description=name_description)]
30
- label: Annotated[str, Field(examples=["My Dashboard"], description=label_description)]
31
- description: Annotated[str, Field(examples=[""], description=description_description)]
32
- parameters_path: Annotated[str, Field(examples=["/squirrels-v0/myproject/v1/dashboard/mydashboard/parameters"], description=parameters_path_description)]
33
- result_path: Annotated[str, Field(examples=["/squirrels-v0/myproject/v1/dashboard/mydashboard"], description=result_path_description)]
34
- result_format: Annotated[str, Field(examples=["png", "html"], description="The format of the dashboard's result API response (one of 'png' or 'html')")]
35
-
36
- class CatalogModel(BaseModel):
37
- datasets: Annotated[list[DatasetItemModel], Field(description="The list of accessible datasets")]
38
- dashboards: Annotated[list[DashboardItemModel], Field(description="The list of accessible dashboards")]
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")]
39
18
 
40
19
 
41
20
  ## Parameters Response Models
@@ -45,7 +24,7 @@ class ParameterOptionModel(BaseModel):
45
24
  label: Annotated[str, Field(examples=["My Option"], description="The human-friendly display name for the option")]
46
25
 
47
26
  class ParameterModelBase(BaseModel):
48
- widget_type: Annotated[str, Field(examples=["none"], description="The parameter type (set to 'none' for this model)")]
27
+ widget_type: Annotated[str, Field(examples=["disabled"], description="The parameter type")]
49
28
  name: Annotated[str, Field(examples=["my_unique_param_name"], description="The name of the parameter. Use this as the key when providing the API request parameters")]
50
29
  label: Annotated[str, Field(examples=["My Parameter"], description="The human-friendly display name for the parameter")]
51
30
  description: Annotated[str, Field(examples=[""], description="The description of the parameter")]
@@ -102,40 +81,140 @@ class TextParameterModel(ParameterModelBase):
102
81
  description='A string for the input type (one of "text", "textarea", "number", "date", "datetime-local", "month", "time", "color", or "password")'
103
82
  )]
104
83
 
84
+ ParametersListType = list[
85
+ NoneParameterModel | SingleSelectParameterModel | MultiSelectParameterModel | DateParameterModel | DateRangeParameterModel |
86
+ NumberParameterModel | NumberRangeParameterModel | TextParameterModel
87
+ ]
88
+
105
89
  class ParametersModel(BaseModel):
106
- parameters: list[
107
- NoneParameterModel | SingleSelectParameterModel | MultiSelectParameterModel | DateParameterModel | DateRangeParameterModel |
108
- NumberParameterModel | NumberRangeParameterModel | TextParameterModel
109
- ]
90
+ parameters: Annotated[ParametersListType, Field(description="The list of parameters for the dataset / dashboard")]
110
91
 
111
92
 
112
- ## Dataset Results Response Models
93
+ ## Datasets / Dashboards Catalog Response Models
94
+
95
+ name_description = "The name of the dataset / dashboard (usually in snake case)"
96
+ label_description = "The human-friendly display name for the dataset / dashboard"
97
+ description_description = "The description of the dataset / dashboard"
98
+ parameters_path_description = "The API path to the parameters for the dataset / dashboard"
99
+ metadata_path_description = "The API path to the metadata (i.e., description and schema) for the dataset"
100
+ result_path_description = "The API path to the results for the dataset / dashboard"
101
+
102
+ class ConfigurableDefaultModel(BaseModel):
103
+ name: str
104
+ default: str
105
+
106
+ class ConfigurableItemModel(ConfigurableDefaultModel):
107
+ label: str
108
+ description: str
113
109
 
114
110
  class ColumnModel(BaseModel):
115
111
  name: Annotated[str, Field(examples=["mycol"], description="Name of column")]
116
- type: Annotated[str, Field(examples=["string", "number", "integer", "boolean", "datetime"], description='Column type. One of "string", "number", "integer", "boolean", and "datetime"')]
112
+ type: Annotated[str, Field(examples=["string", "integer", "boolean", "datetime"], description='Column type (such as "string", "integer", "boolean", "datetime", etc.)')]
113
+ description: Annotated[str, Field(examples=["My column description"], description="The description of the column")]
114
+ category: Annotated[str, Field(examples=["dimension", "measure", "misc"], description="The category of the column (such as 'dimension', 'measure', or 'misc')")]
115
+
116
+ class ColumnWithConditionModel(ColumnModel):
117
+ condition: Annotated[str | None, Field(None, examples=["My condition"], description="The condition of when the column is included (such as based on a parameter selection)")]
117
118
 
118
119
  class SchemaModel(BaseModel):
119
120
  fields: Annotated[list[ColumnModel], Field(description="A list of JSON objects containing the 'name' and 'type' for each of the columns in the result")]
120
- dimensions: Annotated[list[str], Field(examples=[["mycol"]], description="A list of column names that are dimensions")]
121
+
122
+ class SchemaWithConditionModel(BaseModel):
123
+ fields: Annotated[list[ColumnWithConditionModel], Field(description="A list of JSON objects containing the 'name' and 'type' for each of the columns in the result")]
124
+
125
+ class DatasetItemModel(BaseModel):
126
+ name: Annotated[str, Field(examples=["mydataset"], description=name_description)]
127
+ label: Annotated[str, Field(examples=["My Dataset"], description=label_description)]
128
+ description: Annotated[str, Field(examples=[""], description=description_description)]
129
+ configurables: Annotated[list[ConfigurableDefaultModel], Field(default_factory=list, description="The list of configurables with their default values")]
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
+ parameters_path: Annotated[str, Field(examples=["/squirrels/v0/myproject/v1/dataset/mydataset/parameters"], description=parameters_path_description)]
133
+ result_path: Annotated[str, Field(examples=["/squirrels/v0/myproject/v1/dataset/mydataset"], description=result_path_description)]
134
+
135
+ class DashboardItemModel(ParametersModel):
136
+ name: Annotated[str, Field(examples=["mydashboard"], description=name_description)]
137
+ label: Annotated[str, Field(examples=["My Dashboard"], description=label_description)]
138
+ description: Annotated[str, Field(examples=[""], description=description_description)]
139
+ parameters: Annotated[list[str], Field(examples=["myparam1", "myparam2"], description="The list of parameter names used by the dashboard")]
140
+ parameters_path: Annotated[str, Field(examples=["/squirrels/v0/myproject/v1/dashboard/mydashboard/parameters"], description=parameters_path_description)]
141
+ result_path: Annotated[str, Field(examples=["/squirrels/v0/myproject/v1/dashboard/mydashboard"], description=result_path_description)]
142
+ result_format: Annotated[str, Field(examples=["png", "html"], description="The format of the dashboard's result API response (one of 'png' or 'html')")]
143
+
144
+ ModelConfigType = mc.ModelConfig | s.Source | mc.SeedConfig | mc.BuildModelConfig | mc.DbviewModelConfig | mc.FederateModelConfig
145
+
146
+ class ConnectionItemModel(BaseModel):
147
+ name: Annotated[str, Field(examples=["myconnection"], description="The name of the connection")]
148
+ label: Annotated[str, Field(examples=["My Connection"], description="The human-friendly display name for the connection")]
149
+
150
+ class DataModelItem(BaseModel):
151
+ name: Annotated[str, Field(examples=["model_name"], description="The name of the model")]
152
+ model_type: Annotated[Literal["source", "dbview", "federate", "seed", "build"], Field(
153
+ examples=["source", "dbview", "federate", "seed", "build"], description="The type of the model"
154
+ )]
155
+ config: Annotated[ModelConfigType, Field(description="The configuration of the model")]
156
+ is_queryable: Annotated[bool, Field(examples=[True], description="Whether the model is queryable")]
157
+
158
+ class LineageNode(BaseModel):
159
+ name: str
160
+ type: Literal["model", "dataset", "dashboard"]
161
+
162
+ class LineageRelation(BaseModel):
163
+ type: Literal["buildtime", "runtime"]
164
+ source: LineageNode
165
+ target: LineageNode
166
+
167
+ class CatalogModelForTool(BaseModel):
168
+ 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.")]
169
+ datasets: Annotated[list[DatasetItemModel], Field(description="The list of accessible datasets")]
170
+
171
+ class CatalogModel(CatalogModelForTool):
172
+ dashboards: Annotated[list[DashboardItemModel], Field(description="The list of accessible dashboards")]
173
+ connections: Annotated[list[ConnectionItemModel], Field(description="The list of connections in the project (only provided for admin users)")]
174
+ models: Annotated[list[DataModelItem], Field(description="The list of data models in the project (only provided for admin users)")]
175
+ lineage: Annotated[list[LineageRelation], Field(description="The lineage information between data assets (only provided for admin users)")]
176
+ configurables: Annotated[list[ConfigurableItemModel], Field(description="The list of configurables (only provided for admin users)")]
177
+
178
+
179
+ ## Dataset Results Response Models
180
+
181
+ class DataDetailsModel(BaseModel):
182
+ num_rows: Annotated[int, Field(examples=[2], description="The number of rows in the data field")]
183
+ orientation: Annotated[Literal["records", "rows", "columns"], Field(examples=["records", "rows", "columns"], description="The orientation of the data field")]
121
184
 
122
185
  class DatasetResultModel(BaseModel):
123
186
  data_schema: Annotated[SchemaModel, Field(alias="schema", description="JSON object describing the schema of the dataset")]
124
- data: Annotated[list[dict], Field(
125
- examples=[[{"mycol": "myval"}]],
187
+ total_num_rows: Annotated[int, Field(examples=[2], description="The total number of rows for the dataset")]
188
+ data_details: Annotated[DataDetailsModel, Field(description="A JSON object containing the details of the data field")]
189
+ data: Annotated[list[dict] | list[list] | dict[str, list], Field(
190
+ examples=[[{"mycol": "col_value1"}, {"mycol": "col_value2"}], [["col_value1"], ["col_value2"]], {"mycol": ["col_value1", "col_value2"]}],
126
191
  description="A list of JSON objects where each object is a row of the tabular results. The keys and values of the object are column names (described in fields) and values of the row."
127
192
  )]
128
193
 
129
194
 
195
+ ## Compiled Query Response Model
196
+
197
+ class CompiledQueryModel(BaseModel):
198
+ language: Annotated[Literal["sql", "python"], Field(examples=["sql"], description="The language of the data model query: 'sql' or 'python'")]
199
+ definition: Annotated[str, Field("", description="The compiled SQL or Python definition of the data model.")]
200
+ placeholders: Annotated[dict[str, Any], Field({}, description="The placeholders for the data model.")]
201
+
202
+
130
203
  ## Project Metadata Response Models
131
204
 
132
205
  class ProjectVersionModel(BaseModel):
133
- major_version: int
134
- minor_versions: list[int]
135
- token_path: Annotated[str, Field(examples=["/squirrels-v0/myproject/v1/token"])]
136
- data_catalog_path: Annotated[str, Field(examples=["/squirrels-v0/myproject/v1/datasets"])]
206
+ major_version: Annotated[int, Field(examples=[1])]
207
+ data_catalog_path: Annotated[str, Field(examples=["/squirrels/v0/project/myproject/v1/data-catalog"])]
137
208
 
138
209
  class ProjectModel(BaseModel):
139
210
  name: Annotated[str, Field(examples=["myproject"])]
211
+ version: Annotated[str, Field(examples=["v1"])]
140
212
  label: Annotated[str, Field(examples=["My Project"])]
141
- versions: list[ProjectVersionModel]
213
+ description: Annotated[str, Field(examples=["My project description"])]
214
+ elevated_access_level: Annotated[Literal["admin", "member", "guest"], Field(
215
+ examples=["admin"], description="The access level required to access elevated features (such as configurables and data lineage)"
216
+ )]
217
+ redoc_path: Annotated[str, Field(examples=["/squirrels/v0/project/myproject/v1/redoc"])]
218
+ swagger_path: Annotated[str, Field(examples=["/squirrels/v0/project/myproject/v1/docs"])]
219
+ mcp_server_path: Annotated[str, Field(examples=["/squirrels/v0/project/myproject/v1/mcp"])]
220
+ squirrels_version: Annotated[str, Field(examples=["0.1.0"])]
squirrels/_seeds.py CHANGED
@@ -1,39 +1,58 @@
1
1
  from dataclasses import dataclass
2
- import os, time, glob, pandas as pd
2
+ import os, time, glob, polars as pl, json
3
3
 
4
- from ._manifest import ManifestConfig
5
- from . import _utils as _u, _constants as c
4
+ from . import _utils as u, _constants as c, _model_configs as mc
5
+
6
+
7
+ @dataclass
8
+ class Seed:
9
+ config: mc.SeedConfig
10
+ df: pl.LazyFrame
11
+
12
+ def __post_init__(self):
13
+ if self.config.cast_column_types:
14
+ exprs = []
15
+ for col_config in self.config.columns:
16
+ sqrl_dtype = "double" if col_config.type.lower().startswith("decimal") else col_config.type
17
+ polars_dtype = u.sqrl_dtypes_to_polars_dtypes.get(sqrl_dtype, pl.String)
18
+ exprs.append(pl.col(col_config.name).cast(polars_dtype))
19
+
20
+ self.df = self.df.with_columns(*exprs)
6
21
 
7
22
 
8
23
  @dataclass
9
24
  class Seeds:
10
- _data: dict[str, pd.DataFrame]
11
- _manifest_cfg: ManifestConfig
25
+ _data: dict[str, Seed]
12
26
 
13
- def run_query(self, sql_query: str) -> pd.DataFrame:
14
- use_duckdb = self._manifest_cfg.settings_obj.do_use_duckdb()
15
- return _u.run_sql_on_dataframes(sql_query, self._data, use_duckdb)
27
+ def run_query(self, sql_query: str) -> pl.DataFrame:
28
+ dataframes = {key: seed.df for key, seed in self._data.items()}
29
+ return u.run_sql_on_dataframes(sql_query, dataframes)
16
30
 
17
- def get_dataframes(self) -> dict[str, pd.DataFrame]:
31
+ def get_dataframes(self) -> dict[str, Seed]:
18
32
  return self._data.copy()
19
33
 
20
34
 
21
35
  class SeedsIO:
22
36
 
23
37
  @classmethod
24
- def load_files(cls, logger: _u.Logger, base_path: str, manifest_cfg: ManifestConfig) -> Seeds:
38
+ def load_files(cls, logger: u.Logger, base_path: str, env_vars: dict[str, str]) -> Seeds:
25
39
  start = time.time()
26
- infer_schema: bool = manifest_cfg.settings.get(c.SEEDS_INFER_SCHEMA_SETTING, True)
27
- na_values: list[str] = manifest_cfg.settings.get(c.SEEDS_NA_VALUES_SETTING, ["NA"])
28
- csv_dtype = None if infer_schema else str
40
+ infer_schema_setting: bool = u.to_bool(env_vars.get(c.SQRL_SEEDS_INFER_SCHEMA, "true"))
41
+ na_values_setting: list[str] = json.loads(env_vars.get(c.SQRL_SEEDS_NA_VALUES, "[]"))
29
42
 
30
43
  seeds_dict = {}
31
44
  csv_files = glob.glob(os.path.join(base_path, c.SEEDS_FOLDER, '**/*.csv'), recursive=True)
32
45
  for csv_file in csv_files:
46
+ config_file = os.path.splitext(csv_file)[0] + '.yml'
47
+ config_dict = u.load_yaml_config(config_file) if os.path.exists(config_file) else {}
48
+ config = mc.SeedConfig(**config_dict)
49
+
33
50
  file_stem = os.path.splitext(os.path.basename(csv_file))[0]
34
- df = pd.read_csv(csv_file, dtype=csv_dtype, keep_default_na=False, na_values=na_values)
35
- seeds_dict[file_stem] = df
51
+ infer_schema = not config.cast_column_types and infer_schema_setting
52
+ df = pl.read_csv(csv_file, try_parse_dates=True, infer_schema=infer_schema, null_values=na_values_setting).lazy()
53
+
54
+ seeds_dict[file_stem] = Seed(config, df)
36
55
 
37
- seeds = Seeds(seeds_dict, manifest_cfg)
56
+ seeds = Seeds(seeds_dict)
38
57
  logger.log_activity_time("loading seed files", start)
39
58
  return seeds