squirrels 0.1.0__py3-none-any.whl → 0.6.0.post0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. dateutils/__init__.py +6 -0
  2. dateutils/_enums.py +25 -0
  3. squirrels/dateutils.py → dateutils/_implementation.py +409 -380
  4. dateutils/types.py +6 -0
  5. squirrels/__init__.py +21 -18
  6. squirrels/_api_routes/__init__.py +5 -0
  7. squirrels/_api_routes/auth.py +337 -0
  8. squirrels/_api_routes/base.py +196 -0
  9. squirrels/_api_routes/dashboards.py +156 -0
  10. squirrels/_api_routes/data_management.py +148 -0
  11. squirrels/_api_routes/datasets.py +220 -0
  12. squirrels/_api_routes/project.py +289 -0
  13. squirrels/_api_server.py +552 -134
  14. squirrels/_arguments/__init__.py +0 -0
  15. squirrels/_arguments/init_time_args.py +83 -0
  16. squirrels/_arguments/run_time_args.py +111 -0
  17. squirrels/_auth.py +777 -0
  18. squirrels/_command_line.py +239 -107
  19. squirrels/_compile_prompts.py +147 -0
  20. squirrels/_connection_set.py +94 -0
  21. squirrels/_constants.py +141 -64
  22. squirrels/_dashboards.py +179 -0
  23. squirrels/_data_sources.py +570 -0
  24. squirrels/_dataset_types.py +91 -0
  25. squirrels/_env_vars.py +209 -0
  26. squirrels/_exceptions.py +29 -0
  27. squirrels/_http_error_responses.py +52 -0
  28. squirrels/_initializer.py +319 -110
  29. squirrels/_logging.py +121 -0
  30. squirrels/_manifest.py +357 -187
  31. squirrels/_mcp_server.py +578 -0
  32. squirrels/_model_builder.py +69 -0
  33. squirrels/_model_configs.py +74 -0
  34. squirrels/_model_queries.py +52 -0
  35. squirrels/_models.py +1201 -0
  36. squirrels/_package_data/base_project/.env +7 -0
  37. squirrels/_package_data/base_project/.env.example +44 -0
  38. squirrels/_package_data/base_project/connections.yml +16 -0
  39. squirrels/_package_data/base_project/dashboards/dashboard_example.py +40 -0
  40. squirrels/_package_data/base_project/dashboards/dashboard_example.yml +22 -0
  41. squirrels/_package_data/base_project/docker/.dockerignore +16 -0
  42. squirrels/_package_data/base_project/docker/Dockerfile +16 -0
  43. squirrels/_package_data/base_project/docker/compose.yml +7 -0
  44. squirrels/_package_data/base_project/duckdb_init.sql +10 -0
  45. squirrels/_package_data/base_project/gitignore +13 -0
  46. squirrels/_package_data/base_project/macros/macros_example.sql +17 -0
  47. squirrels/_package_data/base_project/models/builds/build_example.py +26 -0
  48. squirrels/_package_data/base_project/models/builds/build_example.sql +16 -0
  49. squirrels/_package_data/base_project/models/builds/build_example.yml +57 -0
  50. squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +17 -0
  51. squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +32 -0
  52. squirrels/_package_data/base_project/models/federates/federate_example.py +51 -0
  53. squirrels/_package_data/base_project/models/federates/federate_example.sql +21 -0
  54. squirrels/_package_data/base_project/models/federates/federate_example.yml +65 -0
  55. squirrels/_package_data/base_project/models/sources.yml +38 -0
  56. squirrels/_package_data/base_project/parameters.yml +142 -0
  57. squirrels/_package_data/base_project/pyconfigs/connections.py +19 -0
  58. squirrels/_package_data/base_project/pyconfigs/context.py +96 -0
  59. squirrels/_package_data/base_project/pyconfigs/parameters.py +141 -0
  60. squirrels/_package_data/base_project/pyconfigs/user.py +56 -0
  61. squirrels/_package_data/base_project/resources/expenses.db +0 -0
  62. squirrels/_package_data/base_project/resources/public/.gitkeep +0 -0
  63. squirrels/_package_data/base_project/resources/weather.db +0 -0
  64. squirrels/_package_data/base_project/seeds/seed_categories.csv +6 -0
  65. squirrels/_package_data/base_project/seeds/seed_categories.yml +15 -0
  66. squirrels/_package_data/base_project/seeds/seed_subcategories.csv +15 -0
  67. squirrels/_package_data/base_project/seeds/seed_subcategories.yml +21 -0
  68. squirrels/_package_data/base_project/squirrels.yml.j2 +61 -0
  69. squirrels/_package_data/base_project/tmp/.gitignore +2 -0
  70. squirrels/_package_data/templates/login_successful.html +53 -0
  71. squirrels/_package_data/templates/squirrels_studio.html +22 -0
  72. squirrels/_package_loader.py +29 -0
  73. squirrels/_parameter_configs.py +592 -0
  74. squirrels/_parameter_options.py +348 -0
  75. squirrels/_parameter_sets.py +207 -0
  76. squirrels/_parameters.py +1703 -0
  77. squirrels/_project.py +796 -0
  78. squirrels/_py_module.py +122 -0
  79. squirrels/_request_context.py +33 -0
  80. squirrels/_schemas/__init__.py +0 -0
  81. squirrels/_schemas/auth_models.py +83 -0
  82. squirrels/_schemas/query_param_models.py +70 -0
  83. squirrels/_schemas/request_models.py +26 -0
  84. squirrels/_schemas/response_models.py +286 -0
  85. squirrels/_seeds.py +97 -0
  86. squirrels/_sources.py +112 -0
  87. squirrels/_utils.py +540 -149
  88. squirrels/_version.py +1 -3
  89. squirrels/arguments.py +7 -0
  90. squirrels/auth.py +4 -0
  91. squirrels/connections.py +3 -0
  92. squirrels/dashboards.py +3 -0
  93. squirrels/data_sources.py +14 -282
  94. squirrels/parameter_options.py +13 -189
  95. squirrels/parameters.py +14 -801
  96. squirrels/types.py +18 -0
  97. squirrels-0.6.0.post0.dist-info/METADATA +148 -0
  98. squirrels-0.6.0.post0.dist-info/RECORD +101 -0
  99. {squirrels-0.1.0.dist-info → squirrels-0.6.0.post0.dist-info}/WHEEL +1 -2
  100. {squirrels-0.1.0.dist-info → squirrels-0.6.0.post0.dist-info}/entry_points.txt +1 -0
  101. squirrels-0.6.0.post0.dist-info/licenses/LICENSE +201 -0
  102. squirrels/_credentials_manager.py +0 -87
  103. squirrels/_module_loader.py +0 -37
  104. squirrels/_parameter_set.py +0 -151
  105. squirrels/_renderer.py +0 -286
  106. squirrels/_timed_imports.py +0 -37
  107. squirrels/connection_set.py +0 -126
  108. squirrels/package_data/base_project/.gitignore +0 -4
  109. squirrels/package_data/base_project/connections.py +0 -21
  110. squirrels/package_data/base_project/database/sample_database.db +0 -0
  111. squirrels/package_data/base_project/database/seattle_weather.db +0 -0
  112. squirrels/package_data/base_project/datasets/sample_dataset/context.py +0 -8
  113. squirrels/package_data/base_project/datasets/sample_dataset/database_view1.py +0 -23
  114. squirrels/package_data/base_project/datasets/sample_dataset/database_view1.sql.j2 +0 -7
  115. squirrels/package_data/base_project/datasets/sample_dataset/final_view.py +0 -10
  116. squirrels/package_data/base_project/datasets/sample_dataset/final_view.sql.j2 +0 -2
  117. squirrels/package_data/base_project/datasets/sample_dataset/parameters.py +0 -30
  118. squirrels/package_data/base_project/datasets/sample_dataset/selections.cfg +0 -6
  119. squirrels/package_data/base_project/squirrels.yaml +0 -26
  120. squirrels/package_data/static/favicon.ico +0 -0
  121. squirrels/package_data/static/script.js +0 -234
  122. squirrels/package_data/static/style.css +0 -110
  123. squirrels/package_data/templates/index.html +0 -32
  124. squirrels-0.1.0.dist-info/LICENSE +0 -22
  125. squirrels-0.1.0.dist-info/METADATA +0 -67
  126. squirrels-0.1.0.dist-info/RECORD +0 -40
  127. squirrels-0.1.0.dist-info/top_level.txt +0 -1
squirrels/_env_vars.py ADDED
@@ -0,0 +1,209 @@
1
+ from typing import Any, Literal, Optional
2
+ from typing_extensions import Self
3
+ from pathlib import Path
4
+ from pydantic import BaseModel, Field, field_validator, model_validator, ConfigDict
5
+ import json
6
+
7
+ from . import _constants as c
8
+
9
+
10
+ class SquirrelsEnvVars(BaseModel):
11
+ """
12
+ Pydantic model for managing and validating Squirrels environment variables.
13
+ These variables are typically loaded from .env files or the system environment.
14
+ """
15
+ model_config = ConfigDict(serialize_by_alias=True)
16
+ project_path: str
17
+
18
+ # Security
19
+ secret_key: Optional[str] = Field(
20
+ None, alias=c.SQRL_SECRET_KEY,
21
+ description="Secret key for JWT encoding/decoding and other security operations"
22
+ )
23
+ secret_admin_password: Optional[str] = Field(
24
+ None, alias=c.SQRL_SECRET_ADMIN_PASSWORD,
25
+ description="Password for the admin user"
26
+ )
27
+
28
+ # Auth
29
+ auth_db_file_path: str = Field(
30
+ "{project_path}/target/auth.sqlite", alias=c.SQRL_AUTH_DB_FILE_PATH,
31
+ description="Path to the SQLite authentication database"
32
+ )
33
+ auth_token_expire_minutes: float = Field(
34
+ 30, ge=0, alias=c.SQRL_AUTH_TOKEN_EXPIRE_MINUTES,
35
+ description="Expiration time for access tokens in minutes"
36
+ )
37
+ auth_credential_origins: list[str] = Field(
38
+ ["https://squirrels-analytics.github.io"], alias=c.SQRL_AUTH_CREDENTIAL_ORIGINS,
39
+ description="Allowed origins for credentials (cookies)"
40
+ )
41
+
42
+ # Permissions
43
+ elevated_access_level: Literal["admin", "member", "guest"] = Field(
44
+ "admin", alias=c.SQRL_PERMISSIONS_ELEVATED_ACCESS_LEVEL,
45
+ description="Minimum access level to access the studio"
46
+ )
47
+
48
+ # Parameters Cache
49
+ parameters_cache_size: int = Field(
50
+ 1024, ge=0, alias=c.SQRL_PARAMETERS_CACHE_SIZE,
51
+ description="Cache size for parameter configs"
52
+ )
53
+ parameters_cache_ttl_minutes: float = Field(
54
+ 60, gt=0, alias=c.SQRL_PARAMETERS_CACHE_TTL_MINUTES,
55
+ description="Cache TTL for parameter configs in minutes"
56
+ )
57
+ parameters_datasource_refresh_minutes: float = Field(
58
+ 60, alias=c.SQRL_PARAMETERS_DATASOURCE_REFRESH_MINUTES,
59
+ description="Interval in minutes for refreshing data sources. A non-positive value disables auto-refresh"
60
+ )
61
+
62
+ # Datasets Cache
63
+ datasets_cache_size: int = Field(
64
+ 128, ge=0, alias=c.SQRL_DATASETS_CACHE_SIZE,
65
+ description="Cache size for dataset results"
66
+ )
67
+ datasets_cache_ttl_minutes: float = Field(
68
+ 60, gt=0, alias=c.SQRL_DATASETS_CACHE_TTL_MINUTES,
69
+ description="Cache TTL for dataset results in minutes"
70
+ )
71
+ datasets_max_rows_for_ai: int = Field(
72
+ 100, ge=0, alias=c.SQRL_DATASETS_MAX_ROWS_FOR_AI,
73
+ description="Max rows for AI queries"
74
+ )
75
+ datasets_max_rows_output: int = Field(
76
+ 100000, ge=0, alias=c.SQRL_DATASETS_MAX_ROWS_OUTPUT,
77
+ description="Max rows for dataset output"
78
+ )
79
+ datasets_sql_timeout_seconds: float = Field(
80
+ 2.0, gt=0, alias=c.SQRL_DATASETS_SQL_TIMEOUT_SECONDS,
81
+ description="Timeout for SQL operations in seconds"
82
+ )
83
+
84
+ # Dashboards Cache
85
+ dashboards_cache_size: int = Field(
86
+ 128, ge=0, alias=c.SQRL_DASHBOARDS_CACHE_SIZE,
87
+ description="Cache size for dashboards"
88
+ )
89
+ dashboards_cache_ttl_minutes: float = Field(
90
+ 60, gt=0, alias=c.SQRL_DASHBOARDS_CACHE_TTL_MINUTES,
91
+ description="Cache TTL for dashboards in minutes"
92
+ )
93
+
94
+ # Seeds
95
+ seeds_infer_schema: bool = Field(
96
+ True, alias=c.SQRL_SEEDS_INFER_SCHEMA,
97
+ description="Whether to infer schema for seeds"
98
+ )
99
+ seeds_na_values: list[str] = Field(
100
+ ["NA"], alias=c.SQRL_SEEDS_NA_VALUES,
101
+ description="List of N/A values for seeds"
102
+ )
103
+
104
+ # Connections
105
+ connections_default_name_used: str = Field(
106
+ "default", alias=c.SQRL_CONNECTIONS_DEFAULT_NAME_USED,
107
+ description="Default connection name to use"
108
+ )
109
+
110
+ # VDL
111
+ vdl_catalog_db_path: str = Field(
112
+ "ducklake:{project_path}/target/vdl_catalog.duckdb", alias=c.SQRL_VDL_CATALOG_DB_PATH,
113
+ description="Path to the DuckDB catalog database"
114
+ )
115
+ vdl_data_path: str = Field(
116
+ "{project_path}/target/vdl_data/", alias=c.SQRL_VDL_DATA_PATH,
117
+ description="Path to the VDL data directory"
118
+ )
119
+
120
+ # Studio
121
+ studio_base_url: str = Field(
122
+ "https://squirrels-analytics.github.io/squirrels-studio-v2", alias=c.SQRL_STUDIO_BASE_URL,
123
+ description="Base URL for Squirrels Studio"
124
+ )
125
+
126
+ # Logging
127
+ logging_level: str = Field(
128
+ "INFO", alias=c.SQRL_LOGGING_LEVEL,
129
+ description="Logging level"
130
+ )
131
+ logging_format: str = Field(
132
+ "text", alias=c.SQRL_LOGGING_FORMAT,
133
+ description="Logging format"
134
+ )
135
+ logging_to_file: bool | str = Field(
136
+ False, alias=c.SQRL_LOGGING_TO_FILE,
137
+ description="Whether to log to file. Can be set to true to use the default 'logs/' folder, or a folder path to write to a custom folder."
138
+ )
139
+ logging_file_size_mb: float = Field(
140
+ 50, gt=0, alias=c.SQRL_LOGGING_FILE_SIZE_MB,
141
+ description="Max log file size in MB"
142
+ )
143
+ logging_file_backup_count: int = Field(
144
+ 1, ge=0, alias=c.SQRL_LOGGING_FILE_BACKUP_COUNT,
145
+ description="Number of backup log files to keep"
146
+ )
147
+
148
+ @field_validator("project_path")
149
+ @classmethod
150
+ def validate_project_path_exists(cls, v: str) -> str:
151
+ """Validate that the project_path is a folder that contains a squirrels.yml file."""
152
+ path = Path(v)
153
+ if not path.exists():
154
+ raise ValueError(f"Project path does not exist: {v}")
155
+ if not path.is_dir():
156
+ raise ValueError(f"Project path must be a directory, not a file: {v}")
157
+ # squirrels_yml = path / c.MANIFEST_FILE
158
+ # if not squirrels_yml.exists():
159
+ # raise ValueError(f"Project path must contain a {c.MANIFEST_FILE} file: {v}")
160
+ return v
161
+
162
+ @field_validator("auth_credential_origins", mode="before")
163
+ @classmethod
164
+ def parse_origins(cls, v: Any) -> list[str]:
165
+ if isinstance(v, str):
166
+ res = [x.strip() for x in v.split(",") if x.strip()]
167
+ return res or ["https://squirrels-analytics.github.io"]
168
+ return v
169
+
170
+ @field_validator("seeds_na_values", mode="before")
171
+ @classmethod
172
+ def parse_json_list(cls, v: Any) -> list[str]:
173
+ if isinstance(v, str):
174
+ try:
175
+ parsed = json.loads(v)
176
+ if not isinstance(parsed, list):
177
+ raise ValueError(f"The {c.SQRL_SEEDS_NA_VALUES} environment variable must be a JSON list")
178
+ return parsed
179
+ except json.JSONDecodeError:
180
+ return []
181
+ return v
182
+
183
+ @field_validator("logging_to_file", mode="before")
184
+ @classmethod
185
+ def parse_logging_to_file(cls, v: Any) -> bool | str:
186
+ if isinstance(v, str):
187
+ v_lower = v.lower()
188
+ if v_lower in ("true", "t", "1", "yes", "y", "on"):
189
+ return True
190
+ if v_lower in ("false", "f", "0", "no", "n", "off"):
191
+ return False
192
+ return v
193
+ return bool(v)
194
+
195
+ @field_validator("seeds_infer_schema", mode="before")
196
+ @classmethod
197
+ def parse_bool(cls, v: Any) -> bool:
198
+ if isinstance(v, str):
199
+ return v.lower() in ("true", "t", "1", "yes", "y", "on")
200
+ return bool(v)
201
+
202
+ @model_validator(mode="after")
203
+ def format_paths_with_filepath(self) -> Self:
204
+ """Format paths containing {filepath} placeholder with the actual filepath value."""
205
+ self.auth_db_file_path = self.auth_db_file_path.format(project_path=self.project_path)
206
+ self.vdl_catalog_db_path = self.vdl_catalog_db_path.format(project_path=self.project_path)
207
+ self.vdl_data_path = self.vdl_data_path.format(project_path=self.project_path)
208
+ return self
209
+
@@ -0,0 +1,29 @@
1
+ class InvalidInputError(Exception):
2
+ """
3
+ Use this exception when the error is due to providing invalid inputs to the REST API
4
+
5
+ Attributes:
6
+ status_code: The HTTP status code to return
7
+ error: A short error message that should never change in the future
8
+ error_description: A detailed error message (that is allowed to change in the future)
9
+ """
10
+ def __init__(self, status_code: int, error: str, error_description: str, *args) -> None:
11
+ self.status_code = status_code
12
+ self.error = error
13
+ self.error_description = error_description
14
+ super().__init__(error_description, *args)
15
+
16
+
17
+ class ConfigurationError(Exception):
18
+ """
19
+ Use this exception when the server error is due to errors in the squirrels project instead of the squirrels framework/library
20
+ """
21
+ pass
22
+
23
+
24
+ class FileExecutionError(Exception):
25
+ def __init__(self, message: str, error: Exception, *args) -> None:
26
+ t = " "
27
+ new_message = f"\n" + message + f"\n{t}Produced error message:\n{t}{t}{error} (see above for more details on handled exception)"
28
+ super().__init__(new_message, *args)
29
+ self.error = error
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ from starlette.requests import Request
4
+ from starlette.responses import JSONResponse
5
+
6
+ from ._exceptions import InvalidInputError
7
+
8
+
9
+ def _strip_path_suffix_from_base_url(request: Request, *, strip_path_suffix: str | None) -> str:
10
+ """
11
+ Return a base URL with a known mount suffix removed.
12
+
13
+ In Squirrels, the main app mounts sub-apps like `/api/0` and `/mcp`. When we're
14
+ handling a request inside those sub-apps, `request.base_url` includes the mount
15
+ path. For `WWW-Authenticate` we want to point to a top-level endpoint on the main
16
+ app (same mount as the main app itself), so we strip only the *sub-app mount*
17
+ suffix (e.g. `/api/0` or `/mcp`), not any outer mount path.
18
+ """
19
+ base_url = str(request.base_url).rstrip("/")
20
+ if strip_path_suffix:
21
+ suffix = strip_path_suffix.rstrip("/")
22
+ if suffix and base_url.endswith(suffix):
23
+ base_url = base_url[: -len(suffix)]
24
+ return base_url.rstrip("/")
25
+
26
+
27
+ def invalid_input_error_to_json_response(
28
+ request: Request,
29
+ exc: InvalidInputError,
30
+ *,
31
+ oauth_resource_metadata_path: str = "/.well-known/oauth-protected-resource",
32
+ strip_path_suffix: str | None = None,
33
+ is_mcp: bool = False,
34
+ ) -> JSONResponse:
35
+ """
36
+ Convert an InvalidInputError into the standard Squirrels JSON error response.
37
+
38
+ For 401s, also sets `WWW-Authenticate` with a top-level `resource_metadata` URL.
39
+ """
40
+ response = JSONResponse(
41
+ status_code=exc.status_code,
42
+ content={"error": exc.error, "error_description": exc.error_description},
43
+ )
44
+
45
+ if exc.status_code == 401:
46
+ top_level_base_url = _strip_path_suffix_from_base_url(request, strip_path_suffix=strip_path_suffix)
47
+ resource_metadata_url = f"{top_level_base_url}{oauth_resource_metadata_path}"
48
+ realm = "mcp" if is_mcp else "api"
49
+ response.headers["WWW-Authenticate"] = f'Bearer realm="{realm}", resource_metadata="{resource_metadata_url}"'
50
+
51
+ return response
52
+