squirrels 0.5.0b3__py3-none-any.whl → 0.5.0b4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of squirrels might be problematic. Click here for more details.
- squirrels/__init__.py +2 -0
- squirrels/_api_routes/__init__.py +5 -0
- squirrels/_api_routes/auth.py +262 -0
- squirrels/_api_routes/base.py +154 -0
- squirrels/_api_routes/dashboards.py +142 -0
- squirrels/_api_routes/data_management.py +103 -0
- squirrels/_api_routes/datasets.py +242 -0
- squirrels/_api_routes/oauth2.py +300 -0
- squirrels/_api_routes/project.py +214 -0
- squirrels/_api_server.py +142 -745
- squirrels/_arguments/__init__.py +0 -0
- squirrels/_arguments/{_init_time_args.py → init_time_args.py} +5 -0
- squirrels/_arguments/{_run_time_args.py → run_time_args.py} +1 -1
- squirrels/_auth.py +645 -92
- squirrels/_connection_set.py +1 -1
- squirrels/_constants.py +6 -0
- squirrels/{_dashboards_io.py → _dashboards.py} +87 -6
- squirrels/_exceptions.py +9 -37
- squirrels/_model_builder.py +1 -1
- squirrels/_model_queries.py +1 -1
- squirrels/_models.py +13 -12
- squirrels/_package_data/base_project/.env +1 -0
- squirrels/_package_data/base_project/.env.example +1 -0
- squirrels/_package_data/base_project/pyconfigs/parameters.py +84 -76
- squirrels/_package_data/base_project/pyconfigs/user.py +30 -2
- squirrels/_package_data/templates/dataset_results.html +112 -0
- squirrels/_package_data/templates/oauth_login.html +271 -0
- squirrels/_parameter_configs.py +1 -1
- squirrels/_parameter_sets.py +31 -21
- squirrels/_parameters.py +521 -123
- squirrels/_project.py +43 -24
- squirrels/_py_module.py +3 -2
- squirrels/_schemas/__init__.py +0 -0
- squirrels/_schemas/auth_models.py +144 -0
- squirrels/_schemas/query_param_models.py +67 -0
- squirrels/{_api_response_models.py → _schemas/response_models.py} +12 -8
- squirrels/_utils.py +34 -2
- squirrels/arguments.py +2 -2
- squirrels/auth.py +1 -0
- squirrels/dashboards.py +1 -1
- squirrels/types.py +3 -3
- {squirrels-0.5.0b3.dist-info → squirrels-0.5.0b4.dist-info}/METADATA +4 -1
- {squirrels-0.5.0b3.dist-info → squirrels-0.5.0b4.dist-info}/RECORD +46 -32
- squirrels/_dashboard_types.py +0 -82
- {squirrels-0.5.0b3.dist-info → squirrels-0.5.0b4.dist-info}/WHEEL +0 -0
- {squirrels-0.5.0b3.dist-info → squirrels-0.5.0b4.dist-info}/entry_points.txt +0 -0
- {squirrels-0.5.0b3.dist-info → squirrels-0.5.0b4.dist-info}/licenses/LICENSE +0 -0
squirrels/_connection_set.py
CHANGED
|
@@ -4,7 +4,7 @@ from sqlalchemy import Engine
|
|
|
4
4
|
import time, polars as pl
|
|
5
5
|
|
|
6
6
|
from . import _utils as u, _constants as c, _py_module as pm
|
|
7
|
-
from ._arguments.
|
|
7
|
+
from ._arguments.init_time_args import ConnectionsArgs
|
|
8
8
|
from ._manifest import ManifestConfig, ConnectionProperties, ConnectionTypeEnum
|
|
9
9
|
|
|
10
10
|
|
squirrels/_constants.py
CHANGED
|
@@ -13,6 +13,7 @@ SQRL_SECRET_ADMIN_PASSWORD = 'SQRL_SECRET__ADMIN_PASSWORD'
|
|
|
13
13
|
|
|
14
14
|
SQRL_AUTH_DB_FILE_PATH = 'SQRL_AUTH__DB_FILE_PATH'
|
|
15
15
|
SQRL_AUTH_TOKEN_EXPIRE_MINUTES = 'SQRL_AUTH__TOKEN_EXPIRE_MINUTES'
|
|
16
|
+
SQRL_AUTH_CREDENTIAL_ORIGINS = 'SQRL_AUTH__ALLOWED_ORIGINS_FOR_COOKIES'
|
|
16
17
|
|
|
17
18
|
SQRL_PARAMETERS_CACHE_SIZE = 'SQRL_PARAMETERS__CACHE_SIZE'
|
|
18
19
|
SQRL_PARAMETERS_CACHE_TTL_MINUTES = 'SQRL_PARAMETERS__CACHE_TTL_MINUTES'
|
|
@@ -114,3 +115,8 @@ MAIN_FUNC = "main"
|
|
|
114
115
|
# Regex
|
|
115
116
|
DATE_REGEX = r"^\d{4}\-\d{2}\-\d{2}$"
|
|
116
117
|
COLOR_REGEX = r"^#[0-9a-fA-F]{6}$"
|
|
118
|
+
|
|
119
|
+
# OAuth2
|
|
120
|
+
SUPPORTED_SCOPES = ['read']
|
|
121
|
+
SUPPORTED_GRANT_TYPES = ['authorization_code', 'refresh_token']
|
|
122
|
+
SUPPORTED_RESPONSE_TYPES = ['code']
|
|
@@ -2,15 +2,96 @@ from typing import Type, TypeVar, Callable, Coroutine, Any
|
|
|
2
2
|
from enum import Enum
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from pydantic import BaseModel, Field
|
|
5
|
-
import
|
|
5
|
+
import matplotlib.figure as figure
|
|
6
|
+
import os, time, io, abc, typing
|
|
6
7
|
|
|
7
|
-
from ._arguments.
|
|
8
|
+
from ._arguments.run_time_args import DashboardArgs
|
|
8
9
|
from ._py_module import PyModule
|
|
9
10
|
from ._manifest import AnalyticsOutputConfig
|
|
10
11
|
from ._exceptions import InvalidInputError, ConfigurationError, FileExecutionError
|
|
11
|
-
from . import _constants as c,
|
|
12
|
+
from . import _constants as c, _utils as u
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
|
|
15
|
+
class Dashboard(metaclass=abc.ABCMeta):
|
|
16
|
+
"""
|
|
17
|
+
Abstract parent class for all Dashboard classes.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
@abc.abstractmethod
|
|
22
|
+
def _content(self) -> bytes | str:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
@abc.abstractmethod
|
|
27
|
+
def _format(self) -> str:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PngDashboard(Dashboard):
|
|
32
|
+
"""
|
|
33
|
+
Instantiate a Dashboard in PNG format from a matplotlib figure or bytes
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, content: figure.Figure | io.BytesIO | bytes) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Constructor for PngDashboard
|
|
39
|
+
|
|
40
|
+
Arguments:
|
|
41
|
+
content: The content of the dashboard as a matplotlib.figure.Figure or bytes
|
|
42
|
+
"""
|
|
43
|
+
if isinstance(content, figure.Figure):
|
|
44
|
+
buffer = io.BytesIO()
|
|
45
|
+
content.savefig(buffer, format=c.PNG)
|
|
46
|
+
content = buffer.getvalue()
|
|
47
|
+
|
|
48
|
+
if isinstance(content, io.BytesIO):
|
|
49
|
+
content = content.getvalue()
|
|
50
|
+
|
|
51
|
+
self.__content = content
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def _content(self) -> bytes:
|
|
55
|
+
return self.__content
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def _format(self) -> typing.Literal['png']:
|
|
59
|
+
return c.PNG
|
|
60
|
+
|
|
61
|
+
def _repr_png_(self):
|
|
62
|
+
return self._content
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class HtmlDashboard(Dashboard):
|
|
66
|
+
"""
|
|
67
|
+
Instantiate a Dashboard from an HTML string
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(self, content: io.StringIO | str) -> None:
|
|
71
|
+
"""
|
|
72
|
+
Constructor for HtmlDashboard
|
|
73
|
+
|
|
74
|
+
Arguments:
|
|
75
|
+
content: The content of the dashboard as HTML string
|
|
76
|
+
"""
|
|
77
|
+
if isinstance(content, io.StringIO):
|
|
78
|
+
content = content.getvalue()
|
|
79
|
+
|
|
80
|
+
self.__content = content
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def _content(self) -> str:
|
|
84
|
+
return self.__content
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def _format(self) -> typing.Literal['html']:
|
|
88
|
+
return c.HTML
|
|
89
|
+
|
|
90
|
+
def _repr_html_(self):
|
|
91
|
+
return self._content
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
T = TypeVar('T', bound=Dashboard)
|
|
14
95
|
|
|
15
96
|
|
|
16
97
|
class DashboardFormat(Enum):
|
|
@@ -34,7 +115,7 @@ class DashboardDefinition:
|
|
|
34
115
|
config: DashboardConfig
|
|
35
116
|
|
|
36
117
|
@property
|
|
37
|
-
def dashboard_func(self) -> Callable[[DashboardArgs], Coroutine[Any, Any,
|
|
118
|
+
def dashboard_func(self) -> Callable[[DashboardArgs], Coroutine[Any, Any, Dashboard]]:
|
|
38
119
|
if not hasattr(self, '_dashboard_func'):
|
|
39
120
|
module = PyModule(self.filepath)
|
|
40
121
|
self._dashboard_func = module.get_func_or_class(c.MAIN_FUNC)
|
|
@@ -43,7 +124,7 @@ class DashboardDefinition:
|
|
|
43
124
|
def get_dashboard_format(self) -> str:
|
|
44
125
|
return self.config.format.value
|
|
45
126
|
|
|
46
|
-
async def get_dashboard(self, args: DashboardArgs, *, dashboard_type: Type[T] =
|
|
127
|
+
async def get_dashboard(self, args: DashboardArgs, *, dashboard_type: Type[T] = Dashboard) -> T:
|
|
47
128
|
try:
|
|
48
129
|
dashboard = await self.dashboard_func(args)
|
|
49
130
|
assert isinstance(dashboard, dashboard_type), f"Function does not return expected Dashboard type: {dashboard_type}"
|
squirrels/_exceptions.py
CHANGED
|
@@ -2,44 +2,16 @@ class InvalidInputError(Exception):
|
|
|
2
2
|
"""
|
|
3
3
|
Use this exception when the error is due to providing invalid inputs to the REST API
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
60-69: 409 conflict errors
|
|
10
|
-
70-99: Reserved for future use
|
|
11
|
-
100-199: 400 bad request errors related to authentication
|
|
12
|
-
200-299: 400 bad request errors related to data analytics
|
|
13
|
-
|
|
14
|
-
Error code definitions:
|
|
15
|
-
0 - Incorrect username or password
|
|
16
|
-
1 - Invalid authorization token
|
|
17
|
-
2 - Username not found for password change
|
|
18
|
-
3 - Incorrect password for password change
|
|
19
|
-
20 - Authorized user is forbidden to add or update users
|
|
20
|
-
21 - Authorized user is forbidden to delete users
|
|
21
|
-
22 - Cannot delete your own user
|
|
22
|
-
23 - Cannot delete the admin user
|
|
23
|
-
24 - Setting the admin user to non-admin is not permitted
|
|
24
|
-
25 - User does not have permission to access the dataset / dashboard
|
|
25
|
-
26 - User does not have permission to build the virtual data environment
|
|
26
|
-
27 - User does not have permission to query data models
|
|
27
|
-
40 - No token found for token_id
|
|
28
|
-
41 - No user found for username
|
|
29
|
-
60 - An existing build process is already running and a concurrent build is not allowed
|
|
30
|
-
61 - Model depends on static data models that cannot be found
|
|
31
|
-
100 - Missing required field 'username' or 'password' when adding a new user
|
|
32
|
-
101 - Username already exists when adding a new user
|
|
33
|
-
102 - Invalid user data when adding a new user
|
|
34
|
-
200 - Invalid value for dataset parameter
|
|
35
|
-
201 - Invalid query parameter provided
|
|
36
|
-
202 - Could not determine parent parameter for parameter refresh
|
|
37
|
-
203 - SQL query must be provided
|
|
38
|
-
204 - Failed to run provided SQL query
|
|
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)
|
|
39
9
|
"""
|
|
40
|
-
def __init__(self,
|
|
41
|
-
self.
|
|
42
|
-
|
|
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)
|
|
43
15
|
|
|
44
16
|
|
|
45
17
|
class ConfigurationError(Exception):
|
squirrels/_model_builder.py
CHANGED
|
@@ -68,7 +68,7 @@ class ModelBuilder:
|
|
|
68
68
|
# If the development copy is already in use, a concurrent build is not allowed
|
|
69
69
|
duckdb_dev_lock_path = u.Path(self._duckdb_venv_path + ".dev.lock")
|
|
70
70
|
if duckdb_dev_lock_path.exists():
|
|
71
|
-
raise InvalidInputError(
|
|
71
|
+
raise InvalidInputError(409, "Concurrent build not allowed", "An existing build process is already running and a concurrent build is not allowed")
|
|
72
72
|
duckdb_dev_lock_path.touch(exist_ok=False)
|
|
73
73
|
|
|
74
74
|
# Ensure the lock file is deleted even if an exception is raised
|
squirrels/_model_queries.py
CHANGED
|
@@ -3,7 +3,7 @@ from dataclasses import dataclass, field
|
|
|
3
3
|
from typing import Callable, Generic, TypeVar, Any
|
|
4
4
|
import polars as pl, pandas as pd
|
|
5
5
|
|
|
6
|
-
from ._arguments.
|
|
6
|
+
from ._arguments.run_time_args import BuildModelArgs
|
|
7
7
|
from ._model_configs import ModelConfig
|
|
8
8
|
|
|
9
9
|
|
squirrels/_models.py
CHANGED
|
@@ -7,9 +7,10 @@ from pathlib import Path
|
|
|
7
7
|
import asyncio, os, re, time, duckdb, sqlglot
|
|
8
8
|
import polars as pl, pandas as pd, networkx as nx
|
|
9
9
|
|
|
10
|
-
from . import _constants as c, _utils as u, _py_module as pm, _model_queries as mq, _model_configs as mc, _sources as src
|
|
10
|
+
from . import _constants as c, _utils as u, _py_module as pm, _model_queries as mq, _model_configs as mc, _sources as src
|
|
11
|
+
from ._schemas import response_models as rm
|
|
11
12
|
from ._exceptions import FileExecutionError, InvalidInputError
|
|
12
|
-
from ._arguments.
|
|
13
|
+
from ._arguments.run_time_args import ContextArgs, ModelArgs, BuildModelArgs
|
|
13
14
|
from ._auth import BaseUser
|
|
14
15
|
from ._connection_set import ConnectionsArgs, ConnectionSet, ConnectionProperties
|
|
15
16
|
from ._manifest import DatasetConfig
|
|
@@ -173,7 +174,7 @@ class StaticModel(DataModel):
|
|
|
173
174
|
try:
|
|
174
175
|
return self._load_duckdb_view_to_python_df(local_conn, use_venv=True)
|
|
175
176
|
except Exception as e:
|
|
176
|
-
raise InvalidInputError(
|
|
177
|
+
raise InvalidInputError(409, f'Dependent data model not found.', f'Model "{self.name}" depends on static data models that cannot be found. Trying building the virtual data environment first.')
|
|
177
178
|
finally:
|
|
178
179
|
local_conn.close()
|
|
179
180
|
|
|
@@ -531,7 +532,7 @@ class DbviewModel(QueryModel):
|
|
|
531
532
|
self.logger.info(f"Running dbview '{self.name}' on duckdb")
|
|
532
533
|
return local_conn.sql(query, params=placeholders).pl()
|
|
533
534
|
except duckdb.CatalogException as e:
|
|
534
|
-
raise InvalidInputError(
|
|
535
|
+
raise InvalidInputError(409, f'Dependent data model not found.', f'Model "{self.name}" depends on static data models that cannot be found. Trying building the virtual data environment first.')
|
|
535
536
|
except Exception as e:
|
|
536
537
|
raise RuntimeError(e)
|
|
537
538
|
finally:
|
|
@@ -657,10 +658,10 @@ class FederateModel(QueryModel):
|
|
|
657
658
|
try:
|
|
658
659
|
return local_conn.execute(create_query, existing_placeholders)
|
|
659
660
|
except duckdb.CatalogException as e:
|
|
660
|
-
raise InvalidInputError(
|
|
661
|
+
raise InvalidInputError(409, f'Dependent data model not found.', f'Model "{self.name}" depends on static data models that cannot be found. Trying building the virtual data environment first.')
|
|
661
662
|
except Exception as e:
|
|
662
663
|
if self.name == "__fake_target":
|
|
663
|
-
raise InvalidInputError(
|
|
664
|
+
raise InvalidInputError(400, "Invalid SQL query", f"Failed to run provided SQL query")
|
|
664
665
|
else:
|
|
665
666
|
raise FileExecutionError(f'Failed to run federate sql model "{self.name}"', e) from e
|
|
666
667
|
|
|
@@ -960,24 +961,24 @@ class DAG:
|
|
|
960
961
|
|
|
961
962
|
return G
|
|
962
963
|
|
|
963
|
-
def get_all_data_models(self) -> list[
|
|
964
|
+
def get_all_data_models(self) -> list[rm.DataModelItem]:
|
|
964
965
|
data_models = []
|
|
965
966
|
for model_name, model in self.models_dict.items():
|
|
966
967
|
is_queryable = model.is_queryable
|
|
967
|
-
data_model =
|
|
968
|
+
data_model = rm.DataModelItem(name=model_name, model_type=model.model_type.value, config=model.model_config, is_queryable=is_queryable)
|
|
968
969
|
data_models.append(data_model)
|
|
969
970
|
return data_models
|
|
970
971
|
|
|
971
|
-
def get_all_model_lineage(self) -> list[
|
|
972
|
+
def get_all_model_lineage(self) -> list[rm.LineageRelation]:
|
|
972
973
|
model_lineage = []
|
|
973
974
|
for model_name, model in self.models_dict.items():
|
|
974
975
|
if not isinstance(model, QueryModel):
|
|
975
976
|
continue
|
|
976
977
|
for dep_model_name in model.model_config.depends_on:
|
|
977
978
|
edge_type = "buildtime" if isinstance(model, BuildModel) else "runtime"
|
|
978
|
-
source_model =
|
|
979
|
-
target_model =
|
|
980
|
-
model_lineage.append(
|
|
979
|
+
source_model = rm.LineageNode(name=dep_model_name, type="model")
|
|
980
|
+
target_model = rm.LineageNode(name=model_name, type="model")
|
|
981
|
+
model_lineage.append(rm.LineageRelation(type=edge_type, source=source_model, target=target_model))
|
|
981
982
|
return model_lineage
|
|
982
983
|
|
|
983
984
|
|
|
@@ -10,6 +10,7 @@ SQRL_SECRET__ADMIN_PASSWORD="{{ random_admin_password }}"
|
|
|
10
10
|
# (default values are shown below)
|
|
11
11
|
SQRL_AUTH__DB_FILE_PATH="target/auth.sqlite"
|
|
12
12
|
SQRL_AUTH__TOKEN_EXPIRE_MINUTES="30"
|
|
13
|
+
SQRL_AUTH__ALLOWED_ORIGINS_FOR_COOKIES="https://squirrels-analytics.github.io"
|
|
13
14
|
|
|
14
15
|
SQRL_PARAMETERS__CACHE_SIZE="1024"
|
|
15
16
|
SQRL_PARAMETERS__CACHE_TTL_MINUTES="60"
|
|
@@ -10,6 +10,7 @@ SQRL_SECRET__ADMIN_PASSWORD=""
|
|
|
10
10
|
# (default values are shown below)
|
|
11
11
|
SQRL_AUTH__DB_FILE_PATH="target/auth.sqlite"
|
|
12
12
|
SQRL_AUTH__TOKEN_EXPIRE_MINUTES="30"
|
|
13
|
+
SQRL_AUTH__ALLOWED_ORIGINS_FOR_COOKIES="https://squirrels-analytics.github.io"
|
|
13
14
|
|
|
14
15
|
SQRL_PARAMETERS__CACHE_SIZE="1024"
|
|
15
16
|
SQRL_PARAMETERS__CACHE_TTL_MINUTES="60"
|
|
@@ -1,98 +1,106 @@
|
|
|
1
|
-
from squirrels import arguments as args,
|
|
1
|
+
from squirrels import arguments as args, parameters as p, parameter_options as po, data_sources as ds
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
> p.SingleSelectParameter.CreateWithOptions(...)
|
|
11
|
-
|
|
12
|
-
The parameter classes available are:
|
|
13
|
-
- SingleSelectParameter, MultiSelectParameter, DateParameter, DateRangeParameter, NumberParameter, NumberRangeParameter, TextParameter
|
|
14
|
-
|
|
15
|
-
The factory methods available are:
|
|
16
|
-
- CreateSimple, CreateWithOptions, CreateFromSource
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
## Example of creating SingleSelectParameter and specifying each option by code
|
|
20
|
-
user_attribute = "role"
|
|
21
|
-
group_by_options = [
|
|
4
|
+
## Example of creating SingleSelectParameter and specifying each option by code
|
|
5
|
+
@p.SingleSelectParameter.create_with_options(
|
|
6
|
+
"group_by", "Group By", description="Dimension(s) to aggregate by", user_attribute="role"
|
|
7
|
+
)
|
|
8
|
+
def group_by_options():
|
|
9
|
+
return [
|
|
22
10
|
po.SelectParameterOption(
|
|
23
11
|
"trans", "Transaction",
|
|
24
|
-
columns=["id",
|
|
25
|
-
aliases=["_id",
|
|
12
|
+
columns=["id","date","category","subcategory","description"],
|
|
13
|
+
aliases=["_id","date","category","subcategory","description"], # any alias starting with "_" will not be selected - see context.py for implementation
|
|
26
14
|
user_groups=["manager"]
|
|
27
15
|
),
|
|
28
|
-
po.SelectParameterOption("day", "Day",
|
|
29
|
-
po.SelectParameterOption("month", "Month",
|
|
30
|
-
po.SelectParameterOption("cat", "Category",
|
|
31
|
-
po.SelectParameterOption("subcat", "Subcategory", columns=["category",
|
|
16
|
+
po.SelectParameterOption("day" , "Day" , columns=["date"], aliases=["day"] , user_groups=["manager","employee"]),
|
|
17
|
+
po.SelectParameterOption("month" , "Month" , columns=["month"] , user_groups=["manager","employee"]),
|
|
18
|
+
po.SelectParameterOption("cat" , "Category" , columns=["category"] , user_groups=["manager","employee"]),
|
|
19
|
+
po.SelectParameterOption("subcat" , "Subcategory" , columns=["category","subcategory"] , user_groups=["manager","employee"]),
|
|
32
20
|
]
|
|
33
|
-
p.SingleSelectParameter.CreateWithOptions(
|
|
34
|
-
"group_by", "Group By", group_by_options, description="Dimension(s) to aggregate by", user_attribute=user_attribute
|
|
35
|
-
)
|
|
36
21
|
|
|
37
|
-
## Example of creating NumberParameter with options
|
|
38
|
-
parent = "group_by"
|
|
39
|
-
limit_options = [po.NumberParameterOption(0, 1000, increment=10, default_value=1000, parent_option_ids="trans")]
|
|
40
|
-
p.NumberParameter.CreateWithOptions(
|
|
41
|
-
"limit", "Max Number of Rows", limit_options, parent_name=parent, description="Maximum number of rows to return"
|
|
42
|
-
)
|
|
43
22
|
|
|
44
|
-
|
|
45
|
-
|
|
23
|
+
## Example of creating NumberParameter with options
|
|
24
|
+
@p.NumberParameter.create_with_options(
|
|
25
|
+
"limit", "Max Number of Rows", description="Maximum number of rows to return", parent_name="group_by"
|
|
26
|
+
)
|
|
27
|
+
def limit_options():
|
|
28
|
+
return [po.NumberParameterOption(0, 1000, increment=10, default_value=1000, parent_option_ids="trans")]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## Example of creating DateParameter
|
|
32
|
+
@p.DateParameter.create_from_source(
|
|
33
|
+
"start_date", "Start Date", description="Start date to filter transactions by"
|
|
34
|
+
)
|
|
35
|
+
def start_date_source():
|
|
36
|
+
return ds.DateDataSource(
|
|
46
37
|
"SELECT min(date) AS min_date, max(date) AS max_date FROM expenses",
|
|
47
38
|
default_date_col="min_date", min_date_col="min_date", max_date_col="max_date"
|
|
48
39
|
)
|
|
49
|
-
p.DateParameter.CreateFromSource(
|
|
50
|
-
"start_date", "Start Date", start_date_source, description="Start date to filter transactions by"
|
|
51
|
-
)
|
|
52
40
|
|
|
53
|
-
## Example of creating DateParameter from list of DateParameterOption's
|
|
54
|
-
end_date_option = [po.DateParameterOption("2024-12-31", min_date="2024-01-01", max_date="2024-12-31")]
|
|
55
|
-
p.DateParameter.CreateWithOptions(
|
|
56
|
-
"end_date", "End Date", end_date_option, description="End date to filter transactions by"
|
|
57
|
-
)
|
|
58
41
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
42
|
+
## Example of creating DateParameter from list of DateParameterOption's
|
|
43
|
+
@p.DateParameter.create_with_options(
|
|
44
|
+
"end_date", "End Date", description="End date to filter transactions by"
|
|
45
|
+
)
|
|
46
|
+
def end_date_options():
|
|
47
|
+
return [po.DateParameterOption("2024-12-31", min_date="2024-01-01", max_date="2024-12-31")]
|
|
64
48
|
|
|
65
|
-
## Example of creating MultiSelectParameter from lookup query/table
|
|
66
|
-
category_ds = ds.SelectDataSource("seed_categories", "category_id", "category", from_seeds=True)
|
|
67
|
-
p.MultiSelectParameter.CreateFromSource(
|
|
68
|
-
"category", "Category Filter", category_ds, description="The expense categories to filter transactions by"
|
|
69
|
-
)
|
|
70
49
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
50
|
+
## Example of creating DateRangeParameter
|
|
51
|
+
@p.DateRangeParameter.create_simple(
|
|
52
|
+
"date_range", "Date Range", "2024-01-01", "2024-12-31", min_date="2024-01-01", max_date="2024-12-31",
|
|
53
|
+
description="Date range to filter transactions by"
|
|
54
|
+
)
|
|
55
|
+
def date_range_options():
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
## Example of creating MultiSelectParameter from lookup query/table
|
|
60
|
+
@p.MultiSelectParameter.create_from_source(
|
|
61
|
+
"category", "Category Filter",
|
|
62
|
+
description="The expense categories to filter transactions by"
|
|
63
|
+
)
|
|
64
|
+
def category_source():
|
|
65
|
+
return ds.SelectDataSource("seed_categories", "category_id", "category", from_seeds=True)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## Example of creating MultiSelectParameter with parent from lookup query/table
|
|
69
|
+
@p.MultiSelectParameter.create_from_source(
|
|
70
|
+
"subcategory", "Subcategory Filter", parent_name="category",
|
|
71
|
+
description="The expense subcategories to filter transactions by (available options are based on selected value(s) of 'Category Filter')"
|
|
72
|
+
)
|
|
73
|
+
def subcategory_source():
|
|
74
|
+
return ds.SelectDataSource(
|
|
74
75
|
"seed_subcategories", "subcategory_id", "subcategory", from_seeds=True, parent_id_col="category_id"
|
|
75
76
|
)
|
|
76
|
-
p.MultiSelectParameter.CreateFromSource(
|
|
77
|
-
"subcategory", "Subcategory Filter", subcategory_ds, parent_name=parent_name,
|
|
78
|
-
description="The expense subcategories to filter transactions by (available options are based on selected value(s) of 'Category Filter')"
|
|
79
|
-
)
|
|
80
77
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
78
|
+
|
|
79
|
+
## Example of creating NumberParameter
|
|
80
|
+
@p.NumberParameter.create_simple(
|
|
81
|
+
"min_filter", "Amounts Greater Than", min_value=0, max_value=300, increment=10,
|
|
82
|
+
description="Number to filter on transactions with an amount greater than this value"
|
|
83
|
+
)
|
|
84
|
+
def min_filter_options():
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
## Example of creating NumberParameter from lookup query/table
|
|
89
|
+
@p.NumberParameter.create_from_source(
|
|
90
|
+
"max_filter", "Amounts Less Than",
|
|
91
|
+
description="Number to filter on transactions with an amount less than this value"
|
|
92
|
+
)
|
|
93
|
+
def max_filter_source():
|
|
88
94
|
query = "SELECT 0 as min_value, 300 as max_value, 10 as increment"
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
"max_filter", "Amounts Less Than", max_amount_ds, description="Number to filter on transactions with an amount less than this value"
|
|
95
|
+
return ds.NumberDataSource(
|
|
96
|
+
query, "min_value", "max_value", increment_col="increment", default_value_col="max_value"
|
|
92
97
|
)
|
|
93
98
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
|
|
100
|
+
## Example of creating NumberRangeParameter
|
|
101
|
+
@p.NumberRangeParameter.create_simple(
|
|
102
|
+
"between_filter", "Amounts Between", min_value=0, max_value=300, default_lower_value=0, default_upper_value=300,
|
|
103
|
+
description="Number range to filter on transactions with an amount within this range"
|
|
104
|
+
)
|
|
105
|
+
def between_filter_options():
|
|
106
|
+
pass
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from typing import Literal
|
|
2
|
-
from squirrels import
|
|
2
|
+
from squirrels import auth, arguments as args
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
class User(
|
|
5
|
+
class User(auth.BaseUser):
|
|
6
6
|
"""
|
|
7
7
|
Extend the BaseUser class with custom attributes. The attributes defined here will be added as columns to the users table.
|
|
8
8
|
- Only the following types are supported: [str, int, float, bool, typing.Literal]
|
|
@@ -21,3 +21,31 @@ class User(t.BaseUser):
|
|
|
21
21
|
However, you can choose to drop columns by adding them to this list.
|
|
22
22
|
"""
|
|
23
23
|
return []
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# @auth.provider(name="google", label="Google", icon="https://www.google.com/favicon.ico")
|
|
27
|
+
def google_auth_provider(sqrl: args.AuthProviderArgs) -> auth.ProviderConfigs:
|
|
28
|
+
"""
|
|
29
|
+
Provider configs for authenticating a user using Google credentials.
|
|
30
|
+
|
|
31
|
+
See the following page for setting up the CLIENT_ID and CLIENT_SECRET for Google specifically:
|
|
32
|
+
https://support.google.com/googleapi/answer/6158849?hl=en
|
|
33
|
+
"""
|
|
34
|
+
def get_sqrl_user(claims: dict) -> User:
|
|
35
|
+
return User(
|
|
36
|
+
username=claims["email"],
|
|
37
|
+
is_admin=False,
|
|
38
|
+
role="employee"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# TODO: Add GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET to the .env file
|
|
42
|
+
# Then, uncomment the @auth.provider decorator above and set the client_id and client_secret below
|
|
43
|
+
provider_configs = auth.ProviderConfigs(
|
|
44
|
+
client_id="", # sqrl.env_vars["GOOGLE_CLIENT_ID"],
|
|
45
|
+
client_secret="", # sqrl.env_vars["GOOGLE_CLIENT_SECRET"],
|
|
46
|
+
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
|
|
47
|
+
client_kwargs={"scope": "openid email profile"},
|
|
48
|
+
get_user=get_sqrl_user
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return provider_configs
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.dataset-results {
|
|
3
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
4
|
+
background: white;
|
|
5
|
+
border-radius: 8px;
|
|
6
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
max-width: 100%;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.dataset-results .table-container {
|
|
12
|
+
overflow: auto;
|
|
13
|
+
max-height: 400px;
|
|
14
|
+
background: white;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.dataset-results .data-table {
|
|
18
|
+
width: 100%;
|
|
19
|
+
border-collapse: collapse;
|
|
20
|
+
font-size: 13px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.dataset-results .data-table thead {
|
|
24
|
+
background: #f8f9fa;
|
|
25
|
+
position: sticky;
|
|
26
|
+
top: 0;
|
|
27
|
+
z-index: 10;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.dataset-results .data-table th {
|
|
31
|
+
padding: 8px 12px;
|
|
32
|
+
text-align: left;
|
|
33
|
+
font-weight: 600;
|
|
34
|
+
color: #374151;
|
|
35
|
+
border-bottom: 2px solid #e1e5e9;
|
|
36
|
+
white-space: nowrap;
|
|
37
|
+
font-size: 12px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.dataset-results .data-table th .column-type {
|
|
41
|
+
display: block;
|
|
42
|
+
font-size: 10px;
|
|
43
|
+
color: #6b7280;
|
|
44
|
+
font-weight: 400;
|
|
45
|
+
margin-top: 1px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.dataset-results .data-table td {
|
|
49
|
+
padding: 8px 12px;
|
|
50
|
+
border-bottom: 1px solid #f3f4f6;
|
|
51
|
+
color: #374151;
|
|
52
|
+
vertical-align: top;
|
|
53
|
+
font-size: 12px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.dataset-results .data-table tbody tr:hover {
|
|
57
|
+
background: #f8f9fa;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.dataset-results .data-table tbody tr:nth-child(even) {
|
|
61
|
+
background: #fafbfc;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.dataset-results .data-table tbody tr:nth-child(even):hover {
|
|
65
|
+
background: #f1f3f4;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.dataset-results .empty-state {
|
|
69
|
+
text-align: center;
|
|
70
|
+
padding: 40px 20px;
|
|
71
|
+
color: #6b7280;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.dataset-results .empty-state h3 {
|
|
75
|
+
font-size: 16px;
|
|
76
|
+
margin-bottom: 6px;
|
|
77
|
+
color: #374151;
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
80
|
+
|
|
81
|
+
<div class="dataset-results">
|
|
82
|
+
{% if schema.fields and data %}
|
|
83
|
+
<div class="table-container">
|
|
84
|
+
<table class="data-table">
|
|
85
|
+
<thead>
|
|
86
|
+
<tr>
|
|
87
|
+
{% for field in schema.fields %}
|
|
88
|
+
<th>
|
|
89
|
+
{{ field.name }}
|
|
90
|
+
<span class="column-type">{{ field.type | upper }}</span>
|
|
91
|
+
</th>
|
|
92
|
+
{% endfor %}
|
|
93
|
+
</tr>
|
|
94
|
+
</thead>
|
|
95
|
+
<tbody>
|
|
96
|
+
{% for row in data %}
|
|
97
|
+
<tr>
|
|
98
|
+
{% for cell in row %}
|
|
99
|
+
<td>{{ cell if cell is not none else '' }}</td>
|
|
100
|
+
{% endfor %}
|
|
101
|
+
</tr>
|
|
102
|
+
{% endfor %}
|
|
103
|
+
</tbody>
|
|
104
|
+
</table>
|
|
105
|
+
</div>
|
|
106
|
+
{% else %}
|
|
107
|
+
<div class="empty-state">
|
|
108
|
+
<h3>No Data Available</h3>
|
|
109
|
+
<p>This dataset returned no results.</p>
|
|
110
|
+
</div>
|
|
111
|
+
{% endif %}
|
|
112
|
+
</div>
|