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.

Files changed (108) 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 +10 -12
  6. squirrels/_api_routes/__init__.py +5 -0
  7. squirrels/_api_routes/auth.py +271 -0
  8. squirrels/_api_routes/base.py +171 -0
  9. squirrels/_api_routes/dashboards.py +158 -0
  10. squirrels/_api_routes/data_management.py +148 -0
  11. squirrels/_api_routes/datasets.py +265 -0
  12. squirrels/_api_routes/oauth2.py +298 -0
  13. squirrels/_api_routes/project.py +252 -0
  14. squirrels/_api_server.py +245 -781
  15. squirrels/_arguments/__init__.py +0 -0
  16. squirrels/{arguments → _arguments}/init_time_args.py +7 -2
  17. squirrels/{arguments → _arguments}/run_time_args.py +13 -35
  18. squirrels/_auth.py +720 -212
  19. squirrels/_command_line.py +81 -41
  20. squirrels/_compile_prompts.py +147 -0
  21. squirrels/_connection_set.py +16 -7
  22. squirrels/_constants.py +29 -9
  23. squirrels/{_dashboards_io.py → _dashboards.py} +87 -6
  24. squirrels/_data_sources.py +570 -0
  25. squirrels/{dataset_result.py → _dataset_types.py} +2 -4
  26. squirrels/_exceptions.py +9 -37
  27. squirrels/_initializer.py +83 -59
  28. squirrels/_logging.py +117 -0
  29. squirrels/_manifest.py +129 -62
  30. squirrels/_model_builder.py +10 -52
  31. squirrels/_model_configs.py +3 -3
  32. squirrels/_model_queries.py +1 -1
  33. squirrels/_models.py +249 -118
  34. squirrels/{package_data → _package_data}/base_project/.env +16 -4
  35. squirrels/{package_data → _package_data}/base_project/.env.example +15 -3
  36. squirrels/{package_data → _package_data}/base_project/connections.yml +4 -3
  37. squirrels/{package_data → _package_data}/base_project/dashboards/dashboard_example.py +4 -4
  38. squirrels/_package_data/base_project/dashboards/dashboard_example.yml +22 -0
  39. squirrels/{package_data → _package_data}/base_project/duckdb_init.sql +1 -0
  40. squirrels/_package_data/base_project/macros/macros_example.sql +17 -0
  41. squirrels/{package_data → _package_data}/base_project/models/builds/build_example.py +2 -2
  42. squirrels/{package_data → _package_data}/base_project/models/builds/build_example.sql +1 -1
  43. squirrels/{package_data → _package_data}/base_project/models/builds/build_example.yml +2 -0
  44. squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +17 -0
  45. squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +32 -0
  46. squirrels/_package_data/base_project/models/federates/federate_example.py +48 -0
  47. squirrels/_package_data/base_project/models/federates/federate_example.sql +21 -0
  48. squirrels/{package_data → _package_data}/base_project/models/federates/federate_example.yml +7 -7
  49. squirrels/{package_data → _package_data}/base_project/models/sources.yml +5 -6
  50. squirrels/{package_data → _package_data}/base_project/parameters.yml +32 -45
  51. squirrels/_package_data/base_project/pyconfigs/connections.py +18 -0
  52. squirrels/{package_data → _package_data}/base_project/pyconfigs/context.py +31 -22
  53. squirrels/_package_data/base_project/pyconfigs/parameters.py +141 -0
  54. squirrels/_package_data/base_project/pyconfigs/user.py +44 -0
  55. squirrels/{package_data → _package_data}/base_project/seeds/seed_categories.yml +1 -1
  56. squirrels/{package_data → _package_data}/base_project/seeds/seed_subcategories.yml +1 -1
  57. squirrels/_package_data/base_project/squirrels.yml.j2 +61 -0
  58. squirrels/_package_data/templates/dataset_results.html +112 -0
  59. squirrels/_package_data/templates/oauth_login.html +271 -0
  60. squirrels/_package_data/templates/squirrels_studio.html +20 -0
  61. squirrels/_parameter_configs.py +76 -55
  62. squirrels/_parameter_options.py +348 -0
  63. squirrels/_parameter_sets.py +53 -45
  64. squirrels/_parameters.py +1664 -0
  65. squirrels/_project.py +403 -242
  66. squirrels/_py_module.py +3 -2
  67. squirrels/_request_context.py +33 -0
  68. squirrels/_schemas/__init__.py +0 -0
  69. squirrels/_schemas/auth_models.py +167 -0
  70. squirrels/_schemas/query_param_models.py +75 -0
  71. squirrels/{_api_response_models.py → _schemas/response_models.py} +48 -18
  72. squirrels/_seeds.py +1 -1
  73. squirrels/_sources.py +23 -19
  74. squirrels/_utils.py +121 -39
  75. squirrels/_version.py +1 -1
  76. squirrels/arguments.py +7 -0
  77. squirrels/auth.py +4 -0
  78. squirrels/connections.py +3 -0
  79. squirrels/dashboards.py +2 -81
  80. squirrels/data_sources.py +14 -563
  81. squirrels/parameter_options.py +13 -348
  82. squirrels/parameters.py +14 -1266
  83. squirrels/types.py +16 -0
  84. {squirrels-0.5.0rc0.dist-info → squirrels-0.5.1.dist-info}/METADATA +42 -30
  85. squirrels-0.5.1.dist-info/RECORD +98 -0
  86. squirrels/package_data/base_project/dashboards/dashboard_example.yml +0 -22
  87. squirrels/package_data/base_project/macros/macros_example.sql +0 -15
  88. squirrels/package_data/base_project/models/dbviews/dbview_example.sql +0 -12
  89. squirrels/package_data/base_project/models/dbviews/dbview_example.yml +0 -26
  90. squirrels/package_data/base_project/models/federates/federate_example.py +0 -44
  91. squirrels/package_data/base_project/models/federates/federate_example.sql +0 -17
  92. squirrels/package_data/base_project/pyconfigs/connections.py +0 -14
  93. squirrels/package_data/base_project/pyconfigs/parameters.py +0 -93
  94. squirrels/package_data/base_project/pyconfigs/user.py +0 -23
  95. squirrels/package_data/base_project/squirrels.yml.j2 +0 -71
  96. squirrels-0.5.0rc0.dist-info/RECORD +0 -70
  97. /squirrels/{package_data → _package_data}/base_project/assets/expenses.db +0 -0
  98. /squirrels/{package_data → _package_data}/base_project/assets/weather.db +0 -0
  99. /squirrels/{package_data → _package_data}/base_project/docker/.dockerignore +0 -0
  100. /squirrels/{package_data → _package_data}/base_project/docker/Dockerfile +0 -0
  101. /squirrels/{package_data → _package_data}/base_project/docker/compose.yml +0 -0
  102. /squirrels/{package_data/base_project/.gitignore → _package_data/base_project/gitignore} +0 -0
  103. /squirrels/{package_data → _package_data}/base_project/seeds/seed_categories.csv +0 -0
  104. /squirrels/{package_data → _package_data}/base_project/seeds/seed_subcategories.csv +0 -0
  105. /squirrels/{package_data → _package_data}/base_project/tmp/.gitignore +0 -0
  106. {squirrels-0.5.0rc0.dist-info → squirrels-0.5.1.dist-info}/WHEEL +0 -0
  107. {squirrels-0.5.0rc0.dist-info → squirrels-0.5.1.dist-info}/entry_points.txt +0 -0
  108. {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 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
@@ -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
- parameters: Annotated[list[str], Field(examples=["myparam1", "myparam2"], description="The list of parameter names used by the dataset")]
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-v0/myproject/v1/dataset/mydataset/parameters"], description=parameters_path_description)]
120
- result_path: Annotated[str, Field(examples=["/squirrels-v0/myproject/v1/dataset/mydataset"], description=result_path_description)]
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-v0/myproject/v1/dashboard/mydashboard/parameters"], description=parameters_path_description)]
128
- result_path: Annotated[str, Field(examples=["/squirrels-v0/myproject/v1/dashboard/mydashboard"], description=result_path_description)]
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 CatalogModel(BaseModel):
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-v0/project/myproject/v1/data-catalog"])]
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").lower() == "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 value is set")
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
- load_to_duckdb: bool = Field(default=False, description="Whether to load the data to DuckDB")
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
- primary_key_clause = f", PRIMARY KEY ({', '.join(self.primary_key)})" if self.primary_key else ""
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 get_query_for_insert(self, dialect: str, conn_name: str, table_name: str, max_value_of_increasing_col: Any | None, *, full_refresh: bool = True) -> str:
41
- select_cols = self.get_cols_for_insert_stmt()
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 dialect in ['postgres', 'mysql']:
51
- transpiled_query = sqlglot.transpile(pushdown_query, read='duckdb', write=dialect)[0].replace("'", "''")
52
- return f"FROM {dialect}_query('db_{conn_name}', '{transpiled_query}')"
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
- sources_data = u.load_yaml_config(sources_path) if sources_path.exists() else {}
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