squirrels 0.5.0rc0__py3-none-any.whl → 0.5.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of squirrels might be problematic. Click here for more details.
- dateutils/__init__.py +6 -0
- dateutils/_enums.py +25 -0
- squirrels/dateutils.py → dateutils/_implementation.py +58 -111
- dateutils/types.py +6 -0
- squirrels/__init__.py +10 -12
- squirrels/_api_routes/__init__.py +5 -0
- squirrels/_api_routes/auth.py +271 -0
- squirrels/_api_routes/base.py +171 -0
- squirrels/_api_routes/dashboards.py +158 -0
- squirrels/_api_routes/data_management.py +148 -0
- squirrels/_api_routes/datasets.py +265 -0
- squirrels/_api_routes/oauth2.py +298 -0
- squirrels/_api_routes/project.py +252 -0
- squirrels/_api_server.py +245 -781
- squirrels/_arguments/__init__.py +0 -0
- squirrels/{arguments → _arguments}/init_time_args.py +7 -2
- squirrels/{arguments → _arguments}/run_time_args.py +13 -35
- squirrels/_auth.py +720 -212
- squirrels/_command_line.py +81 -41
- squirrels/_compile_prompts.py +147 -0
- squirrels/_connection_set.py +16 -7
- squirrels/_constants.py +29 -9
- squirrels/{_dashboards_io.py → _dashboards.py} +87 -6
- squirrels/_data_sources.py +570 -0
- squirrels/{dataset_result.py → _dataset_types.py} +2 -4
- squirrels/_exceptions.py +9 -37
- squirrels/_initializer.py +83 -59
- squirrels/_logging.py +117 -0
- squirrels/_manifest.py +129 -62
- squirrels/_model_builder.py +10 -52
- squirrels/_model_configs.py +3 -3
- squirrels/_model_queries.py +1 -1
- squirrels/_models.py +249 -118
- squirrels/{package_data → _package_data}/base_project/.env +16 -4
- squirrels/{package_data → _package_data}/base_project/.env.example +15 -3
- squirrels/{package_data → _package_data}/base_project/connections.yml +4 -3
- squirrels/{package_data → _package_data}/base_project/dashboards/dashboard_example.py +4 -4
- squirrels/_package_data/base_project/dashboards/dashboard_example.yml +22 -0
- squirrels/{package_data → _package_data}/base_project/duckdb_init.sql +1 -0
- squirrels/_package_data/base_project/macros/macros_example.sql +17 -0
- squirrels/{package_data → _package_data}/base_project/models/builds/build_example.py +2 -2
- squirrels/{package_data → _package_data}/base_project/models/builds/build_example.sql +1 -1
- squirrels/{package_data → _package_data}/base_project/models/builds/build_example.yml +2 -0
- squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +17 -0
- squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +32 -0
- squirrels/_package_data/base_project/models/federates/federate_example.py +48 -0
- squirrels/_package_data/base_project/models/federates/federate_example.sql +21 -0
- squirrels/{package_data → _package_data}/base_project/models/federates/federate_example.yml +7 -7
- squirrels/{package_data → _package_data}/base_project/models/sources.yml +5 -6
- squirrels/{package_data → _package_data}/base_project/parameters.yml +32 -45
- squirrels/_package_data/base_project/pyconfigs/connections.py +18 -0
- squirrels/{package_data → _package_data}/base_project/pyconfigs/context.py +31 -22
- squirrels/_package_data/base_project/pyconfigs/parameters.py +141 -0
- squirrels/_package_data/base_project/pyconfigs/user.py +44 -0
- squirrels/{package_data → _package_data}/base_project/seeds/seed_categories.yml +1 -1
- squirrels/{package_data → _package_data}/base_project/seeds/seed_subcategories.yml +1 -1
- squirrels/_package_data/base_project/squirrels.yml.j2 +61 -0
- squirrels/_package_data/templates/dataset_results.html +112 -0
- squirrels/_package_data/templates/oauth_login.html +271 -0
- squirrels/_package_data/templates/squirrels_studio.html +20 -0
- squirrels/_parameter_configs.py +76 -55
- squirrels/_parameter_options.py +348 -0
- squirrels/_parameter_sets.py +53 -45
- squirrels/_parameters.py +1664 -0
- squirrels/_project.py +403 -242
- squirrels/_py_module.py +3 -2
- squirrels/_request_context.py +33 -0
- squirrels/_schemas/__init__.py +0 -0
- squirrels/_schemas/auth_models.py +167 -0
- squirrels/_schemas/query_param_models.py +75 -0
- squirrels/{_api_response_models.py → _schemas/response_models.py} +48 -18
- squirrels/_seeds.py +1 -1
- squirrels/_sources.py +23 -19
- squirrels/_utils.py +121 -39
- squirrels/_version.py +1 -1
- squirrels/arguments.py +7 -0
- squirrels/auth.py +4 -0
- squirrels/connections.py +3 -0
- squirrels/dashboards.py +2 -81
- squirrels/data_sources.py +14 -563
- squirrels/parameter_options.py +13 -348
- squirrels/parameters.py +14 -1266
- squirrels/types.py +16 -0
- {squirrels-0.5.0rc0.dist-info → squirrels-0.5.1.dist-info}/METADATA +42 -30
- squirrels-0.5.1.dist-info/RECORD +98 -0
- squirrels/package_data/base_project/dashboards/dashboard_example.yml +0 -22
- squirrels/package_data/base_project/macros/macros_example.sql +0 -15
- squirrels/package_data/base_project/models/dbviews/dbview_example.sql +0 -12
- squirrels/package_data/base_project/models/dbviews/dbview_example.yml +0 -26
- squirrels/package_data/base_project/models/federates/federate_example.py +0 -44
- squirrels/package_data/base_project/models/federates/federate_example.sql +0 -17
- squirrels/package_data/base_project/pyconfigs/connections.py +0 -14
- squirrels/package_data/base_project/pyconfigs/parameters.py +0 -93
- squirrels/package_data/base_project/pyconfigs/user.py +0 -23
- squirrels/package_data/base_project/squirrels.yml.j2 +0 -71
- squirrels-0.5.0rc0.dist-info/RECORD +0 -70
- /squirrels/{package_data → _package_data}/base_project/assets/expenses.db +0 -0
- /squirrels/{package_data → _package_data}/base_project/assets/weather.db +0 -0
- /squirrels/{package_data → _package_data}/base_project/docker/.dockerignore +0 -0
- /squirrels/{package_data → _package_data}/base_project/docker/Dockerfile +0 -0
- /squirrels/{package_data → _package_data}/base_project/docker/compose.yml +0 -0
- /squirrels/{package_data/base_project/.gitignore → _package_data/base_project/gitignore} +0 -0
- /squirrels/{package_data → _package_data}/base_project/seeds/seed_categories.csv +0 -0
- /squirrels/{package_data → _package_data}/base_project/seeds/seed_subcategories.csv +0 -0
- /squirrels/{package_data → _package_data}/base_project/tmp/.gitignore +0 -0
- {squirrels-0.5.0rc0.dist-info → squirrels-0.5.1.dist-info}/WHEEL +0 -0
- {squirrels-0.5.0rc0.dist-info → squirrels-0.5.1.dist-info}/entry_points.txt +0 -0
- {squirrels-0.5.0rc0.dist-info → squirrels-0.5.1.dist-info}/licenses/LICENSE +0 -0
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
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Request context management using ContextVars for request-scoped data.
|
|
3
|
+
Provides thread-safe and async-safe access to request IDs throughout the request lifecycle.
|
|
4
|
+
"""
|
|
5
|
+
from contextvars import ContextVar
|
|
6
|
+
import uuid
|
|
7
|
+
import base64
|
|
8
|
+
|
|
9
|
+
# ContextVar for storing the current request ID
|
|
10
|
+
_request_id: ContextVar[str | None] = ContextVar("request_id", default=None)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_request_id() -> str | None:
|
|
14
|
+
"""
|
|
15
|
+
Get the current request ID from the context.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
The request ID string if available, None otherwise (e.g., in background tasks).
|
|
19
|
+
"""
|
|
20
|
+
return _request_id.get()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def set_request_id() -> str:
|
|
24
|
+
"""
|
|
25
|
+
Set a new request ID in the context.
|
|
26
|
+
Uses base64 URL-safe encoding of UUID bytes to create a shorter ID (22 chars vs 36).
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
The request ID that was set.
|
|
30
|
+
"""
|
|
31
|
+
request_id = base64.urlsafe_b64encode(uuid.uuid4().bytes).decode().rstrip('=')
|
|
32
|
+
_request_id.set(request_id)
|
|
33
|
+
return request_id
|
|
File without changes
|
|
@@ -0,0 +1,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,16 +1,20 @@
|
|
|
1
|
-
from typing import Annotated, Literal
|
|
1
|
+
from typing import Annotated, Literal, Any
|
|
2
2
|
from pydantic import BaseModel, Field
|
|
3
|
-
from datetime import
|
|
3
|
+
from datetime import date
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from .. import _model_configs as mc, _sources as s
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
@@ -95,6 +99,14 @@ parameters_path_description = "The API path to the parameters for the dataset /
|
|
|
95
99
|
metadata_path_description = "The API path to the metadata (i.e., description and schema) for the dataset"
|
|
96
100
|
result_path_description = "The API path to the results for the dataset / dashboard"
|
|
97
101
|
|
|
102
|
+
class ConfigurableDefaultModel(BaseModel):
|
|
103
|
+
name: str
|
|
104
|
+
default: str
|
|
105
|
+
|
|
106
|
+
class ConfigurableItemModel(ConfigurableDefaultModel):
|
|
107
|
+
label: str
|
|
108
|
+
description: str
|
|
109
|
+
|
|
98
110
|
class ColumnModel(BaseModel):
|
|
99
111
|
name: Annotated[str, Field(examples=["mycol"], description="Name of column")]
|
|
100
112
|
type: Annotated[str, Field(examples=["string", "integer", "boolean", "datetime"], description='Column type (such as "string", "integer", "boolean", "datetime", etc.)')]
|
|
@@ -114,18 +126,19 @@ class DatasetItemModel(BaseModel):
|
|
|
114
126
|
name: Annotated[str, Field(examples=["mydataset"], description=name_description)]
|
|
115
127
|
label: Annotated[str, Field(examples=["My Dataset"], description=label_description)]
|
|
116
128
|
description: Annotated[str, Field(examples=[""], description=description_description)]
|
|
117
|
-
|
|
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.")]
|
|
118
131
|
data_schema: Annotated[SchemaWithConditionModel, Field(alias="schema", description="JSON object describing the schema of the dataset")]
|
|
119
|
-
parameters_path: Annotated[str, Field(examples=["/squirrels
|
|
120
|
-
result_path: Annotated[str, Field(examples=["/squirrels
|
|
121
|
-
|
|
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
|
+
|
|
122
135
|
class DashboardItemModel(ParametersModel):
|
|
123
136
|
name: Annotated[str, Field(examples=["mydashboard"], description=name_description)]
|
|
124
137
|
label: Annotated[str, Field(examples=["My Dashboard"], description=label_description)]
|
|
125
138
|
description: Annotated[str, Field(examples=[""], description=description_description)]
|
|
126
139
|
parameters: Annotated[list[str], Field(examples=["myparam1", "myparam2"], description="The list of parameter names used by the dashboard")]
|
|
127
|
-
parameters_path: Annotated[str, Field(examples=["/squirrels
|
|
128
|
-
result_path: Annotated[str, Field(examples=["/squirrels
|
|
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)]
|
|
129
142
|
result_format: Annotated[str, Field(examples=["png", "html"], description="The format of the dashboard's result API response (one of 'png' or 'html')")]
|
|
130
143
|
|
|
131
144
|
ModelConfigType = mc.ModelConfig | s.Source | mc.SeedConfig | mc.BuildModelConfig | mc.DbviewModelConfig | mc.FederateModelConfig
|
|
@@ -151,13 +164,16 @@ class LineageRelation(BaseModel):
|
|
|
151
164
|
source: LineageNode
|
|
152
165
|
target: LineageNode
|
|
153
166
|
|
|
154
|
-
class
|
|
155
|
-
parameters: Annotated[ParametersListType, Field(description="The list of all parameters in the project")]
|
|
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.")]
|
|
156
169
|
datasets: Annotated[list[DatasetItemModel], Field(description="The list of accessible datasets")]
|
|
170
|
+
|
|
171
|
+
class CatalogModel(CatalogModelForTool):
|
|
157
172
|
dashboards: Annotated[list[DashboardItemModel], Field(description="The list of accessible dashboards")]
|
|
158
173
|
connections: Annotated[list[ConnectionItemModel], Field(description="The list of connections in the project (only provided for admin users)")]
|
|
159
174
|
models: Annotated[list[DataModelItem], Field(description="The list of data models in the project (only provided for admin users)")]
|
|
160
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)")]
|
|
161
177
|
|
|
162
178
|
|
|
163
179
|
## Dataset Results Response Models
|
|
@@ -176,15 +192,29 @@ class DatasetResultModel(BaseModel):
|
|
|
176
192
|
)]
|
|
177
193
|
|
|
178
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
|
+
|
|
179
203
|
## Project Metadata Response Models
|
|
180
204
|
|
|
181
205
|
class ProjectVersionModel(BaseModel):
|
|
182
206
|
major_version: Annotated[int, Field(examples=[1])]
|
|
183
|
-
data_catalog_path: Annotated[str, Field(examples=["/squirrels
|
|
207
|
+
data_catalog_path: Annotated[str, Field(examples=["/squirrels/v0/project/myproject/v1/data-catalog"])]
|
|
184
208
|
|
|
185
209
|
class ProjectModel(BaseModel):
|
|
186
210
|
name: Annotated[str, Field(examples=["myproject"])]
|
|
187
211
|
version: Annotated[str, Field(examples=["v1"])]
|
|
188
212
|
label: Annotated[str, Field(examples=["My Project"])]
|
|
189
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"])]
|
|
190
220
|
squirrels_version: Annotated[str, Field(examples=["0.1.0"])]
|
squirrels/_seeds.py
CHANGED
|
@@ -37,7 +37,7 @@ class SeedsIO:
|
|
|
37
37
|
@classmethod
|
|
38
38
|
def load_files(cls, logger: u.Logger, base_path: str, env_vars: dict[str, str]) -> Seeds:
|
|
39
39
|
start = time.time()
|
|
40
|
-
infer_schema_setting: bool = (env_vars.get(c.SQRL_SEEDS_INFER_SCHEMA, "true")
|
|
40
|
+
infer_schema_setting: bool = u.to_bool(env_vars.get(c.SQRL_SEEDS_INFER_SCHEMA, "true"))
|
|
41
41
|
na_values_setting: list[str] = json.loads(env_vars.get(c.SQRL_SEEDS_NA_VALUES, "[]"))
|
|
42
42
|
|
|
43
43
|
seeds_dict = {}
|
squirrels/_sources.py
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
from pydantic import BaseModel, Field, model_validator
|
|
3
|
-
import time, sqlglot
|
|
3
|
+
import time, sqlglot, yaml
|
|
4
4
|
|
|
5
5
|
from . import _utils as u, _constants as c, _model_configs as mc
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class UpdateHints(BaseModel):
|
|
9
9
|
increasing_column: str | None = Field(default=None)
|
|
10
|
-
strictly_increasing: bool = Field(default=True, description="Delete the max value of the increasing column, ignored if
|
|
11
|
-
selective_overwrite_value: Any = Field(default=None)
|
|
10
|
+
strictly_increasing: bool = Field(default=True, description="Delete the max value of the increasing column, ignored if selective_overwrite_value is set")
|
|
11
|
+
selective_overwrite_value: Any = Field(default=None, description="Delete all values of the increasing column greater than or equal to this value")
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Source(mc.ConnectionInterface, mc.ModelConfig):
|
|
15
15
|
table: str | None = Field(default=None)
|
|
16
|
-
|
|
16
|
+
load_to_vdl: bool = Field(default=False, description="Whether to load the data to the 'virtual data lake' (VDL)")
|
|
17
17
|
primary_key: list[str] = Field(default_factory=list)
|
|
18
18
|
update_hints: UpdateHints = Field(default_factory=UpdateHints)
|
|
19
19
|
|
|
@@ -28,34 +28,28 @@ class Source(mc.ConnectionInterface, mc.ModelConfig):
|
|
|
28
28
|
|
|
29
29
|
def get_cols_for_create_table_stmt(self) -> str:
|
|
30
30
|
cols_clause = ", ".join([f"{col.name} {col.type}" for col in self.columns])
|
|
31
|
-
|
|
32
|
-
return f"{cols_clause}{primary_key_clause}"
|
|
33
|
-
|
|
34
|
-
def get_cols_for_insert_stmt(self) -> str:
|
|
35
|
-
return ", ".join([col.name for col in self.columns])
|
|
31
|
+
return cols_clause
|
|
36
32
|
|
|
37
33
|
def get_max_incr_col_query(self, source_name: str) -> str:
|
|
38
34
|
return f"SELECT max({self.update_hints.increasing_column}) FROM {source_name}"
|
|
39
35
|
|
|
40
|
-
def
|
|
41
|
-
select_cols = self.
|
|
36
|
+
def get_query_for_upsert(self, dialect: str, conn_name: str, table_name: str, max_value_of_increasing_col: Any | None, *, full_refresh: bool = True) -> str:
|
|
37
|
+
select_cols = ", ".join([col.name for col in self.columns])
|
|
42
38
|
if full_refresh or max_value_of_increasing_col is None:
|
|
43
39
|
return f"SELECT {select_cols} FROM db_{conn_name}.{table_name}"
|
|
44
40
|
|
|
45
41
|
increasing_col = self.update_hints.increasing_column
|
|
46
42
|
increasing_col_type = next(col.type for col in self.columns if col.name == increasing_col)
|
|
47
43
|
where_cond = f"{increasing_col}::{increasing_col_type} > '{max_value_of_increasing_col}'::{increasing_col_type}"
|
|
48
|
-
pushdown_query = f"SELECT {select_cols} FROM {table_name} WHERE {where_cond}"
|
|
49
44
|
|
|
50
|
-
if
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
# TODO: figure out if using pushdown query is worth it
|
|
46
|
+
# if dialect in ['postgres', 'mysql']:
|
|
47
|
+
# pushdown_query = f"SELECT {select_cols} FROM {table_name} WHERE {where_cond}"
|
|
48
|
+
# transpiled_query = sqlglot.transpile(pushdown_query, read='duckdb', write=dialect)[0].replace("'", "''")
|
|
49
|
+
# return f"FROM {dialect}_query('db_{conn_name}', '{transpiled_query}')"
|
|
53
50
|
|
|
54
51
|
return f"SELECT {select_cols} FROM db_{conn_name}.{table_name} WHERE {where_cond}"
|
|
55
52
|
|
|
56
|
-
def get_insert_replace_clause(self) -> str:
|
|
57
|
-
return "" if len(self.primary_key) == 0 else "OR REPLACE"
|
|
58
|
-
|
|
59
53
|
|
|
60
54
|
class Sources(BaseModel):
|
|
61
55
|
sources: dict[str, Source] = Field(default_factory=dict)
|
|
@@ -98,7 +92,17 @@ class SourcesIO:
|
|
|
98
92
|
start = time.time()
|
|
99
93
|
|
|
100
94
|
sources_path = u.Path(base_path, c.MODELS_FOLDER, c.SOURCES_FILE)
|
|
101
|
-
|
|
95
|
+
if sources_path.exists():
|
|
96
|
+
raw_content = u.read_file(sources_path)
|
|
97
|
+
rendered = u.render_string(raw_content, base_path=base_path, env_vars=env_vars)
|
|
98
|
+
sources_data = yaml.safe_load(rendered) or {}
|
|
99
|
+
else:
|
|
100
|
+
sources_data = {}
|
|
101
|
+
|
|
102
|
+
if not isinstance(sources_data, dict):
|
|
103
|
+
raise u.ConfigurationError(
|
|
104
|
+
f"Parsed content from YAML file must be a dictionary. Got: {sources_data}"
|
|
105
|
+
)
|
|
102
106
|
|
|
103
107
|
sources = Sources(**sources_data).finalize_null_fields(env_vars)
|
|
104
108
|
|