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
@@ -1,5 +1,6 @@
1
1
  from argparse import ArgumentParser, _SubParsersAction
2
- import sys, asyncio, traceback, io, os, subprocess
2
+ from pathlib import Path
3
+ import sys, asyncio, traceback, io, subprocess
3
4
 
4
5
  sys.path.append('.')
5
6
 
@@ -9,16 +10,31 @@ from ._initializer import Initializer
9
10
  from ._package_loader import PackageLoaderIO
10
11
  from ._project import SquirrelsProject
11
12
  from . import _constants as c, _utils as u
13
+ from ._compile_prompts import prompt_compile_options
12
14
 
13
15
 
14
- def _run_duckdb_cli(project: SquirrelsProject):
15
- _, target_init_path = u._read_duckdb_init_sql()
16
- init_args = f"-init {target_init_path}" if target_init_path else ""
16
+ def _run_duckdb_cli(project: SquirrelsProject, ui: bool):
17
+ init_sql = u._read_duckdb_init_sql(datalake_db_path=project._datalake_db_path)
18
+
19
+ target_init_path = None
20
+ if init_sql:
21
+ target_init_path = Path(c.TARGET_FOLDER, c.DUCKDB_INIT_FILE)
22
+ target_init_path.parent.mkdir(parents=True, exist_ok=True)
23
+ target_init_path.write_text(init_sql)
24
+
25
+ init_args = ["-init", str(target_init_path)] if target_init_path else []
17
26
  command = ['duckdb']
27
+ if ui:
28
+ command.append('-ui')
18
29
  if init_args:
19
- command.extend(init_args.split())
20
- command.extend(['-readonly', project._duckdb_venv_path])
21
- print(f'Running command: {" ".join(command)}')
30
+ command.extend(init_args)
31
+
32
+ print("Starting DuckDB CLI with command:")
33
+ print(f"$ {' '.join(command)}")
34
+ print()
35
+ print("To exit the DuckDB CLI, enter '.exit'")
36
+ print()
37
+
22
38
  try:
23
39
  subprocess.run(command, check=True)
24
40
  except FileNotFoundError:
@@ -31,38 +47,39 @@ def main():
31
47
  """
32
48
  Main entry point for the squirrels command line utilities.
33
49
  """
34
- def with_help(parser: ArgumentParser):
35
- parser.add_argument('-h', '--help', action="help", help="Show this help message and exit")
36
- return parser
37
-
38
- parser = with_help(ArgumentParser(description="Command line utilities from the squirrels python package", add_help=False))
50
+ # Create a parent parser with common logging options
51
+ parent_parser = ArgumentParser(add_help=False)
52
+ parent_parser.add_argument('-h', '--help', action="help", help="Show this help message and exit")
53
+ parent_parser.add_argument('--log-level', type=str, choices=["DEBUG", "INFO", "WARNING"], help='Level of logging to use. Default is from SQRL_LOGGING__LOG_LEVEL environment variable or INFO.')
54
+ parent_parser.add_argument('--log-format', type=str, choices=["text", "json"], help='Format of the log records. Default is from SQRL_LOGGING__LOG_FORMAT environment variable or text.')
55
+ parent_parser.add_argument('--log-to-file', action='store_true', help='Enable logging to file(s) in the "logs/" folder with rotation and retention policies.')
56
+
57
+ parser = ArgumentParser(description="Command line utilities from the squirrels python package", add_help=False, parents=[parent_parser])
39
58
  parser.add_argument('-V', '--version', action='store_true', help='Show the version and exit')
40
- parser.add_argument('--log-level', type=str, choices=["DEBUG", "INFO", "WARNING"], default="INFO", help='Level of logging to use')
41
- parser.add_argument('--log-format', type=str, choices=["text", "json"], default="text", help='Format of the log records')
42
- parser.add_argument('--log-file', type=str, default=c.LOGS_FILE, help=f'Name of log file to write to in the "logs/" folder. Default is {c.LOGS_FILE}. If name is empty, then file logging is disabled')
43
59
  subparsers = parser.add_subparsers(title='commands', dest='command')
44
60
 
45
61
  def add_subparser(subparsers: _SubParsersAction, cmd: str, help_text: str):
46
- subparser = with_help(subparsers.add_parser(cmd, description=help_text, help=help_text, add_help=False))
62
+ subparser: ArgumentParser = subparsers.add_parser(cmd, description=help_text, help=help_text, add_help=False, parents=[parent_parser])
47
63
  return subparser
48
64
 
49
65
  init_parser = add_subparser(subparsers, c.INIT_CMD, 'Create a new squirrels project')
50
66
 
51
- init_parser.add_argument('name', nargs='?', type=str, help='The name of the project')
52
- init_parser.add_argument('-o', '--overwrite', action='store_true', help="Overwrite files that already exist")
53
- init_parser.add_argument('--core', action='store_true', help='Include all core files')
67
+ init_parser.add_argument('name', nargs='?', type=str, help='The name of the project folder to create. Ignored if --curr-dir is used')
68
+ init_parser.add_argument('--curr-dir', action='store_true', help='Create the project in the current directory')
69
+ init_parser.add_argument('--use-defaults', action='store_true', help='Use default values for unspecified options (except project folder name) instead of prompting for input')
54
70
  init_parser.add_argument('--connections', type=str, choices=c.CONF_FORMAT_CHOICES, help=f'Configure database connections as yaml (default) or python')
55
71
  init_parser.add_argument('--parameters', type=str, choices=c.CONF_FORMAT_CHOICES, help=f'Configure parameters as python (default) or yaml')
56
72
  init_parser.add_argument('--build', type=str, choices=c.FILE_TYPE_CHOICES, help='Create build model as sql (default) or python file')
57
73
  init_parser.add_argument('--federate', type=str, choices=c.FILE_TYPE_CHOICES, help='Create federated model as sql (default) or python file')
58
- init_parser.add_argument('--dashboard', action='store_true', help=f'Include a sample dashboard file')
74
+ init_parser.add_argument('--dashboard', type=str, choices=['y', 'n'], help=f'Include (y) or exclude (n, default) a sample dashboard file')
75
+ init_parser.add_argument('--admin-password', type=str, help='The password for the admin user. If --use-defaults is used, then a random password is generated')
59
76
 
60
77
  def with_file_format_options(parser: ArgumentParser):
61
78
  help_text = "Create model as sql (default) or python file"
62
79
  parser.add_argument('--format', type=str, choices=c.FILE_TYPE_CHOICES, default=c.SQL_FILE_TYPE, help=help_text)
63
80
  return parser
64
81
 
65
- get_file_help_text = "Get a sample file for the squirrels project. If the file name already exists, it will be prefixed with a timestamp."
82
+ get_file_help_text = "Get a sample file for the squirrels project. If the file name already exists, it will be suffixed with a timestamp."
66
83
  get_file_parser = add_subparser(subparsers, c.GET_FILE_CMD, get_file_help_text)
67
84
  get_file_subparsers = get_file_parser.add_subparsers(title='file_name', dest='file_name')
68
85
  add_subparser(get_file_subparsers, c.DOTENV_FILE, f'Get sample {c.DOTENV_FILE} and {c.DOTENV_FILE}.example files')
@@ -89,24 +106,26 @@ def main():
89
106
  deps_parser = add_subparser(subparsers, c.DEPS_CMD, f'Load all packages specified in {c.MANIFEST_FILE} (from git)')
90
107
 
91
108
  compile_parser = add_subparser(subparsers, c.COMPILE_CMD, 'Create rendered SQL files in the folder "./target/compile"')
92
- compile_dataset_group = compile_parser.add_mutually_exclusive_group(required=True)
93
- compile_dataset_group.add_argument('-d', '--dataset', type=str, help="Select dataset to use for dataset traits. Is required, unless using --all-datasets")
94
- compile_dataset_group.add_argument('-D', '--all-datasets', action="store_true", help="Compile models for all datasets. Only required if --dataset is not specified")
109
+ compile_parser.add_argument('-y', '--yes', action='store_true', help='Disable prompts and assume the default for all settings not overridden by CLI options')
110
+ compile_parser.add_argument('-c', '--clear', action='store_true', help='Clear the "target/compile/" folder before compiling')
111
+ compile_scope_group = compile_parser.add_mutually_exclusive_group(required=False)
112
+ compile_scope_group.add_argument('--buildtime-only', action='store_true', help='Compile only buildtime models')
113
+ compile_scope_group.add_argument('--runtime-only', action='store_true', help='Compile only runtime models')
95
114
  compile_test_set_group = compile_parser.add_mutually_exclusive_group(required=False)
96
- compile_test_set_group.add_argument('-t', '--test-set', type=str, help="The selection test set to use. If not specified, default selections are used, unless using --all-test-sets")
97
- compile_test_set_group.add_argument('-T', '--all-test-sets', action="store_true", help="Compile models for all selection test sets")
98
- compile_parser.add_argument('-s', '--select', type=str, help="Select single model to compile. If not specified, all models for the dataset are compiled. Ignored if using --all-datasets")
99
- compile_parser.add_argument('-r', '--runquery', action='store_true', help='Runs all target models, and produce the results as csv files')
100
-
101
- build_parser = add_subparser(subparsers, c.BUILD_CMD, 'Build the virtual data environment (with duckdb) for the project')
115
+ compile_test_set_group.add_argument('-t', '--test-set', type=str, help="The selection test set to use. If not specified, default selections are used (unless using --all-test-sets). Not applicable for buildtime models")
116
+ compile_test_set_group.add_argument('-T', '--all-test-sets', action="store_true", help="Compile models for all selection test sets. Not applicable for buildtime models")
117
+ compile_parser.add_argument('-s', '--select', type=str, help="Select single model to compile. If not specified, all models are compiled")
118
+ compile_parser.add_argument('-r', '--runquery', action='store_true', help='Run runtime models and write CSV outputs too. Does not apply to buildtime models')
119
+
120
+ build_parser = add_subparser(subparsers, c.BUILD_CMD, 'Build the Virtual Data Lake (VDL) for the project')
102
121
  build_parser.add_argument('-f', '--full-refresh', action='store_true', help='Drop all tables before building')
103
122
  build_parser.add_argument('-s', '--select', type=str, help="Select one static model to build. If not specified, all models are built")
104
- build_parser.add_argument('--stage', type=str, help='If the venv file is in use, stage the duckdb file to replace the venv later')
105
123
 
106
124
  duckdb_parser = add_subparser(subparsers, c.DUCKDB_CMD, 'Run the duckdb command line tool')
125
+ duckdb_parser.add_argument('--ui', action='store_true', help='Run the duckdb local UI')
107
126
 
108
127
  run_parser = add_subparser(subparsers, c.RUN_CMD, 'Run the API server')
109
- run_parser.add_argument('--build', action='store_true', help='Build the virtual data environment (with duckdb) first before running the API server')
128
+ run_parser.add_argument('--build', action='store_true', help='Build the VDL first (without full refresh) before running the API server')
110
129
  run_parser.add_argument('--no-cache', action='store_true', help='Do not cache any api results')
111
130
  run_parser.add_argument('--host', type=str, default='127.0.0.1', help="The host to run on")
112
131
  run_parser.add_argument('--port', type=int, default=4465, help="The port to run on")
@@ -116,22 +135,22 @@ def main():
116
135
  if args.version:
117
136
  print(__version__)
118
137
  elif args.command == c.INIT_CMD:
119
- Initializer(project_name=args.name, overwrite=args.overwrite).init_project(args)
138
+ Initializer(project_name=args.name, use_curr_dir=args.curr_dir).init_project(args)
120
139
  elif args.command == c.GET_FILE_CMD:
121
140
  Initializer().get_file(args)
122
141
  elif args.command is None:
123
142
  print(f'Command is missing. Enter "squirrels -h" for help.')
124
143
  else:
125
- project = SquirrelsProject(log_level=args.log_level, log_format=args.log_format, log_file=args.log_file)
144
+ project = SquirrelsProject(load_dotenv_globally=True, log_level=args.log_level, log_format=args.log_format, log_to_file=args.log_to_file)
126
145
  try:
127
146
  if args.command == c.DEPS_CMD:
128
147
  PackageLoaderIO.load_packages(project._logger, project._manifest_cfg, reload=True)
129
148
  elif args.command == c.BUILD_CMD:
130
- task = project.build(full_refresh=args.full_refresh, select=args.select, stage_file=args.stage)
149
+ task = project.build(full_refresh=args.full_refresh, select=args.select)
131
150
  asyncio.run(task)
132
151
  print()
133
152
  elif args.command == c.DUCKDB_CMD:
134
- _run_duckdb_cli(project)
153
+ _run_duckdb_cli(project, args.ui)
135
154
  elif args.command == c.RUN_CMD:
136
155
  if args.build:
137
156
  task = project.build(full_refresh=True)
@@ -139,11 +158,32 @@ def main():
139
158
  server = ApiServer(args.no_cache, project)
140
159
  server.run(args)
141
160
  elif args.command == c.COMPILE_CMD:
142
- task = project.compile(
143
- dataset=args.dataset, do_all_datasets=args.all_datasets, selected_model=args.select, test_set=args.test_set,
144
- do_all_test_sets=args.all_test_sets, runquery=args.runquery
145
- )
146
- asyncio.run(task)
161
+ # Derive final options with optional interactive prompts (unless --yes is provided)
162
+ buildtime_only = args.buildtime_only
163
+ runtime_only = args.runtime_only
164
+ test_set = args.test_set
165
+ do_all_test_sets = args.all_test_sets
166
+ selected_model = args.select
167
+
168
+ try:
169
+ if not args.yes:
170
+ buildtime_only, runtime_only, test_set, do_all_test_sets, selected_model = prompt_compile_options(
171
+ project,
172
+ buildtime_only=buildtime_only,
173
+ runtime_only=runtime_only,
174
+ test_set=test_set,
175
+ do_all_test_sets=do_all_test_sets,
176
+ selected_model=selected_model,
177
+ )
178
+
179
+ task = project.compile(
180
+ selected_model=selected_model, test_set=test_set, do_all_test_sets=do_all_test_sets, runquery=args.runquery,
181
+ clear=args.clear, buildtime_only=buildtime_only, runtime_only=runtime_only
182
+ )
183
+ asyncio.run(task)
184
+
185
+ except KeyError:
186
+ pass
147
187
  else:
148
188
  print(f'Error: No such command "{args.command}". Enter "squirrels -h" for help.')
149
189
 
@@ -0,0 +1,147 @@
1
+ from typing import Tuple, Optional
2
+ import inquirer
3
+
4
+ from ._project import SquirrelsProject
5
+ from . import _constants as c, _models as m
6
+
7
+
8
+ def prompt_compile_options(
9
+ project: SquirrelsProject,
10
+ *,
11
+ buildtime_only: bool,
12
+ runtime_only: bool,
13
+ test_set: Optional[str],
14
+ do_all_test_sets: bool,
15
+ selected_model: Optional[str],
16
+ ) -> Tuple[bool, bool, Optional[str], bool, Optional[str]]:
17
+ """
18
+ Interactive prompts to derive compile options when --yes is not provided.
19
+
20
+ Returns updated values for:
21
+ (buildtime_only, runtime_only, test_set, do_all_test_sets, selected_model)
22
+ """
23
+ # 1) Scope prompt (skip if explicit flags provided)
24
+ if not buildtime_only and not runtime_only:
25
+ scope_answer = inquirer.prompt([
26
+ inquirer.List(
27
+ 'scope',
28
+ message='Select scope',
29
+ choices=[
30
+ ('All models', 'all'),
31
+ ('Buildtime models only', 'buildtime'),
32
+ ('Runtime models only', 'runtime'),
33
+ ],
34
+ default='all',
35
+ )
36
+ ]) or {}
37
+ scope_val = scope_answer['scope']
38
+ if scope_val == 'buildtime':
39
+ buildtime_only, runtime_only = True, False
40
+ elif scope_val == 'runtime':
41
+ buildtime_only, runtime_only = False, True
42
+ else:
43
+ buildtime_only, runtime_only = False, False
44
+
45
+ # 2) Runtime test set prompt (only if runtime is included and -t/-T not provided)
46
+ runtime_included, buildtime_included = not buildtime_only, not runtime_only
47
+ if runtime_included and not test_set and not do_all_test_sets:
48
+ test_mode_answer = inquirer.prompt([
49
+ inquirer.List(
50
+ 'ts_mode',
51
+ message='Runtime selections for parameters, configurables, and user attributes?',
52
+ choices=[
53
+ ('Use default selections', 'default'),
54
+ ('Use a custom test set', 'custom'),
55
+ ('Use all test sets (including default selections)', 'all'),
56
+ ],
57
+ default='default',
58
+ )
59
+ ]) or {}
60
+ ts_mode = test_mode_answer['ts_mode']
61
+ if ts_mode == 'all':
62
+ do_all_test_sets = True
63
+ elif ts_mode == 'custom':
64
+ # Build available test set list
65
+ ts_names = list(project._manifest_cfg.selection_test_sets.keys())
66
+ ts_answer = inquirer.prompt([
67
+ inquirer.List(
68
+ 'test_set',
69
+ message='Pick a selection test set',
70
+ choices=ts_names if ts_names else [c.DEFAULT_TEST_SET_NAME],
71
+ default=c.DEFAULT_TEST_SET_NAME,
72
+ )
73
+ ]) or {}
74
+ test_set = ts_answer['test_set']
75
+
76
+ # 3) Model selection prompt (only if -s not provided)
77
+ if not selected_model:
78
+ # Ask whether to compile all or a specific model
79
+ model_mode_answer = inquirer.prompt([
80
+ inquirer.List(
81
+ 'model_mode',
82
+ message='Compile all models in scope or just a specific model?',
83
+ choices=[
84
+ ('All models', 'all'),
85
+ ('Select a specific model', 'one'),
86
+ ],
87
+ default='all',
88
+ )
89
+ ]) or {}
90
+ model_mode = model_mode_answer['model_mode']
91
+
92
+ if model_mode == 'one':
93
+ # Build list of runtime query models (dbviews + federates) and build models if included
94
+ models_dict = project._get_models_dict(always_python_df=False)
95
+
96
+ valid_model_types: list[m.ModelType] = []
97
+ if runtime_included:
98
+ valid_model_types.extend([m.ModelType.DBVIEW, m.ModelType.FEDERATE])
99
+ if buildtime_included:
100
+ valid_model_types.append(m.ModelType.BUILD)
101
+
102
+ runtime_names = sorted([
103
+ (f"({model.model_type.value}) {name}", name) for name, model in models_dict.items()
104
+ if isinstance(model, m.QueryModel) and model.model_type in valid_model_types
105
+ ])
106
+ if runtime_names:
107
+ model_answer = inquirer.prompt([
108
+ inquirer.List(
109
+ 'selected_model',
110
+ message='Pick a runtime model to compile',
111
+ choices=runtime_names,
112
+ )
113
+ ]) or {}
114
+ selected_model = model_answer['selected_model']
115
+
116
+ # Tips and equivalent command without prompts
117
+ def _maybe_quote(val: str) -> str:
118
+ return f'"{val}"' if any(ch.isspace() for ch in val) else val
119
+
120
+ parts = ["sqrl", "compile", "-y"]
121
+ if buildtime_only:
122
+ parts.append("--buildtime-only")
123
+ elif runtime_only:
124
+ parts.append("--runtime-only")
125
+ if do_all_test_sets:
126
+ parts.append("-T")
127
+ elif test_set:
128
+ parts.extend(["-t", _maybe_quote(test_set)])
129
+ if selected_model:
130
+ parts.extend(["-s", _maybe_quote(selected_model)])
131
+
132
+ # Pretty tips block
133
+ tips_header = " Compile tips "
134
+ border = "=" * 80
135
+ print(border)
136
+ print(tips_header)
137
+ print("-" * len(border))
138
+ print("Equivalent command (no prompts):")
139
+ print(f" $ {' '.join(parts)}")
140
+ print()
141
+ print("You can also:")
142
+ print(" - Add -c/--clear to clear the 'target/compile/' folder before compiling")
143
+ print(" - Add -r/--runquery to generate CSVs for runtime models")
144
+ print(border)
145
+ print()
146
+
147
+ return buildtime_only, runtime_only, test_set, do_all_test_sets, selected_model
@@ -1,11 +1,11 @@
1
1
  from typing import Any
2
2
  from dataclasses import dataclass, field
3
3
  from sqlalchemy import Engine
4
- import time, polars as pl
4
+ import time, polars as pl, duckdb
5
5
 
6
6
  from . import _utils as u, _constants as c, _py_module as pm
7
- from .arguments.init_time_args import ConnectionsArgs
8
- from ._manifest import ManifestConfig, ConnectionProperties, ConnectionType
7
+ from ._arguments.init_time_args import ConnectionsArgs
8
+ from ._manifest import ManifestConfig, ConnectionProperties, ConnectionTypeEnum
9
9
 
10
10
 
11
11
  @dataclass
@@ -31,13 +31,20 @@ class ConnectionSet:
31
31
  def run_sql_query_from_conn_name(self, query: str, conn_name: str, placeholders: dict = {}) -> pl.DataFrame:
32
32
  conn = self.get_connection(conn_name)
33
33
  try:
34
- if isinstance(conn, ConnectionProperties) and (conn.type == ConnectionType.CONNECTORX or conn.type == ConnectionType.ADBC):
34
+ if isinstance(conn, ConnectionProperties) and (conn.type == ConnectionTypeEnum.CONNECTORX or conn.type == ConnectionTypeEnum.ADBC):
35
35
  if len(placeholders) > 0:
36
36
  raise u.ConfigurationError(f"Connection '{conn_name}' is a ConnectorX or ADBC connection, which does not support placeholders")
37
37
  df = pl.read_database_uri(query, conn.uri, engine=conn.type.value)
38
- elif isinstance(conn, ConnectionProperties) and conn.type == ConnectionType.SQLALCHEMY:
38
+ elif isinstance(conn, ConnectionProperties) and conn.type == ConnectionTypeEnum.SQLALCHEMY:
39
39
  with conn.engine.connect() as connection:
40
40
  df = pl.read_database(query, connection, execute_options={"parameters": placeholders})
41
+ elif isinstance(conn, ConnectionProperties) and conn.type == ConnectionTypeEnum.DUCKDB:
42
+ path = conn.uri # .replace("duckdb://", "")
43
+ dd_conn = duckdb.connect(path)
44
+ try:
45
+ df = dd_conn.sql(query, params=placeholders).pl()
46
+ finally:
47
+ dd_conn.close()
41
48
  else:
42
49
  df = pl.read_database(query, conn, execute_options={"parameters": placeholders}) # type: ignore
43
50
  return df
@@ -52,7 +59,7 @@ class ConnectionSet:
52
59
  if isinstance(conn, Engine):
53
60
  conn.dispose()
54
61
  elif isinstance(conn, ConnectionProperties):
55
- if conn.type == ConnectionType.SQLALCHEMY:
62
+ if conn.type == ConnectionTypeEnum.SQLALCHEMY:
56
63
  conn.engine.dispose()
57
64
  elif hasattr(conn, 'close'):
58
65
  conn.close()
@@ -83,7 +90,9 @@ class ConnectionSetIO:
83
90
  connections: dict[str, ConnectionProperties | Any] = {}
84
91
 
85
92
  for config in manifest_cfg.connections.values():
86
- connections[config.name] = ConnectionProperties(label=config.label, type=config.type, uri=config.uri)
93
+ connections[config.name] = ConnectionProperties(
94
+ label=config.label, type=config.type, uri=config.uri, sa_create_engine_args=config.sa_create_engine_args
95
+ )
87
96
 
88
97
  pm.run_pyconfig_main(base_path, c.CONNECTIONS_FILE, {"connections": connections, "sqrl": conn_args})
89
98
 
squirrels/_constants.py CHANGED
@@ -13,27 +13,39 @@ 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'
20
+ SQRL_PARAMETERS_DATASOURCE_REFRESH_MINUTES = 'SQRL_PARAMETERS__DATASOURCE_REFRESH_MINUTES'
19
21
 
20
22
  SQRL_DATASETS_CACHE_SIZE = 'SQRL_DATASETS__CACHE_SIZE'
21
23
  SQRL_DATASETS_CACHE_TTL_MINUTES = 'SQRL_DATASETS__CACHE_TTL_MINUTES'
24
+ SQRL_DATASETS_MAX_ROWS_FOR_AI = 'SQRL_DATASETS__MAX_ROWS_FOR_AI'
22
25
 
23
26
  SQRL_DASHBOARDS_CACHE_SIZE = 'SQRL_DASHBOARDS__CACHE_SIZE'
24
27
  SQRL_DASHBOARDS_CACHE_TTL_MINUTES = 'SQRL_DASHBOARDS__CACHE_TTL_MINUTES'
25
28
 
29
+ SQRL_PERMISSIONS_ELEVATED_ACCESS_LEVEL = 'SQRL_PERMISSIONS__ELEVATED_ACCESS_LEVEL'
30
+
26
31
  SQRL_SEEDS_INFER_SCHEMA = 'SQRL_SEEDS__INFER_SCHEMA'
27
32
  SQRL_SEEDS_NA_VALUES = 'SQRL_SEEDS__NA_VALUES'
28
33
 
29
- SQRL_TEST_SETS_DEFAULT_NAME_USED = 'SQRL_TEST_SETS__DEFAULT_NAME_USED'
30
-
31
34
  SQRL_CONNECTIONS_DEFAULT_NAME_USED = 'SQRL_CONNECTIONS__DEFAULT_NAME_USED'
32
35
 
33
- SQRL_DUCKDB_VENV_DB_FILE_PATH = 'SQRL_DUCKDB_VENV__DB_FILE_PATH'
36
+ SQRL_VDL_CATALOG_DB_PATH = 'SQRL_VDL__CATALOG_DB_PATH'
37
+ SQRL_VDL_DATA_PATH = 'SQRL_VDL__DATA_PATH'
38
+
39
+ SQRL_STUDIO_BASE_URL = 'SQRL_STUDIO__BASE_URL'
40
+
41
+ SQRL_LOGGING_LOG_LEVEL = 'SQRL_LOGGING__LOG_LEVEL'
42
+ SQRL_LOGGING_LOG_FORMAT = 'SQRL_LOGGING__LOG_FORMAT'
43
+ SQRL_LOGGING_LOG_TO_FILE = 'SQRL_LOGGING__LOG_TO_FILE'
44
+ SQRL_LOGGING_LOG_FILE_SIZE_MB = 'SQRL_LOGGING__LOG_FILE_SIZE_MB'
45
+ SQRL_LOGGING_LOG_FILE_BACKUP_COUNT = 'SQRL_LOGGING__LOG_FILE_BACKUP_COUNT'
34
46
 
35
47
  # Folder/File names
36
- PACKAGE_DATA_FOLDER = 'package_data'
48
+ PACKAGE_DATA_FOLDER = '_package_data'
37
49
  BASE_PROJECT_FOLDER = 'base_project'
38
50
 
39
51
  GLOBAL_ENV_FOLDER = '.squirrels'
@@ -52,6 +64,7 @@ LOGS_FILE = 'squirrels.log'
52
64
 
53
65
  DATABASE_FOLDER = 'assets'
54
66
  PACKAGES_FOLDER = 'sqrl_packages'
67
+ PUBLIC_FOLDER = 'public'
55
68
 
56
69
  MACROS_FOLDER = 'macros'
57
70
  MACROS_FILE = 'macros_example.sql'
@@ -77,17 +90,19 @@ USER_FILE = 'user.py'
77
90
  ADMIN_USERNAME = 'admin'
78
91
 
79
92
  TARGET_FOLDER = 'target'
80
- DB_FILE = 'auth.sqlite'
81
93
  COMPILE_FOLDER = 'compile'
82
- DUCKDB_VENV_FILE = 'venv.duckdb'
94
+ COMPILE_BUILDTIME_FOLDER = 'buildtime'
95
+ COMPILE_RUNTIME_FOLDER = 'runtime'
96
+ DB_FILE = 'auth.sqlite'
97
+ DEFAULT_VDL_CATALOG_DB_PATH = 'ducklake:{project_path}/target/vdl_catalog.duckdb'
98
+ DEFAULT_VDL_DATA_PATH = '{project_path}/target/vdl_data/'
83
99
 
84
100
  SEEDS_FOLDER = 'seeds'
85
101
  SEED_CATEGORY_FILE_STEM = 'seed_categories'
86
102
  SEED_SUBCATEGORY_FILE_STEM = 'seed_subcategories'
87
103
 
88
- # Selection cfg sections
89
- USER_ATTRIBUTES_SECTION = 'user_attributes'
90
- PARAMETERS_SECTION = 'parameters'
104
+ # Test sets
105
+ DEFAULT_TEST_SET_NAME = 'default'
91
106
 
92
107
  # Init Command Choices
93
108
  SQL_FILE_TYPE = 'sql'
@@ -114,3 +129,8 @@ MAIN_FUNC = "main"
114
129
  # Regex
115
130
  DATE_REGEX = r"^\d{4}\-\d{2}\-\d{2}$"
116
131
  COLOR_REGEX = r"^#[0-9a-fA-F]{6}$"
132
+
133
+ # OAuth2
134
+ SUPPORTED_SCOPES = ['read']
135
+ SUPPORTED_GRANT_TYPES = ['authorization_code', 'refresh_token']
136
+ 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, _utils as u, dashboards as d
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}"