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.

Files changed (47) hide show
  1. squirrels/__init__.py +2 -0
  2. squirrels/_api_routes/__init__.py +5 -0
  3. squirrels/_api_routes/auth.py +262 -0
  4. squirrels/_api_routes/base.py +154 -0
  5. squirrels/_api_routes/dashboards.py +142 -0
  6. squirrels/_api_routes/data_management.py +103 -0
  7. squirrels/_api_routes/datasets.py +242 -0
  8. squirrels/_api_routes/oauth2.py +300 -0
  9. squirrels/_api_routes/project.py +214 -0
  10. squirrels/_api_server.py +142 -745
  11. squirrels/_arguments/__init__.py +0 -0
  12. squirrels/_arguments/{_init_time_args.py → init_time_args.py} +5 -0
  13. squirrels/_arguments/{_run_time_args.py → run_time_args.py} +1 -1
  14. squirrels/_auth.py +645 -92
  15. squirrels/_connection_set.py +1 -1
  16. squirrels/_constants.py +6 -0
  17. squirrels/{_dashboards_io.py → _dashboards.py} +87 -6
  18. squirrels/_exceptions.py +9 -37
  19. squirrels/_model_builder.py +1 -1
  20. squirrels/_model_queries.py +1 -1
  21. squirrels/_models.py +13 -12
  22. squirrels/_package_data/base_project/.env +1 -0
  23. squirrels/_package_data/base_project/.env.example +1 -0
  24. squirrels/_package_data/base_project/pyconfigs/parameters.py +84 -76
  25. squirrels/_package_data/base_project/pyconfigs/user.py +30 -2
  26. squirrels/_package_data/templates/dataset_results.html +112 -0
  27. squirrels/_package_data/templates/oauth_login.html +271 -0
  28. squirrels/_parameter_configs.py +1 -1
  29. squirrels/_parameter_sets.py +31 -21
  30. squirrels/_parameters.py +521 -123
  31. squirrels/_project.py +43 -24
  32. squirrels/_py_module.py +3 -2
  33. squirrels/_schemas/__init__.py +0 -0
  34. squirrels/_schemas/auth_models.py +144 -0
  35. squirrels/_schemas/query_param_models.py +67 -0
  36. squirrels/{_api_response_models.py → _schemas/response_models.py} +12 -8
  37. squirrels/_utils.py +34 -2
  38. squirrels/arguments.py +2 -2
  39. squirrels/auth.py +1 -0
  40. squirrels/dashboards.py +1 -1
  41. squirrels/types.py +3 -3
  42. {squirrels-0.5.0b3.dist-info → squirrels-0.5.0b4.dist-info}/METADATA +4 -1
  43. {squirrels-0.5.0b3.dist-info → squirrels-0.5.0b4.dist-info}/RECORD +46 -32
  44. squirrels/_dashboard_types.py +0 -82
  45. {squirrels-0.5.0b3.dist-info → squirrels-0.5.0b4.dist-info}/WHEEL +0 -0
  46. {squirrels-0.5.0b3.dist-info → squirrels-0.5.0b4.dist-info}/entry_points.txt +0 -0
  47. {squirrels-0.5.0b3.dist-info → squirrels-0.5.0b4.dist-info}/licenses/LICENSE +0 -0
@@ -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._init_time_args import ConnectionsArgs
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 os, time
5
+ import matplotlib.figure as figure
6
+ import os, time, io, abc, typing
6
7
 
7
- from ._arguments._run_time_args import DashboardArgs
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, _dashboard_types as d, _utils as u
12
+ from . import _constants as c, _utils as u
12
13
 
13
- T = TypeVar('T', bound=d.Dashboard)
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, d.Dashboard]]:
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] = d.Dashboard) -> 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
- Specific error code ranges are reserved for specific categories of errors.
6
- 0-19: 401 unauthorized errors
7
- 20-39: 403 forbidden errors
8
- 40-59: 404 not found errors
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, error_code: int, message: str, *args) -> None:
41
- self.error_code = error_code
42
- super().__init__(message, *args)
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):
@@ -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(60, "An existing build process is already running and a concurrent build is not allowed")
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
@@ -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._run_time_args import BuildModelArgs
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, _api_response_models as arm
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._run_time_args import ContextArgs, ModelArgs, BuildModelArgs
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(61, f'Model "{self.name}" depends on static data models that cannot be found.')
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(61, f'Model "{self.name}" depends on static data models that cannot be found.')
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(61, f'Model "{self.name}" depends on static data models that cannot be found.')
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(204, f"Failed to run provided SQL query")
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[arm.DataModelItem]:
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 = arm.DataModelItem(name=model_name, model_type=model.model_type.value, config=model.model_config, is_queryable=is_queryable)
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[arm.LineageRelation]:
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 = arm.LineageNode(name=dep_model_name, type="model")
979
- target_model = arm.LineageNode(name=model_name, type="model")
980
- model_lineage.append(arm.LineageRelation(type=edge_type, source=source_model, target=target_model))
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, data_sources as ds, parameter_options as po, parameters as p
1
+ from squirrels import arguments as args, parameters as p, parameter_options as po, data_sources as ds
2
2
 
3
3
 
4
- def main(sqrl: args.ParametersArgs) -> None:
5
- """
6
- Create all widget parameters in this file. If two or more datasets use a different set of parameters, define them all
7
- here, and specify the subset of parameters used for each dataset in the "squirrels.yml" file.
8
-
9
- Parameters are created by a factory method associated to the parameter class. For example, "CreateWithOptions" is the factory method used here:
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", "date", "category", "subcategory", "description"],
25
- aliases=["_id", "date", "category", "subcategory", "description"], # any alias starting with "_" will not be selected - see context.py for implementation
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", columns=["date"], aliases=["day"], user_groups=["manager", "employee"]),
29
- po.SelectParameterOption("month", "Month", columns=["month"], user_groups=["manager", "employee"]),
30
- po.SelectParameterOption("cat", "Category", columns=["category"], user_groups=["manager", "employee"]),
31
- po.SelectParameterOption("subcat", "Subcategory", columns=["category", "subcategory"], user_groups=["manager", "employee"]),
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
- ## Example of creating DateParameter
45
- start_date_source = ds.DateDataSource(
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
- ## Example of creating DateRangeParameter
60
- p.DateRangeParameter.CreateSimple(
61
- "date_range", "Date Range", "2024-01-01", "2024-12-31", min_date="2024-01-01", max_date="2024-12-31",
62
- description="Date range to filter transactions by"
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
- ## Example of creating MultiSelectParameter with parent from lookup query/table
72
- parent_name = "category"
73
- subcategory_ds = ds.SelectDataSource(
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
- ## Example of creating NumberParameter
82
- p.NumberParameter.CreateSimple(
83
- "min_filter", "Amounts Greater Than", min_value=0, max_value=300, increment=10,
84
- description="Number to filter on transactions with an amount greater than this value"
85
- )
86
-
87
- ## Example of creating NumberParameter from lookup query/table
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
- max_amount_ds = ds.NumberDataSource(query, "min_value", "max_value", increment_col="increment", default_value_col="max_value")
90
- p.NumberParameter.CreateFromSource(
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
- ## Example of creating NumberRangeParameter
95
- p.NumberRangeParameter.CreateSimple(
96
- "between_filter", "Amounts Between", 0, 300, default_lower_value=0, default_upper_value=300,
97
- description="Number range to filter on transactions with an amount within this range"
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 types as t
2
+ from squirrels import auth, arguments as args
3
3
 
4
4
 
5
- class User(t.BaseUser):
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>