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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. dateutils/__init__.py +6 -0
  2. dateutils/_enums.py +25 -0
  3. squirrels/dateutils.py → dateutils/_implementation.py +409 -380
  4. dateutils/types.py +6 -0
  5. squirrels/__init__.py +21 -18
  6. squirrels/_api_routes/__init__.py +5 -0
  7. squirrels/_api_routes/auth.py +337 -0
  8. squirrels/_api_routes/base.py +196 -0
  9. squirrels/_api_routes/dashboards.py +156 -0
  10. squirrels/_api_routes/data_management.py +148 -0
  11. squirrels/_api_routes/datasets.py +220 -0
  12. squirrels/_api_routes/project.py +289 -0
  13. squirrels/_api_server.py +552 -134
  14. squirrels/_arguments/__init__.py +0 -0
  15. squirrels/_arguments/init_time_args.py +83 -0
  16. squirrels/_arguments/run_time_args.py +111 -0
  17. squirrels/_auth.py +777 -0
  18. squirrels/_command_line.py +239 -107
  19. squirrels/_compile_prompts.py +147 -0
  20. squirrels/_connection_set.py +94 -0
  21. squirrels/_constants.py +141 -64
  22. squirrels/_dashboards.py +179 -0
  23. squirrels/_data_sources.py +570 -0
  24. squirrels/_dataset_types.py +91 -0
  25. squirrels/_env_vars.py +209 -0
  26. squirrels/_exceptions.py +29 -0
  27. squirrels/_http_error_responses.py +52 -0
  28. squirrels/_initializer.py +319 -110
  29. squirrels/_logging.py +121 -0
  30. squirrels/_manifest.py +357 -187
  31. squirrels/_mcp_server.py +578 -0
  32. squirrels/_model_builder.py +69 -0
  33. squirrels/_model_configs.py +74 -0
  34. squirrels/_model_queries.py +52 -0
  35. squirrels/_models.py +1201 -0
  36. squirrels/_package_data/base_project/.env +7 -0
  37. squirrels/_package_data/base_project/.env.example +44 -0
  38. squirrels/_package_data/base_project/connections.yml +16 -0
  39. squirrels/_package_data/base_project/dashboards/dashboard_example.py +40 -0
  40. squirrels/_package_data/base_project/dashboards/dashboard_example.yml +22 -0
  41. squirrels/_package_data/base_project/docker/.dockerignore +16 -0
  42. squirrels/_package_data/base_project/docker/Dockerfile +16 -0
  43. squirrels/_package_data/base_project/docker/compose.yml +7 -0
  44. squirrels/_package_data/base_project/duckdb_init.sql +10 -0
  45. squirrels/_package_data/base_project/gitignore +13 -0
  46. squirrels/_package_data/base_project/macros/macros_example.sql +17 -0
  47. squirrels/_package_data/base_project/models/builds/build_example.py +26 -0
  48. squirrels/_package_data/base_project/models/builds/build_example.sql +16 -0
  49. squirrels/_package_data/base_project/models/builds/build_example.yml +57 -0
  50. squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +17 -0
  51. squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +32 -0
  52. squirrels/_package_data/base_project/models/federates/federate_example.py +51 -0
  53. squirrels/_package_data/base_project/models/federates/federate_example.sql +21 -0
  54. squirrels/_package_data/base_project/models/federates/federate_example.yml +65 -0
  55. squirrels/_package_data/base_project/models/sources.yml +38 -0
  56. squirrels/_package_data/base_project/parameters.yml +142 -0
  57. squirrels/_package_data/base_project/pyconfigs/connections.py +19 -0
  58. squirrels/_package_data/base_project/pyconfigs/context.py +96 -0
  59. squirrels/_package_data/base_project/pyconfigs/parameters.py +141 -0
  60. squirrels/_package_data/base_project/pyconfigs/user.py +56 -0
  61. squirrels/_package_data/base_project/resources/expenses.db +0 -0
  62. squirrels/_package_data/base_project/resources/public/.gitkeep +0 -0
  63. squirrels/_package_data/base_project/resources/weather.db +0 -0
  64. squirrels/_package_data/base_project/seeds/seed_categories.csv +6 -0
  65. squirrels/_package_data/base_project/seeds/seed_categories.yml +15 -0
  66. squirrels/_package_data/base_project/seeds/seed_subcategories.csv +15 -0
  67. squirrels/_package_data/base_project/seeds/seed_subcategories.yml +21 -0
  68. squirrels/_package_data/base_project/squirrels.yml.j2 +61 -0
  69. squirrels/_package_data/base_project/tmp/.gitignore +2 -0
  70. squirrels/_package_data/templates/login_successful.html +53 -0
  71. squirrels/_package_data/templates/squirrels_studio.html +22 -0
  72. squirrels/_package_loader.py +29 -0
  73. squirrels/_parameter_configs.py +592 -0
  74. squirrels/_parameter_options.py +348 -0
  75. squirrels/_parameter_sets.py +207 -0
  76. squirrels/_parameters.py +1703 -0
  77. squirrels/_project.py +796 -0
  78. squirrels/_py_module.py +122 -0
  79. squirrels/_request_context.py +33 -0
  80. squirrels/_schemas/__init__.py +0 -0
  81. squirrels/_schemas/auth_models.py +83 -0
  82. squirrels/_schemas/query_param_models.py +70 -0
  83. squirrels/_schemas/request_models.py +26 -0
  84. squirrels/_schemas/response_models.py +286 -0
  85. squirrels/_seeds.py +97 -0
  86. squirrels/_sources.py +112 -0
  87. squirrels/_utils.py +540 -149
  88. squirrels/_version.py +1 -3
  89. squirrels/arguments.py +7 -0
  90. squirrels/auth.py +4 -0
  91. squirrels/connections.py +3 -0
  92. squirrels/dashboards.py +3 -0
  93. squirrels/data_sources.py +14 -282
  94. squirrels/parameter_options.py +13 -189
  95. squirrels/parameters.py +14 -801
  96. squirrels/types.py +18 -0
  97. squirrels-0.6.0.post0.dist-info/METADATA +148 -0
  98. squirrels-0.6.0.post0.dist-info/RECORD +101 -0
  99. {squirrels-0.1.0.dist-info → squirrels-0.6.0.post0.dist-info}/WHEEL +1 -2
  100. {squirrels-0.1.0.dist-info → squirrels-0.6.0.post0.dist-info}/entry_points.txt +1 -0
  101. squirrels-0.6.0.post0.dist-info/licenses/LICENSE +201 -0
  102. squirrels/_credentials_manager.py +0 -87
  103. squirrels/_module_loader.py +0 -37
  104. squirrels/_parameter_set.py +0 -151
  105. squirrels/_renderer.py +0 -286
  106. squirrels/_timed_imports.py +0 -37
  107. squirrels/connection_set.py +0 -126
  108. squirrels/package_data/base_project/.gitignore +0 -4
  109. squirrels/package_data/base_project/connections.py +0 -21
  110. squirrels/package_data/base_project/database/sample_database.db +0 -0
  111. squirrels/package_data/base_project/database/seattle_weather.db +0 -0
  112. squirrels/package_data/base_project/datasets/sample_dataset/context.py +0 -8
  113. squirrels/package_data/base_project/datasets/sample_dataset/database_view1.py +0 -23
  114. squirrels/package_data/base_project/datasets/sample_dataset/database_view1.sql.j2 +0 -7
  115. squirrels/package_data/base_project/datasets/sample_dataset/final_view.py +0 -10
  116. squirrels/package_data/base_project/datasets/sample_dataset/final_view.sql.j2 +0 -2
  117. squirrels/package_data/base_project/datasets/sample_dataset/parameters.py +0 -30
  118. squirrels/package_data/base_project/datasets/sample_dataset/selections.cfg +0 -6
  119. squirrels/package_data/base_project/squirrels.yaml +0 -26
  120. squirrels/package_data/static/favicon.ico +0 -0
  121. squirrels/package_data/static/script.js +0 -234
  122. squirrels/package_data/static/style.css +0 -110
  123. squirrels/package_data/templates/index.html +0 -32
  124. squirrels-0.1.0.dist-info/LICENSE +0 -22
  125. squirrels-0.1.0.dist-info/METADATA +0 -67
  126. squirrels-0.1.0.dist-info/RECORD +0 -40
  127. squirrels-0.1.0.dist-info/top_level.txt +0 -1
@@ -1,107 +1,239 @@
1
- from typing import List, Tuple, Optional
2
- from argparse import ArgumentParser
3
- import sys, time, pwinput
4
- sys.path.append('.')
5
-
6
- from squirrels import _constants as c, _credentials_manager as cm, _manifest as mf, _module_loader as ml
7
- from squirrels import connection_set as cs
8
- from squirrels._version import __version__
9
- from squirrels._api_server import ApiServer
10
- from squirrels._renderer import RendererIOWrapper
11
- from squirrels._initializer import Initializer
12
- from squirrels._timed_imports import timer
13
-
14
-
15
- def _prompt_user_pw(args_values: Optional[List[str]]) -> Tuple[str, str]:
16
- if args_values is not None:
17
- user, pw = args_values
18
- else:
19
- user = input("Enter username: ")
20
- pw = pwinput.pwinput("Enter password: ")
21
- return user, pw
22
-
23
-
24
- def main():
25
- """
26
- Main entry point for the squirrels command line utilities.
27
- """
28
- start = time.time()
29
- parser = ArgumentParser(description="Command line utilities from the squirrels python package")
30
- parser.add_argument('-v', '--version', action='store_true', help='Show the version and exit')
31
- parser.add_argument('--verbose', action='store_true', help='Enable verbose output')
32
- subparsers = parser.add_subparsers(title='commands', dest='command')
33
-
34
- init_parser = subparsers.add_parser(c.INIT_CMD, help='Initialize a squirrels project')
35
- init_parser.add_argument('--overwrite', action='store_true', help="Overwrite files that already exist")
36
- init_parser.add_argument('--core', action='store_true', help='Include all core files (squirrels.yaml, parameters.py, database view, etc.)')
37
- init_parser.add_argument('--db-view', type=str, choices=c.FILE_TYPE_CHOICES, help='Create database view as sql (default) or python file. Ignored if "--core" is not specified')
38
- init_parser.add_argument('--connections', action='store_true', help=f'Include the {c.CONNECTIONS_FILE} file')
39
- init_parser.add_argument('--context', action='store_true', help=f'Include the {c.CONTEXT_FILE} file')
40
- init_parser.add_argument('--selections-cfg', action='store_true', help=f'Include the {c.SELECTIONS_CFG_FILE} file')
41
- init_parser.add_argument('--final-view', type=str, choices=c.FILE_TYPE_CHOICES, help='Include final view as sql or python file')
42
- init_parser.add_argument('--sample-db', type=str, choices=c.DATABASE_CHOICES, help='Sample sqlite database to include')
43
-
44
- subparsers.add_parser(c.LOAD_MODULES_CMD, help='Load all the modules specified in squirrels.yaml from git')
45
-
46
- def _add_profile_argument(parser: ArgumentParser):
47
- parser.add_argument('key', type=str, help='Key to the database connection credential')
48
-
49
- set_cred_parser = subparsers.add_parser(c.SET_CRED_CMD, help='Set a database connection credential key')
50
- _add_profile_argument(set_cred_parser)
51
- set_cred_parser.add_argument('--values', type=str, nargs=2, help='The username and password')
52
-
53
- subparsers.add_parser(c.GET_CREDS_CMD, help='Get all database connection credential keys')
54
-
55
- delete_cred_parser = subparsers.add_parser(c.DELETE_CRED_CMD, help='Delete a database connection credential key')
56
- _add_profile_argument(delete_cred_parser)
57
-
58
- test_parser = subparsers.add_parser(c.TEST_CMD, help='For a given dataset, create outputs for parameter API response and rendered sql queries')
59
- test_parser.add_argument('dataset', type=str, help='Name of dataset (provided in squirrels.yaml) to test. Results are written in an "outputs" folder')
60
- test_parser.add_argument('-c', '--cfg', type=str, help="Configuration file for parameter selections. Path is relative to the dataset's folder")
61
- test_parser.add_argument('-d', '--data', type=str, help="Excel file with lookup data to avoid making a database connection. Path is relative to the dataset's folder")
62
- test_parser.add_argument('-r', '--runquery', action='store_true', help='Runs all database queries and final view, and produce the results as csv files')
63
-
64
- run_parser = subparsers.add_parser(c.RUN_CMD, help='Run the builtin API server')
65
- run_parser.add_argument('--no-cache', action='store_true', help='Do not cache any api results')
66
- run_parser.add_argument('--debug', action='store_true', help='In debug mode, all "hidden parameters" show in the parameters response')
67
- run_parser.add_argument('--host', type=str, default='127.0.0.1')
68
- run_parser.add_argument('--port', type=int, default=8000)
69
-
70
- args, _ = parser.parse_known_args()
71
- timer.verbose = args.verbose
72
- timer.add_activity_time('parsing arguments', start)
73
-
74
- if args.version:
75
- print(__version__)
76
- elif args.command == c.INIT_CMD:
77
- Initializer(args.overwrite).init_project(args)
78
- elif args.command == c.LOAD_MODULES_CMD:
79
- manifest = mf._from_file()
80
- ml.load_modules(manifest)
81
- elif args.command == c.SET_CRED_CMD:
82
- user, pw = _prompt_user_pw(args.values)
83
- cm.squirrels_config_io.set_credential(args.key, user, pw)
84
- elif args.command == c.GET_CREDS_CMD:
85
- cm.squirrels_config_io.print_all_credentials()
86
- elif args.command == c.DELETE_CRED_CMD:
87
- cm.squirrels_config_io.delete_credential(args.key)
88
- elif args.command in [c.RUN_CMD, c.TEST_CMD]:
89
- manifest = mf._from_file()
90
- conn_set = cs._from_file(manifest)
91
- if args.command == c.RUN_CMD:
92
- server = ApiServer(manifest, conn_set, args.no_cache, args.debug)
93
- server.run(args)
94
- elif args.command == c.TEST_CMD:
95
- rendererIO = RendererIOWrapper(args.dataset, manifest, conn_set, args.data)
96
- rendererIO.write_outputs(args.cfg, args.runquery)
97
- conn_set._dispose()
98
- elif args.command is None:
99
- print(f'Command is missing. Enter "squirrels -h" for help.')
100
- else:
101
- print(f'Error: No such command "{args.command}". Enter "squirrels -h" for help.')
102
-
103
- timer.report_times()
104
-
105
-
106
- if __name__ == '__main__':
107
- main()
1
+ from argparse import ArgumentParser, _SubParsersAction
2
+ from pathlib import Path
3
+ from update_checker import UpdateChecker
4
+ import sys, asyncio, traceback, io, subprocess
5
+
6
+ sys.path.append('.')
7
+
8
+ from ._version import __version__
9
+ from ._api_server import ApiServer
10
+ from ._initializer import Initializer
11
+ from ._package_loader import PackageLoaderIO
12
+ from ._project import SquirrelsProject
13
+ from . import _constants as c, _utils as u
14
+ from ._compile_prompts import prompt_compile_options
15
+
16
+
17
+ def _run_duckdb_cli(project: SquirrelsProject, ui: bool):
18
+ init_sql = u._read_duckdb_init_sql(datalake_db_path=project._vdl_catalog_db_path)
19
+
20
+ target_init_path = None
21
+ if init_sql:
22
+ target_init_path = Path(c.TARGET_FOLDER, c.DUCKDB_INIT_FILE)
23
+ target_init_path.parent.mkdir(parents=True, exist_ok=True)
24
+ target_init_path.write_text(init_sql)
25
+
26
+ init_args = ["-init", str(target_init_path)] if target_init_path else []
27
+ command = ['duckdb']
28
+ if ui:
29
+ command.append('-ui')
30
+ if init_args:
31
+ command.extend(init_args)
32
+
33
+ print("Starting DuckDB CLI with command:")
34
+ print(f"$ {' '.join(command)}")
35
+ print()
36
+ print("To exit the DuckDB CLI, enter '.exit'")
37
+ print()
38
+
39
+ try:
40
+ subprocess.run(command, check=True)
41
+ except FileNotFoundError:
42
+ print("DuckDB CLI not found. Please install it from: https://duckdb.org/install")
43
+ except subprocess.CalledProcessError:
44
+ pass # ignore errors that occured on duckdb shell commands
45
+
46
+
47
+ def main():
48
+ """
49
+ Main entry point for the squirrels command line utilities.
50
+ """
51
+ # Create a parent parser with common logging options
52
+ parent_parser = ArgumentParser(add_help=False)
53
+ parent_parser.add_argument('-h', '--help', action="help", help="Show this help message and exit")
54
+ parent_parser.add_argument('--log-level', type=str, choices=["DEBUG", "INFO", "WARNING"], help='Level of logging to use. Default is from SQRL_LOGGING__LEVEL environment variable or INFO.')
55
+ parent_parser.add_argument('--log-format', type=str, choices=["text", "json"], help='Format of the log records. Default is from SQRL_LOGGING__FORMAT environment variable or text.')
56
+ parent_parser.add_argument(
57
+ '--log-to-file', action='store_true',
58
+ help='Enable logging to file(s) in the "logs/" folder (or a custom folder) with rotation and retention policies. '
59
+ 'Default is False unless overridden by the SQRL_LOGGING__TO_FILE environment variable.'
60
+ )
61
+
62
+ parser = ArgumentParser(description="Command line utilities from the squirrels python package", add_help=False, parents=[parent_parser])
63
+ parser.add_argument('-V', '--version', action='store_true', help='Show the version and exit')
64
+ subparsers = parser.add_subparsers(title='commands', dest='command')
65
+
66
+ def add_subparser(subparsers: _SubParsersAction, cmd: str, help_text: str):
67
+ subparser: ArgumentParser = subparsers.add_parser(cmd, description=help_text, help=help_text, add_help=False, parents=[parent_parser])
68
+ return subparser
69
+
70
+ new_parser = add_subparser(subparsers, c.NEW_CMD, 'Create a new squirrels project')
71
+
72
+ new_parser.add_argument('name', nargs='?', type=str, help='The name of the project folder to create. Ignored if --curr-dir is used')
73
+ new_parser.add_argument('--curr-dir', action='store_true', help='Create the project in the current directory')
74
+ new_parser.add_argument('--use-defaults', action='store_true', help='Use default values for unspecified options (except project folder name) instead of prompting for input')
75
+ new_parser.add_argument('--connections', type=str, choices=c.CONF_FORMAT_CHOICES, help=f'Configure database connections as yaml (default) or python')
76
+ new_parser.add_argument('--parameters', type=str, choices=c.CONF_FORMAT_CHOICES, help=f'Configure parameters as python (default) or yaml')
77
+ new_parser.add_argument('--build', type=str, choices=c.FILE_TYPE_CHOICES, help='Create build model as sql (default) or python file')
78
+ new_parser.add_argument('--federate', type=str, choices=c.FILE_TYPE_CHOICES, help='Create federated model as sql (default) or python file')
79
+ new_parser.add_argument('--dashboard', type=str, choices=['y', 'n'], help=f'Include (y) or exclude (n, default) a sample dashboard file')
80
+ new_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')
81
+
82
+ init_parser = add_subparser(subparsers, c.INIT_CMD, 'Create a new squirrels project in the current directory (alias for "new --curr-dir")')
83
+ init_parser.add_argument('--use-defaults', action='store_true', help='Use default values for unspecified options instead of prompting for input')
84
+ init_parser.add_argument('--connections', type=str, choices=c.CONF_FORMAT_CHOICES, help=f'Configure database connections as yaml (default) or python')
85
+ init_parser.add_argument('--parameters', type=str, choices=c.CONF_FORMAT_CHOICES, help=f'Configure parameters as python (default) or yaml')
86
+ init_parser.add_argument('--build', type=str, choices=c.FILE_TYPE_CHOICES, help='Create build model as sql (default) or python file')
87
+ init_parser.add_argument('--federate', type=str, choices=c.FILE_TYPE_CHOICES, help='Create federated model as sql (default) or python file')
88
+ init_parser.add_argument('--dashboard', type=str, choices=['y', 'n'], help=f'Include (y) or exclude (n, default) a sample dashboard file')
89
+ init_parser.add_argument('--admin-password', type=str, help='The password for the admin user. By default, a random password is generated')
90
+
91
+ def with_file_format_options(parser: ArgumentParser):
92
+ help_text = "Create model as sql (default) or python file"
93
+ parser.add_argument('--format', type=str, choices=c.FILE_TYPE_CHOICES, default=c.SQL_FILE_TYPE, help=help_text)
94
+ return parser
95
+
96
+ 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."
97
+ get_file_parser = add_subparser(subparsers, c.GET_FILE_CMD, get_file_help_text)
98
+ get_file_subparsers = get_file_parser.add_subparsers(title='file_name', dest='file_name')
99
+ add_subparser(get_file_subparsers, c.DOTENV_FILE, f'Creates sample {c.DOTENV_FILE} and {c.DOTENV_FILE}.example files')
100
+ add_subparser(get_file_subparsers, c.GITIGNORE_FILE, f'Creates a sample {c.GITIGNORE_FILE} file')
101
+ manifest_parser = add_subparser(get_file_subparsers, c.MANIFEST_FILE, f'Creates a sample {c.MANIFEST_FILE} file')
102
+ manifest_parser.add_argument("--no-connections", action='store_true', help=f'Exclude the connections section')
103
+ manifest_parser.add_argument("--parameters", action='store_true', help=f'Include the parameters section')
104
+ manifest_parser.add_argument("--dashboards", action='store_true', help=f'Include the dashboards section')
105
+ add_subparser(get_file_subparsers, c.USER_FILE, f'Creates a sample {c.USER_FILE} file')
106
+ add_subparser(get_file_subparsers, c.CONNECTIONS_FILE, f'Creates a sample {c.CONNECTIONS_FILE} file')
107
+ add_subparser(get_file_subparsers, c.PARAMETERS_FILE, f'Creates a sample {c.PARAMETERS_FILE} file')
108
+ add_subparser(get_file_subparsers, c.CONTEXT_FILE, f'Creates a sample {c.CONTEXT_FILE} file')
109
+ add_subparser(get_file_subparsers, c.MACROS_FILE, f'Creates a sample {c.MACROS_FILE} file')
110
+ add_subparser(get_file_subparsers, c.SOURCES_FILE, f'Creates a sample {c.SOURCES_FILE} file')
111
+ with_file_format_options(add_subparser(get_file_subparsers, c.BUILD_FILE_STEM, f'Creates a sample build model file'))
112
+ add_subparser(get_file_subparsers, c.DBVIEW_FILE_STEM, f'Creates a sample dbview model file')
113
+ with_file_format_options(add_subparser(get_file_subparsers, c.FEDERATE_FILE_STEM, f'Creates a sample federate model file'))
114
+ add_subparser(get_file_subparsers, c.DASHBOARD_FILE_STEM, f'Creates a sample dashboard file')
115
+ add_subparser(get_file_subparsers, c.EXPENSES_DB, f'Creates the sample SQLite database on expenses')
116
+ add_subparser(get_file_subparsers, c.WEATHER_DB, f'Creates the sample SQLite database on weather')
117
+ add_subparser(get_file_subparsers, c.SEED_CATEGORY_FILE_STEM, f'Creates the sample seed files for categories lookup')
118
+ add_subparser(get_file_subparsers, c.SEED_SUBCATEGORY_FILE_STEM, f'Creates the sample seed files for subcategories lookup')
119
+
120
+ deps_parser = add_subparser(subparsers, c.DEPS_CMD, f'Load all packages specified in {c.MANIFEST_FILE} (from git)')
121
+
122
+ compile_parser = add_subparser(subparsers, c.COMPILE_CMD, 'Create rendered SQL files in the folder "./target/compile"')
123
+ compile_parser.add_argument('-y', '--yes', action='store_true', help='Disable prompts and assume the default for all settings not overridden by CLI options')
124
+ compile_parser.add_argument('-c', '--clear', action='store_true', help='Clear the "target/compile/" folder before compiling')
125
+ compile_scope_group = compile_parser.add_mutually_exclusive_group(required=False)
126
+ compile_scope_group.add_argument('--buildtime-only', action='store_true', help='Compile only buildtime models')
127
+ compile_scope_group.add_argument('--runtime-only', action='store_true', help='Compile only runtime models')
128
+ compile_test_set_group = compile_parser.add_mutually_exclusive_group(required=False)
129
+ 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")
130
+ 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")
131
+ compile_parser.add_argument('-s', '--select', type=str, help="Select single model to compile. If not specified, all models are compiled")
132
+ compile_parser.add_argument('-r', '--runquery', action='store_true', help='Run runtime models and write CSV outputs too. Does not apply to buildtime models')
133
+
134
+ build_parser = add_subparser(subparsers, c.BUILD_CMD, 'Build the Virtual Data Lake (VDL) for the project')
135
+ build_parser.add_argument('-f', '--full-refresh', action='store_true', help='Drop all tables before building')
136
+ build_parser.add_argument('-s', '--select', type=str, help="Select one static model to build. If not specified, all models are built")
137
+
138
+ duckdb_parser = add_subparser(subparsers, c.DUCKDB_CMD, 'Run the duckdb command line tool')
139
+ duckdb_parser.add_argument('--ui', action='store_true', help='Run the duckdb local UI')
140
+
141
+ run_parser = add_subparser(subparsers, c.RUN_CMD, 'Run the API server')
142
+ run_parser.add_argument('--build', action='store_true', help='Build the VDL first (without full refresh) before running the API server')
143
+ run_parser.add_argument('--no-cache', action='store_true', help='Do not cache any api results')
144
+ run_parser.add_argument('--host', type=str, default='127.0.0.1', help="The host to run on")
145
+ run_parser.add_argument('--port', type=int, default=8000, help="The port to run on")
146
+ run_parser.add_argument(
147
+ '--forwarded-allow-ips', type=str, default=None,
148
+ help="Comma-separated list of IP Addresses, IP Networks, or literals (e.g. UNIX Socket path) to trust with proxy headers. "
149
+ "Defaults to the FORWARDED_ALLOW_IPS environment variable if available, or '127.0.0.1'. The literal '*' means trust everything."
150
+ )
151
+
152
+ args, _ = parser.parse_known_args()
153
+
154
+ update_checker_result = UpdateChecker().check("squirrels", __version__)
155
+ if update_checker_result:
156
+ current_version = update_checker_result.running_version
157
+ latest_version = update_checker_result.available_version
158
+ print("\033[1;33mA newer version of Squirrels is available!\033[0m")
159
+ print(f"{current_version} -> \033[1;32m{latest_version}\033[0m (released on {update_checker_result.release_date.date()})")
160
+ print()
161
+ print("See \033[4mhttps://docs.pysquirrels.com/releases\033[0m for release notes.")
162
+ print()
163
+
164
+ if args.version:
165
+ print(f"Squirrels version: \033[1;32m{__version__}\033[0m")
166
+ elif args.command == c.NEW_CMD:
167
+ Initializer(project_name=args.name, use_curr_dir=args.curr_dir).init_project(args)
168
+ elif args.command == c.INIT_CMD:
169
+ Initializer(project_name=None, use_curr_dir=True).init_project(args)
170
+ elif args.command == c.GET_FILE_CMD:
171
+ Initializer().get_file(args)
172
+ elif args.command is None:
173
+ print(f'Command is missing. Enter "squirrels -h" for help.')
174
+ else:
175
+ project = SquirrelsProject(
176
+ load_dotenv_globally=True,
177
+ log_level=args.log_level,
178
+ log_format=args.log_format,
179
+ log_to_file=args.log_to_file
180
+ )
181
+ try:
182
+ if args.command == c.DEPS_CMD:
183
+ PackageLoaderIO.load_packages(project._logger, project._manifest_cfg, reload=True)
184
+ elif args.command == c.BUILD_CMD:
185
+ task = project.build(full_refresh=args.full_refresh, select=args.select)
186
+ asyncio.run(task)
187
+ print()
188
+ elif args.command == c.DUCKDB_CMD:
189
+ _run_duckdb_cli(project, args.ui)
190
+ elif args.command == c.RUN_CMD:
191
+ if args.build:
192
+ task = project.build(full_refresh=True)
193
+ asyncio.run(task)
194
+ server = ApiServer(args.no_cache, project)
195
+ server.run(args)
196
+ elif args.command == c.COMPILE_CMD:
197
+ # Derive final options with optional interactive prompts (unless --yes is provided)
198
+ buildtime_only = args.buildtime_only
199
+ runtime_only = args.runtime_only
200
+ test_set = args.test_set
201
+ do_all_test_sets = args.all_test_sets
202
+ selected_model = args.select
203
+
204
+ try:
205
+ if not args.yes:
206
+ buildtime_only, runtime_only, test_set, do_all_test_sets, selected_model = prompt_compile_options(
207
+ project,
208
+ buildtime_only=buildtime_only,
209
+ runtime_only=runtime_only,
210
+ test_set=test_set,
211
+ do_all_test_sets=do_all_test_sets,
212
+ selected_model=selected_model,
213
+ )
214
+
215
+ task = project.compile(
216
+ selected_model=selected_model, test_set=test_set, do_all_test_sets=do_all_test_sets, runquery=args.runquery,
217
+ clear=args.clear, buildtime_only=buildtime_only, runtime_only=runtime_only
218
+ )
219
+ asyncio.run(task)
220
+
221
+ except KeyError:
222
+ pass
223
+ else:
224
+ print(f'Error: No such command "{args.command}". Enter "squirrels -h" for help.')
225
+
226
+ except KeyboardInterrupt:
227
+ pass
228
+ except Exception as e:
229
+ buffer = io.StringIO()
230
+ traceback.print_exception(e, file=buffer)
231
+ err_msg = buffer.getvalue()
232
+ print(err_msg)
233
+ project._logger.error(err_msg)
234
+ finally:
235
+ project.close()
236
+
237
+
238
+ if __name__ == '__main__':
239
+ main()
@@ -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
@@ -0,0 +1,94 @@
1
+ from typing import Any
2
+ from dataclasses import dataclass, field
3
+ from sqlalchemy import Engine
4
+ import time, polars as pl, duckdb
5
+
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, ConnectionTypeEnum
9
+
10
+
11
+ @dataclass
12
+ class ConnectionSet:
13
+ """
14
+ A wrapper class around a collection of sqlalchemy engines
15
+
16
+ Attributes:
17
+ _engines: A dictionary of connection name to the corresponding sqlalchemy engine
18
+ """
19
+ _connections: dict[str, ConnectionProperties | Any] = field(default_factory=dict)
20
+
21
+ def get_connections_as_dict(self):
22
+ return self._connections.copy()
23
+
24
+ def get_connection(self, conn_name: str) -> ConnectionProperties | Any:
25
+ try:
26
+ connection = self._connections[conn_name]
27
+ except KeyError as e:
28
+ raise u.ConfigurationError(f'Connection name "{conn_name}" was not configured') from e
29
+ return connection
30
+
31
+ def run_sql_query_from_conn_name(self, query: str, conn_name: str, placeholders: dict = {}) -> pl.DataFrame:
32
+ conn = self.get_connection(conn_name)
33
+ try:
34
+ if isinstance(conn, ConnectionProperties) and (conn.type == ConnectionTypeEnum.CONNECTORX or conn.type == ConnectionTypeEnum.ADBC):
35
+ if len(placeholders) > 0:
36
+ raise u.ConfigurationError(f"Connection '{conn_name}' is a ConnectorX or ADBC connection, which does not support placeholders")
37
+ df = pl.read_database_uri(query, conn.uri, engine=conn.type.value)
38
+ elif isinstance(conn, ConnectionProperties) and conn.type == ConnectionTypeEnum.SQLALCHEMY:
39
+ with conn.engine.connect() as connection:
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()
48
+ else:
49
+ df = pl.read_database(query, conn, execute_options={"parameters": placeholders}) # type: ignore
50
+ return df
51
+ except Exception as e:
52
+ raise RuntimeError(e) from e
53
+
54
+ def dispose(self) -> None:
55
+ """
56
+ Disposes / closes all the connections in this ConnectionSet
57
+ """
58
+ for conn in self._connections.values():
59
+ if isinstance(conn, Engine):
60
+ conn.dispose()
61
+ elif isinstance(conn, ConnectionProperties):
62
+ if conn.type == ConnectionTypeEnum.SQLALCHEMY:
63
+ conn.engine.dispose()
64
+ elif hasattr(conn, 'close'):
65
+ conn.close()
66
+
67
+
68
+ class ConnectionSetIO:
69
+
70
+ @classmethod
71
+ def load_from_file(
72
+ cls, logger: u.Logger, project_path: str, manifest_cfg: ManifestConfig, conn_args: ConnectionsArgs
73
+ ) -> ConnectionSet:
74
+ """
75
+ Takes the DB connection engines from both the squirrels.yml and connections.py files and merges them
76
+ into a single ConnectionSet
77
+
78
+ Returns:
79
+ A ConnectionSet with the DB connections from both squirrels.yml and connections.py
80
+ """
81
+ start = time.time()
82
+ connections: dict[str, ConnectionProperties | Any] = {}
83
+
84
+ for config in manifest_cfg.connections.values():
85
+ connections[config.name] = ConnectionProperties(
86
+ label=config.label, type=config.type, uri=config.uri, sa_create_engine_args=config.sa_create_engine_args
87
+ )
88
+
89
+ pm.run_pyconfig_main(project_path, c.CONNECTIONS_FILE, {"connections": connections, "sqrl": conn_args})
90
+
91
+ conn_set = ConnectionSet(connections)
92
+
93
+ logger.log_activity_time("creating sqlalchemy engines", start)
94
+ return conn_set