squirrels 0.4.0__py3-none-any.whl → 0.5.0__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 (125) 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 +13 -11
  6. squirrels/_api_routes/__init__.py +5 -0
  7. squirrels/_api_routes/auth.py +271 -0
  8. squirrels/_api_routes/base.py +165 -0
  9. squirrels/_api_routes/dashboards.py +150 -0
  10. squirrels/_api_routes/data_management.py +145 -0
  11. squirrels/_api_routes/datasets.py +257 -0
  12. squirrels/_api_routes/oauth2.py +298 -0
  13. squirrels/_api_routes/project.py +252 -0
  14. squirrels/_api_server.py +256 -450
  15. squirrels/_arguments/__init__.py +0 -0
  16. squirrels/_arguments/init_time_args.py +108 -0
  17. squirrels/_arguments/run_time_args.py +147 -0
  18. squirrels/_auth.py +960 -0
  19. squirrels/_command_line.py +126 -45
  20. squirrels/_compile_prompts.py +147 -0
  21. squirrels/_connection_set.py +48 -26
  22. squirrels/_constants.py +68 -38
  23. squirrels/_dashboards.py +160 -0
  24. squirrels/_data_sources.py +570 -0
  25. squirrels/_dataset_types.py +84 -0
  26. squirrels/_exceptions.py +29 -0
  27. squirrels/_initializer.py +177 -80
  28. squirrels/_logging.py +115 -0
  29. squirrels/_manifest.py +208 -79
  30. squirrels/_model_builder.py +69 -0
  31. squirrels/_model_configs.py +74 -0
  32. squirrels/_model_queries.py +52 -0
  33. squirrels/_models.py +926 -367
  34. squirrels/_package_data/base_project/.env +42 -0
  35. squirrels/_package_data/base_project/.env.example +42 -0
  36. squirrels/_package_data/base_project/assets/expenses.db +0 -0
  37. squirrels/_package_data/base_project/connections.yml +16 -0
  38. squirrels/_package_data/base_project/dashboards/dashboard_example.py +34 -0
  39. squirrels/_package_data/base_project/dashboards/dashboard_example.yml +22 -0
  40. squirrels/{package_data → _package_data}/base_project/docker/.dockerignore +5 -2
  41. squirrels/{package_data → _package_data}/base_project/docker/Dockerfile +3 -3
  42. squirrels/{package_data → _package_data}/base_project/docker/compose.yml +1 -1
  43. squirrels/_package_data/base_project/duckdb_init.sql +10 -0
  44. squirrels/{package_data/base_project/.gitignore → _package_data/base_project/gitignore} +3 -2
  45. squirrels/_package_data/base_project/macros/macros_example.sql +17 -0
  46. squirrels/_package_data/base_project/models/builds/build_example.py +26 -0
  47. squirrels/_package_data/base_project/models/builds/build_example.sql +16 -0
  48. squirrels/_package_data/base_project/models/builds/build_example.yml +57 -0
  49. squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +12 -0
  50. squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +26 -0
  51. squirrels/_package_data/base_project/models/federates/federate_example.py +37 -0
  52. squirrels/_package_data/base_project/models/federates/federate_example.sql +19 -0
  53. squirrels/_package_data/base_project/models/federates/federate_example.yml +65 -0
  54. squirrels/_package_data/base_project/models/sources.yml +38 -0
  55. squirrels/{package_data → _package_data}/base_project/parameters.yml +56 -40
  56. squirrels/_package_data/base_project/pyconfigs/connections.py +14 -0
  57. squirrels/{package_data → _package_data}/base_project/pyconfigs/context.py +21 -40
  58. squirrels/_package_data/base_project/pyconfigs/parameters.py +141 -0
  59. squirrels/_package_data/base_project/pyconfigs/user.py +44 -0
  60. squirrels/_package_data/base_project/seeds/seed_categories.yml +15 -0
  61. squirrels/_package_data/base_project/seeds/seed_subcategories.csv +15 -0
  62. squirrels/_package_data/base_project/seeds/seed_subcategories.yml +21 -0
  63. squirrels/_package_data/base_project/squirrels.yml.j2 +61 -0
  64. squirrels/_package_data/templates/dataset_results.html +112 -0
  65. squirrels/_package_data/templates/oauth_login.html +271 -0
  66. squirrels/_package_data/templates/squirrels_studio.html +20 -0
  67. squirrels/_package_loader.py +8 -4
  68. squirrels/_parameter_configs.py +104 -103
  69. squirrels/_parameter_options.py +348 -0
  70. squirrels/_parameter_sets.py +57 -47
  71. squirrels/_parameters.py +1664 -0
  72. squirrels/_project.py +721 -0
  73. squirrels/_py_module.py +7 -5
  74. squirrels/_schemas/__init__.py +0 -0
  75. squirrels/_schemas/auth_models.py +167 -0
  76. squirrels/_schemas/query_param_models.py +75 -0
  77. squirrels/{_api_response_models.py → _schemas/response_models.py} +126 -47
  78. squirrels/_seeds.py +35 -16
  79. squirrels/_sources.py +110 -0
  80. squirrels/_utils.py +248 -73
  81. squirrels/_version.py +1 -1
  82. squirrels/arguments.py +7 -0
  83. squirrels/auth.py +4 -0
  84. squirrels/connections.py +3 -0
  85. squirrels/dashboards.py +2 -81
  86. squirrels/data_sources.py +14 -631
  87. squirrels/parameter_options.py +13 -348
  88. squirrels/parameters.py +14 -1266
  89. squirrels/types.py +16 -0
  90. squirrels-0.5.0.dist-info/METADATA +113 -0
  91. squirrels-0.5.0.dist-info/RECORD +97 -0
  92. {squirrels-0.4.0.dist-info → squirrels-0.5.0.dist-info}/WHEEL +1 -1
  93. squirrels-0.5.0.dist-info/entry_points.txt +3 -0
  94. {squirrels-0.4.0.dist-info → squirrels-0.5.0.dist-info/licenses}/LICENSE +1 -1
  95. squirrels/_authenticator.py +0 -85
  96. squirrels/_dashboards_io.py +0 -61
  97. squirrels/_environcfg.py +0 -84
  98. squirrels/arguments/init_time_args.py +0 -40
  99. squirrels/arguments/run_time_args.py +0 -208
  100. squirrels/package_data/assets/favicon.ico +0 -0
  101. squirrels/package_data/assets/index.css +0 -1
  102. squirrels/package_data/assets/index.js +0 -58
  103. squirrels/package_data/base_project/assets/expenses.db +0 -0
  104. squirrels/package_data/base_project/connections.yml +0 -7
  105. squirrels/package_data/base_project/dashboards/dashboard_example.py +0 -32
  106. squirrels/package_data/base_project/dashboards.yml +0 -10
  107. squirrels/package_data/base_project/env.yml +0 -29
  108. squirrels/package_data/base_project/models/dbviews/dbview_example.py +0 -47
  109. squirrels/package_data/base_project/models/dbviews/dbview_example.sql +0 -22
  110. squirrels/package_data/base_project/models/federates/federate_example.py +0 -21
  111. squirrels/package_data/base_project/models/federates/federate_example.sql +0 -3
  112. squirrels/package_data/base_project/pyconfigs/auth.py +0 -45
  113. squirrels/package_data/base_project/pyconfigs/connections.py +0 -19
  114. squirrels/package_data/base_project/pyconfigs/parameters.py +0 -95
  115. squirrels/package_data/base_project/seeds/seed_subcategories.csv +0 -15
  116. squirrels/package_data/base_project/squirrels.yml.j2 +0 -94
  117. squirrels/package_data/templates/index.html +0 -18
  118. squirrels/project.py +0 -378
  119. squirrels/user_base.py +0 -55
  120. squirrels-0.4.0.dist-info/METADATA +0 -117
  121. squirrels-0.4.0.dist-info/RECORD +0 -60
  122. squirrels-0.4.0.dist-info/entry_points.txt +0 -4
  123. /squirrels/{package_data → _package_data}/base_project/assets/weather.db +0 -0
  124. /squirrels/{package_data → _package_data}/base_project/seeds/seed_categories.csv +0 -0
  125. /squirrels/{package_data → _package_data}/base_project/tmp/.gitignore +0 -0
squirrels/_initializer.py CHANGED
@@ -1,124 +1,181 @@
1
1
  from typing import Optional
2
2
  from datetime import datetime
3
- import inquirer, os, shutil
3
+ from pathlib import Path
4
+ import inquirer, os, shutil, secrets
4
5
 
5
6
  from . import _constants as c, _utils as u
6
7
 
7
- base_proj_dir = u.Path(os.path.dirname(__file__), c.PACKAGE_DATA_FOLDER, c.BASE_PROJECT_FOLDER)
8
+ base_proj_dir = Path(os.path.dirname(__file__), c.PACKAGE_DATA_FOLDER, c.BASE_PROJECT_FOLDER)
9
+
10
+ TMP_FOLDER = "tmp"
8
11
 
9
12
 
10
13
  class Initializer:
11
- def __init__(self, *, overwrite: bool = False):
12
- self.overwrite = overwrite
14
+ def __init__(self, *, project_name: Optional[str] = None, use_curr_dir: bool = False):
15
+ self.project_name = project_name if not use_curr_dir else None
16
+ self.use_curr_dir = use_curr_dir
13
17
 
14
- def _path_exists(self, filepath: u.Path) -> bool:
18
+ def _path_exists(self, filepath: Path) -> bool:
15
19
  return os.path.exists(filepath)
16
20
 
17
- def _files_have_same_content(self, file1: u.Path, file2: u.Path) -> bool:
21
+ def _files_have_same_content(self, file1: Path, file2: Path) -> bool:
18
22
  with open(file1, 'rb') as f1, open(file2, 'rb') as f2:
19
23
  return f1.read() == f2.read()
20
24
 
21
- def _add_timestamp_to_filename(self, path: u.Path) -> u.Path:
25
+ def _add_timestamp_to_filename(self, path: Path) -> Path:
22
26
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
23
27
  new_filename = f"{path.stem}_{timestamp}{path.suffix}"
24
28
  return path.with_name(new_filename)
25
29
 
26
- def _copy_file(self, filepath: u.Path, *, src_folder: str = ""):
27
- src_path = u.Path(base_proj_dir, src_folder, filepath)
30
+ def _copy_file(self, filepath: Path, *, src_folder: str = "", src_file: Path | None = None):
31
+ src_file = src_file if src_file is not None else filepath
32
+ src_path = Path(base_proj_dir, src_folder, src_file)
28
33
 
29
- dest_dir = os.path.dirname(filepath)
34
+ filepath2 = Path(self.project_name, filepath) if self.project_name else filepath
35
+ dest_dir = os.path.dirname(filepath2)
30
36
  if dest_dir != "":
31
37
  os.makedirs(dest_dir, exist_ok=True)
32
38
 
33
39
  perform_copy = True
34
- if self._path_exists(filepath):
35
- old_filepath = filepath
36
- if self._files_have_same_content(src_path, filepath):
40
+ if self._path_exists(filepath2):
41
+ old_filepath = filepath2
42
+ if self._files_have_same_content(src_path, filepath2):
37
43
  perform_copy = False
38
44
  extra_msg = "Skipping... file contents is same as source"
39
- elif self.overwrite:
40
- extra_msg = "Overwriting file..."
41
45
  else:
42
- filepath = self._add_timestamp_to_filename(old_filepath)
43
- extra_msg = f'Creating file as "{filepath}" instead...'
46
+ filepath2 = self._add_timestamp_to_filename(old_filepath)
47
+ extra_msg = f'Creating file as "{filepath2}" instead...'
44
48
  print(f'File "{old_filepath}" already exists.', extra_msg)
45
49
  else:
46
- print(f'Creating file "{filepath}"...')
50
+ print(f'Creating file "{filepath2}"...')
47
51
 
48
52
  if perform_copy:
49
- shutil.copy(src_path, filepath)
53
+ shutil.copy(src_path, filepath2)
54
+
55
+ def _copy_macros_file(self, filepath: str):
56
+ self._copy_file(Path(c.MACROS_FOLDER, filepath))
57
+
58
+ def _copy_models_file(self, filepath: str):
59
+ self._copy_file(Path(c.MODELS_FOLDER, filepath))
60
+
61
+ def _copy_build_file(self, filepath: str):
62
+ self._copy_file(Path(c.MODELS_FOLDER, c.BUILDS_FOLDER, filepath))
50
63
 
51
64
  def _copy_dbview_file(self, filepath: str):
52
- self._copy_file(u.Path(c.MODELS_FOLDER, c.DBVIEWS_FOLDER, filepath))
65
+ self._copy_file(Path(c.MODELS_FOLDER, c.DBVIEWS_FOLDER, filepath))
53
66
 
54
67
  def _copy_federate_file(self, filepath: str):
55
- self._copy_file(u.Path(c.MODELS_FOLDER, c.FEDERATES_FOLDER, filepath))
68
+ self._copy_file(Path(c.MODELS_FOLDER, c.FEDERATES_FOLDER, filepath))
56
69
 
57
70
  def _copy_database_file(self, filepath: str):
58
- self._copy_file(u.Path(c.DATABASE_FOLDER, filepath))
71
+ self._copy_file(Path(c.DATABASE_FOLDER, filepath))
59
72
 
60
73
  def _copy_pyconfig_file(self, filepath: str):
61
- self._copy_file(u.Path(c.PYCONFIGS_FOLDER, filepath))
74
+ self._copy_file(Path(c.PYCONFIGS_FOLDER, filepath))
62
75
 
63
76
  def _copy_seed_file(self, filepath: str):
64
- self._copy_file(u.Path(c.SEEDS_FOLDER, filepath))
77
+ self._copy_file(Path(c.SEEDS_FOLDER, filepath))
65
78
 
66
79
  def _copy_dashboard_file(self, filepath: str):
67
- self._copy_file(u.Path(c.DASHBOARDS_FOLDER, filepath))
68
-
69
- def _create_manifest_file(self, has_connections: bool, has_parameters: bool, has_dashboards: bool):
70
- TMP_FOLDER = "tmp"
80
+ self._copy_file(Path(c.DASHBOARDS_FOLDER, filepath))
71
81
 
82
+ def _create_manifest_file(self, has_connections: bool, has_parameters: bool):
72
83
  def get_content(file_name: Optional[str]) -> str:
73
84
  if file_name is None:
74
85
  return ""
75
86
 
76
- yaml_path = u.Path(base_proj_dir, file_name)
77
- return u.read_file(yaml_path)
87
+ yaml_path = Path(base_proj_dir, file_name)
88
+ return yaml_path.read_text()
78
89
 
79
90
  file_name_dict = {
80
91
  "parameters": c.PARAMETERS_YML_FILE if has_parameters else None,
81
92
  "connections": c.CONNECTIONS_YML_FILE if has_connections else None,
82
- "dashboards": c.DASHBOARDS_YML_FILE if has_dashboards else None
83
93
  }
84
94
  substitutions = {key: get_content(val) for key, val in file_name_dict.items()}
85
95
 
86
96
  manifest_template = get_content(c.MANIFEST_JINJA_FILE)
87
97
  manifest_content = u.render_string(manifest_template, **substitutions)
88
- output_path = u.Path(base_proj_dir, TMP_FOLDER, c.MANIFEST_FILE)
89
- with open(u.Path(output_path), "w") as f:
90
- f.write(manifest_content)
98
+ output_path = Path(base_proj_dir, TMP_FOLDER, c.MANIFEST_FILE)
99
+ output_path.write_text(manifest_content)
91
100
 
92
- self._copy_file(u.Path(c.MANIFEST_FILE), src_folder=TMP_FOLDER)
101
+ self._copy_file(Path(c.MANIFEST_FILE), src_folder=TMP_FOLDER)
102
+
103
+ def _copy_dotenv_files(self, admin_password: str | None = None):
104
+ substitutions = {
105
+ "random_secret_key": secrets.token_hex(32),
106
+ "random_admin_password": admin_password if admin_password else secrets.token_urlsafe(8),
107
+ }
108
+
109
+ dotenv_path = Path(base_proj_dir, c.DOTENV_FILE)
110
+ contents = u.render_string(dotenv_path.read_text(), **substitutions)
111
+
112
+ output_path = Path(base_proj_dir, TMP_FOLDER, c.DOTENV_FILE)
113
+ output_path.write_text(contents)
114
+
115
+ self._copy_file(Path(c.DOTENV_FILE), src_folder=TMP_FOLDER)
116
+ self._copy_file(Path(c.DOTENV_FILE + ".example"))
117
+
118
+ def _copy_gitignore_file(self):
119
+ self._copy_file(Path(c.GITIGNORE_FILE), src_file=Path("gitignore"))
93
120
 
94
121
  def init_project(self, args):
95
- options = ["core", "connections", "parameters", "dbview", "federate", "dashboard", "auth"]
96
- _, CONNECTIONS, PARAMETERS, DBVIEW, FEDERATE, DASHBOARD, AUTH = options
122
+ options = ["connections", "parameters", "build", "federate", "dashboard", "admin_password"]
123
+ CONNECTIONS, PARAMETERS, BUILD, FEDERATE, DASHBOARD, ADMIN_PASSWORD = options
97
124
 
98
- answers = { x: getattr(args, x) for x in options }
99
- if not any(answers.values()):
125
+ # Add project name prompt if not provided
126
+ if self.project_name is None and not args.curr_dir:
100
127
  questions = [
101
- inquirer.List(
102
- CONNECTIONS, message=f"How would you like to configure the database connections?", choices=c.CONF_FORMAT_CHOICES
103
- ),
104
- inquirer.List(
105
- PARAMETERS, message=f"How would you like to configure the parameters?", choices=c.CONF_FORMAT_CHOICES2
106
- ),
107
- inquirer.List(
108
- DBVIEW, message="What's the file format for the database view model?", choices=c.FILE_TYPE_CHOICES
109
- ),
110
- inquirer.List(
111
- FEDERATE, message="What's the file format for the federated model?", choices=c.FILE_TYPE_CHOICES
112
- ),
113
- inquirer.Confirm(
114
- DASHBOARD, message=f"Do you want to include a dashboard example?", default=False
115
- ),
116
- inquirer.Confirm(
117
- AUTH, message=f"Do you want to add the '{c.AUTH_FILE}' file to enable custom API authentication?", default=False
118
- ),
128
+ inquirer.Text('project_name', message="What is your project folder name? (leave blank to create in current directory)")
119
129
  ]
120
130
  answers = inquirer.prompt(questions)
121
131
  assert isinstance(answers, dict)
132
+ self.project_name = answers['project_name']
133
+
134
+ answers = { x: getattr(args, x) for x in options }
135
+ if answers.get(DASHBOARD) is not None:
136
+ answers[DASHBOARD] = (answers[DASHBOARD] == 'y') # convert 'y' or 'n' to boolean
137
+
138
+ if not args.use_defaults:
139
+ questions = []
140
+ if answers.get(CONNECTIONS) is None:
141
+ questions.append(
142
+ inquirer.List(
143
+ CONNECTIONS, message=f"How would you like to configure the database connections?", choices=c.CONF_FORMAT_CHOICES
144
+ ),
145
+ )
146
+ if answers.get(PARAMETERS) is None:
147
+ questions.append(
148
+ inquirer.List(
149
+ PARAMETERS, message=f"How would you like to configure the parameters?", choices=c.CONF_FORMAT_CHOICES2
150
+ ),
151
+ )
152
+ if answers.get(BUILD) is None:
153
+ questions.append(
154
+ inquirer.List(
155
+ BUILD, message="What's the file format for the build model?", choices=c.FILE_TYPE_CHOICES
156
+ ),
157
+ )
158
+ if answers.get(FEDERATE) is None:
159
+ questions.append(
160
+ inquirer.List(
161
+ FEDERATE, message="What's the file format for the federated model?", choices=c.FILE_TYPE_CHOICES
162
+ ),
163
+ )
164
+ if answers.get(DASHBOARD) is None:
165
+ questions.append(
166
+ inquirer.Confirm(
167
+ DASHBOARD, message=f"Do you want to include a dashboard example?", default=False
168
+ ),
169
+ )
170
+ if answers.get(ADMIN_PASSWORD) is None:
171
+ questions.append(
172
+ inquirer.Password(
173
+ "admin_password", message="What's the admin password? (leave blank to generate a random one)"
174
+ ),
175
+ )
176
+ more_answers = inquirer.prompt(questions)
177
+ assert isinstance(more_answers, dict)
178
+ answers.update(more_answers)
122
179
 
123
180
  def get_answer(key, default):
124
181
  """
@@ -133,6 +190,8 @@ class Initializer:
133
190
  answer = answers.get(key)
134
191
  return answer if answer is not None else default
135
192
 
193
+ admin_password = get_answer("admin_password", None)
194
+
136
195
  connections_format = get_answer(CONNECTIONS, c.YML_FORMAT)
137
196
  connections_use_yaml = (connections_format == c.YML_FORMAT)
138
197
  connections_use_py = (connections_format == c.PYTHON_FORMAT)
@@ -141,14 +200,19 @@ class Initializer:
141
200
  parameters_use_yaml = (parameters_format == c.YML_FORMAT)
142
201
  parameters_use_py = (parameters_format == c.PYTHON_FORMAT)
143
202
 
144
- dbview_format = get_answer(DBVIEW, c.SQL_FILE_TYPE)
145
- if dbview_format == c.SQL_FILE_TYPE:
146
- db_view_file = c.DBVIEW_FILE_STEM + ".sql"
147
- elif dbview_format == c.PYTHON_FILE_TYPE:
148
- db_view_file = c.DBVIEW_FILE_STEM + ".py"
203
+ build_config_file = c.BUILD_FILE_STEM + ".yml"
204
+ build_format = get_answer(BUILD, c.SQL_FILE_TYPE)
205
+ if build_format == c.SQL_FILE_TYPE:
206
+ build_file = c.BUILD_FILE_STEM + ".sql"
207
+ elif build_format == c.PYTHON_FILE_TYPE:
208
+ build_file = c.BUILD_FILE_STEM + ".py"
149
209
  else:
150
- raise NotImplementedError(f"Dbview model format '{dbview_format}' not supported")
210
+ raise NotImplementedError(f"Build model format '{build_format}' not supported")
211
+
212
+ db_view_config_file = c.DBVIEW_FILE_STEM + ".yml"
213
+ db_view_file = c.DBVIEW_FILE_STEM + ".sql"
151
214
 
215
+ federate_config_file = c.FEDERATE_FILE_STEM + ".yml"
152
216
  federate_format = get_answer(FEDERATE, c.SQL_FILE_TYPE)
153
217
  if federate_format == c.SQL_FILE_TYPE:
154
218
  federate_file = c.FEDERATE_FILE_STEM + ".sql"
@@ -159,10 +223,10 @@ class Initializer:
159
223
 
160
224
  dashboards_enabled = get_answer(DASHBOARD, False)
161
225
 
162
- self._create_manifest_file(connections_use_yaml, parameters_use_yaml, dashboards_enabled)
226
+ self._copy_dotenv_files(admin_password)
227
+ self._create_manifest_file(connections_use_yaml, parameters_use_yaml)
163
228
 
164
- self._copy_file(u.Path(".gitignore"))
165
- self._copy_file(u.Path(c.ENV_CONFIG_FILE))
229
+ self._copy_gitignore_file()
166
230
 
167
231
  if connections_use_py:
168
232
  self._copy_pyconfig_file(c.CONNECTIONS_FILE)
@@ -178,44 +242,77 @@ class Initializer:
178
242
  else:
179
243
  raise NotImplementedError(f"Format '{parameters_format}' not supported for configuring widget parameters")
180
244
 
245
+ self._copy_pyconfig_file(c.USER_FILE)
246
+
181
247
  self._copy_pyconfig_file(c.CONTEXT_FILE)
182
- self._copy_seed_file(c.CATEGORY_SEED_FILE)
183
- self._copy_seed_file(c.SUBCATEGORY_SEED_FILE)
248
+ self._copy_seed_file(c.SEED_CATEGORY_FILE_STEM + ".csv")
249
+ self._copy_seed_file(c.SEED_CATEGORY_FILE_STEM + ".yml")
250
+ self._copy_seed_file(c.SEED_SUBCATEGORY_FILE_STEM + ".csv")
251
+ self._copy_seed_file(c.SEED_SUBCATEGORY_FILE_STEM + ".yml")
184
252
 
253
+ self._copy_macros_file(c.MACROS_FILE)
254
+
255
+ self._copy_models_file(c.SOURCES_FILE)
256
+ self._copy_build_file(build_file)
257
+ self._copy_build_file(build_config_file)
185
258
  self._copy_dbview_file(db_view_file)
259
+ self._copy_dbview_file(db_view_config_file)
186
260
  self._copy_federate_file(federate_file)
261
+ self._copy_federate_file(federate_config_file)
187
262
 
188
263
  if dashboards_enabled:
189
264
  self._copy_dashboard_file(c.DASHBOARD_FILE_STEM + ".py")
265
+ self._copy_dashboard_file(c.DASHBOARD_FILE_STEM + ".yml")
190
266
 
191
- if get_answer(AUTH, False):
192
- self._copy_pyconfig_file(c.AUTH_FILE)
193
-
194
267
  self._copy_database_file(c.EXPENSES_DB)
195
268
 
196
- print(f"\nSuccessfully created new Squirrels project in current directory!\n")
269
+ print(f"\nSuccessfully created new Squirrels project!\n")
197
270
 
198
271
  def get_file(self, args):
199
- if args.file_name == c.ENV_CONFIG_FILE:
200
- self._copy_file(u.Path(c.ENV_CONFIG_FILE))
201
- print("PLEASE ENSURE THE FILE IS INCLUDED IN .gitignore")
272
+ if args.file_name == c.DOTENV_FILE:
273
+ self._copy_dotenv_files()
274
+ print(f"A random admin password was generated for your project. You can change it in the new {c.DOTENV_FILE} file.")
275
+ print()
276
+ print(f"IMPORTANT: Please ensure the {c.DOTENV_FILE} file is added to your {c.GITIGNORE_FILE} file.")
277
+ print(f"You may also run `sqrl get-file {c.GITIGNORE_FILE}` to add a sample {c.GITIGNORE_FILE} file to your project.")
278
+ print()
279
+ elif args.file_name == c.GITIGNORE_FILE:
280
+ self._copy_gitignore_file()
202
281
  elif args.file_name == c.MANIFEST_FILE:
203
- self._create_manifest_file(not args.no_connections, args.parameters, args.dashboards)
204
- elif args.file_name in (c.AUTH_FILE, c.CONNECTIONS_FILE, c.PARAMETERS_FILE, c.CONTEXT_FILE):
282
+ self._create_manifest_file(not args.no_connections, args.parameters)
283
+ elif args.file_name in (c.USER_FILE, c.CONNECTIONS_FILE, c.PARAMETERS_FILE, c.CONTEXT_FILE):
205
284
  self._copy_pyconfig_file(args.file_name)
206
- elif args.file_name in (c.DBVIEW_FILE_STEM, c.FEDERATE_FILE_STEM):
207
- if args.format == c.SQL_FILE_TYPE:
285
+ elif args.file_name == c.MACROS_FILE:
286
+ self._copy_macros_file(args.file_name)
287
+ elif args.file_name == c.SOURCES_FILE:
288
+ self._copy_models_file(args.file_name)
289
+ elif args.file_name in (c.BUILD_FILE_STEM, c.DBVIEW_FILE_STEM, c.FEDERATE_FILE_STEM):
290
+ if args.file_name == c.DBVIEW_FILE_STEM or args.format == c.SQL_FILE_TYPE:
208
291
  extension = ".sql"
209
292
  elif args.format == c.PYTHON_FILE_TYPE:
210
293
  extension = ".py"
211
294
  else:
212
295
  raise NotImplementedError(f"Format '{args.format}' not supported for {args.file_name}")
213
- copy_method = self._copy_dbview_file if args.file_name == c.DBVIEW_FILE_STEM else self._copy_federate_file
296
+
297
+ if args.file_name == c.BUILD_FILE_STEM:
298
+ copy_method = self._copy_build_file
299
+ elif args.file_name == c.DBVIEW_FILE_STEM:
300
+ copy_method = self._copy_dbview_file
301
+ elif args.file_name == c.FEDERATE_FILE_STEM:
302
+ copy_method = self._copy_federate_file
303
+ else:
304
+ raise NotImplementedError(f"File '{args.file_name}' not supported")
305
+
214
306
  copy_method(args.file_name + extension)
307
+ copy_method(args.file_name + ".yml")
215
308
  elif args.file_name == c.DASHBOARD_FILE_STEM:
216
309
  self._copy_dashboard_file(args.file_name + ".py")
310
+ self._copy_dashboard_file(args.file_name + ".yml")
217
311
  elif args.file_name in (c.EXPENSES_DB, c.WEATHER_DB):
218
312
  self._copy_database_file(args.file_name)
313
+ elif args.file_name in (c.SEED_CATEGORY_FILE_STEM, c.SEED_SUBCATEGORY_FILE_STEM):
314
+ self._copy_seed_file(args.file_name + ".csv")
315
+ self._copy_seed_file(args.file_name + ".yml")
219
316
  else:
220
317
  raise NotImplementedError(f"File '{args.file_name}' not supported")
221
318
 
squirrels/_logging.py ADDED
@@ -0,0 +1,115 @@
1
+ from pathlib import Path
2
+ from logging.handlers import RotatingFileHandler
3
+ from uuid import uuid4
4
+ import logging as l, json
5
+
6
+ from . import _constants as c, _utils as u
7
+
8
+
9
+ class _BaseFormatter(l.Formatter):
10
+ def _format_helper(self, level_for_print: str, record: l.LogRecord) -> str:
11
+ # Save original levelname
12
+ original_levelname = record.levelname
13
+
14
+ # Add padding to the levelname for printing
15
+ visible_length = len(record.levelname) + 1
16
+ padding_needed = max(1, 9 - visible_length)
17
+ padded_level = f"{level_for_print}:{' ' * padding_needed}"
18
+ record.levelname = padded_level
19
+
20
+ # Format the message
21
+ formatted = super().format(record)
22
+
23
+ # Restore original levelname
24
+ record.levelname = original_levelname
25
+
26
+ return formatted
27
+
28
+
29
+ class _ColoredFormatter(_BaseFormatter):
30
+ """Custom formatter that adds colors to log levels for terminal output"""
31
+
32
+ # ANSI color codes
33
+ COLORS = {
34
+ 'DEBUG': '\033[36m', # Cyan
35
+ 'INFO': '\033[32m', # Green
36
+ 'WARNING': '\033[33m', # Yellow
37
+ 'ERROR': '\033[31m', # Red
38
+ 'CRITICAL': '\033[35m', # Magenta
39
+ }
40
+ RESET = '\033[0m'
41
+ BOLD = '\033[1m'
42
+
43
+ def format(self, record: l.LogRecord) -> str:
44
+ # Add color to levelname with colon and padding
45
+ color = self.COLORS.get(record.levelname, '')
46
+ colored_level = f"{color}{record.levelname}{self.RESET}"
47
+ return self._format_helper(colored_level, record)
48
+
49
+
50
+ class _PlainFormatter(_BaseFormatter):
51
+ """Custom formatter that adds colon to log levels for file output"""
52
+
53
+ def format(self, record: l.LogRecord) -> str:
54
+ return self._format_helper(record.levelname, record)
55
+
56
+
57
+ class _CustomJsonFormatter(l.Formatter):
58
+ def format(self, record: l.LogRecord) -> str:
59
+ super().format(record)
60
+ info = {
61
+ "timestamp": self.formatTime(record),
62
+ "project_id": record.name,
63
+ "level": record.levelname,
64
+ "message": record.getMessage(),
65
+ "thread": record.thread,
66
+ "thread_name": record.threadName,
67
+ "process": record.process,
68
+ **record.__dict__.get("info", {})
69
+ }
70
+ output = {
71
+ "data": record.__dict__.get("data", {}),
72
+ "info": info
73
+ }
74
+ return json.dumps(output)
75
+
76
+
77
+ def get_logger(
78
+ base_path: str, log_to_file: bool, log_level: str, log_format: str, log_file_size_mb: int, log_file_backup_count: int
79
+ ) -> u.Logger:
80
+ logger = u.Logger(name=uuid4().hex)
81
+ logger.setLevel(log_level.upper())
82
+
83
+ # Determine the formatter based on log_format
84
+ if log_format.lower() == "json":
85
+ stdout_formatter = _CustomJsonFormatter()
86
+ file_formatter = _CustomJsonFormatter()
87
+ elif log_format.lower() == "text":
88
+ # Use colored formatter for stdout, plain formatter with colon for file
89
+ format_string = "%(levelname)s [%(asctime)s] %(message)s"
90
+ stdout_formatter = _ColoredFormatter(format_string, datefmt="%Y-%m-%d %H:%M:%S")
91
+ file_formatter = _PlainFormatter(format_string, datefmt="%Y-%m-%d %H:%M:%S")
92
+ else:
93
+ raise ValueError("log_format must be either 'text' or 'json'")
94
+
95
+ if log_to_file:
96
+ log_file_path = Path(base_path, c.LOGS_FOLDER, c.LOGS_FILE)
97
+ log_file_path.parent.mkdir(parents=True, exist_ok=True)
98
+
99
+ # Rotating file handler
100
+ file_handler = RotatingFileHandler(
101
+ log_file_path,
102
+ maxBytes=log_file_size_mb * 1024 * 1024,
103
+ backupCount=log_file_backup_count
104
+ )
105
+ file_handler.setLevel(log_level.upper())
106
+ file_handler.setFormatter(file_formatter)
107
+ logger.addHandler(file_handler)
108
+
109
+ else:
110
+ stdout_handler = l.StreamHandler()
111
+ stdout_handler.setLevel(log_level.upper())
112
+ stdout_handler.setFormatter(stdout_formatter)
113
+ logger.addHandler(stdout_handler)
114
+
115
+ return logger