squirrels 0.3.3__py3-none-any.whl → 0.4.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 (56) hide show
  1. squirrels/__init__.py +7 -3
  2. squirrels/_api_response_models.py +96 -72
  3. squirrels/_api_server.py +375 -201
  4. squirrels/_authenticator.py +23 -22
  5. squirrels/_command_line.py +70 -46
  6. squirrels/_connection_set.py +23 -25
  7. squirrels/_constants.py +29 -78
  8. squirrels/_dashboards_io.py +61 -0
  9. squirrels/_environcfg.py +53 -50
  10. squirrels/_initializer.py +184 -141
  11. squirrels/_manifest.py +168 -195
  12. squirrels/_models.py +159 -292
  13. squirrels/_package_loader.py +7 -8
  14. squirrels/_parameter_configs.py +173 -141
  15. squirrels/_parameter_sets.py +49 -38
  16. squirrels/_py_module.py +7 -7
  17. squirrels/_seeds.py +13 -12
  18. squirrels/_utils.py +114 -54
  19. squirrels/_version.py +1 -1
  20. squirrels/arguments/init_time_args.py +16 -10
  21. squirrels/arguments/run_time_args.py +89 -24
  22. squirrels/dashboards.py +82 -0
  23. squirrels/data_sources.py +212 -232
  24. squirrels/dateutils.py +29 -26
  25. squirrels/package_data/assets/index.css +1 -1
  26. squirrels/package_data/assets/index.js +27 -18
  27. squirrels/package_data/base_project/.gitignore +2 -2
  28. squirrels/package_data/base_project/connections.yml +1 -1
  29. squirrels/package_data/base_project/dashboards/dashboard_example.py +32 -0
  30. squirrels/package_data/base_project/dashboards.yml +10 -0
  31. squirrels/package_data/base_project/docker/.dockerignore +9 -4
  32. squirrels/package_data/base_project/docker/Dockerfile +7 -6
  33. squirrels/package_data/base_project/docker/compose.yml +1 -1
  34. squirrels/package_data/base_project/env.yml +2 -2
  35. squirrels/package_data/base_project/models/dbviews/{database_view1.py → dbview_example.py} +2 -1
  36. squirrels/package_data/base_project/models/dbviews/{database_view1.sql → dbview_example.sql} +3 -2
  37. squirrels/package_data/base_project/models/federates/{dataset_example.py → federate_example.py} +6 -6
  38. squirrels/package_data/base_project/models/federates/{dataset_example.sql → federate_example.sql} +1 -1
  39. squirrels/package_data/base_project/parameters.yml +6 -4
  40. squirrels/package_data/base_project/pyconfigs/auth.py +1 -1
  41. squirrels/package_data/base_project/pyconfigs/connections.py +1 -1
  42. squirrels/package_data/base_project/pyconfigs/context.py +38 -10
  43. squirrels/package_data/base_project/pyconfigs/parameters.py +15 -7
  44. squirrels/package_data/base_project/squirrels.yml.j2 +14 -7
  45. squirrels/package_data/templates/index.html +3 -3
  46. squirrels/parameter_options.py +103 -106
  47. squirrels/parameters.py +347 -195
  48. squirrels/project.py +378 -0
  49. squirrels/user_base.py +14 -6
  50. {squirrels-0.3.3.dist-info → squirrels-0.4.0.dist-info}/METADATA +9 -21
  51. squirrels-0.4.0.dist-info/RECORD +60 -0
  52. squirrels/_timer.py +0 -23
  53. squirrels-0.3.3.dist-info/RECORD +0 -56
  54. {squirrels-0.3.3.dist-info → squirrels-0.4.0.dist-info}/LICENSE +0 -0
  55. {squirrels-0.3.3.dist-info → squirrels-0.4.0.dist-info}/WHEEL +0 -0
  56. {squirrels-0.3.3.dist-info → squirrels-0.4.0.dist-info}/entry_points.txt +0 -0
squirrels/project.py ADDED
@@ -0,0 +1,378 @@
1
+ import typing as _t, functools as _ft, asyncio as _aio, os as _os, shutil as _shutil, json as _json
2
+ import logging as _l, uuid as _uu, matplotlib.pyplot as _plt, networkx as _nx, pandas as _pd
3
+
4
+ from . import _utils as _u, _constants as _c, _environcfg as _ec, _manifest as _mf, _authenticator as _auth
5
+ from . import _seeds as _s, _connection_set as _cs, _models as _m, _dashboards_io as _d, _parameter_sets as _ps
6
+ from . import dashboards as _dash
7
+
8
+ T = _t.TypeVar('T', bound=_dash.Dashboard)
9
+
10
+
11
+ class _CustomJsonFormatter(_l.Formatter):
12
+ def format(self, record: _l.LogRecord) -> str:
13
+ super().format(record)
14
+ info = {
15
+ "timestamp": self.formatTime(record),
16
+ "project_id": record.name,
17
+ "level": record.levelname,
18
+ "message": record.getMessage(),
19
+ "thread": record.thread,
20
+ "thread_name": record.threadName,
21
+ "process": record.process,
22
+ **record.__dict__.get("info", {})
23
+ }
24
+ output = {
25
+ "data": record.__dict__.get("data", {}),
26
+ "info": info
27
+ }
28
+ return _json.dumps(output)
29
+
30
+
31
+ class SquirrelsProject:
32
+ """
33
+ Initiate an instance of this class to interact with a Squirrels project through Python code. For example this can be handy to experiment with the datasets produced by Squirrels in a Jupyter notebook.
34
+ """
35
+
36
+ def __init__(self, *, filepath: str = ".", log_file: str | None = _c.LOGS_FILE, log_level: str = "INFO", log_format: str = "text") -> None:
37
+ """
38
+ Constructor for SquirrelsProject class. Loads the file contents of the Squirrels project into memory as member fields.
39
+
40
+ Arguments:
41
+ filepath: The path to the Squirrels project file. Defaults to the current working directory.
42
+ log_level: The logging level to use. Options are "DEBUG", "INFO", and "WARNING". Default is "INFO".
43
+ log_file: The name of the log file to write to from the "logs/" subfolder. If None or empty string, then file logging is disabled. Default is "squirrels.log".
44
+ log_format: The format of the log records. Options are "text" and "json". Default is "text".
45
+ """
46
+ self._filepath = filepath
47
+ self._logger = self._get_logger(self._filepath, log_file, log_level, log_format)
48
+
49
+ def _get_logger(self, base_path: str, log_file: str | None, log_level: str, log_format: str) -> _u.Logger:
50
+ logger = _u.Logger(name=_uu.uuid4().hex)
51
+ logger.setLevel(log_level.upper())
52
+
53
+ if log_file:
54
+ path = _u.Path(base_path, _c.LOGS_FOLDER, log_file)
55
+ path.parent.mkdir(parents=True, exist_ok=True)
56
+
57
+ handler = _l.FileHandler(path)
58
+ if log_format.lower() == "json":
59
+ handler.setFormatter(_CustomJsonFormatter())
60
+ elif log_format.lower() == "text":
61
+ formatter = _l.Formatter("[%(name)s] %(asctime)s - %(levelname)s - %(message)s")
62
+ handler.setFormatter(formatter)
63
+ else:
64
+ raise ValueError("log_format must be either 'text' or 'json'")
65
+ logger.addHandler(handler)
66
+ else:
67
+ logger.disabled = True
68
+
69
+ return logger
70
+
71
+ @property
72
+ @_ft.cache
73
+ def _env_cfg(self) -> _ec.EnvironConfig:
74
+ return _ec.EnvironConfigIO.load_from_file(self._logger, self._filepath)
75
+
76
+ @property
77
+ @_ft.cache
78
+ def _manifest_cfg(self) -> _mf.ManifestConfig:
79
+ return _mf.ManifestIO.load_from_file(self._logger, self._filepath, self._env_cfg)
80
+
81
+ @property
82
+ @_ft.cache
83
+ def _seeds(self) -> _s.Seeds:
84
+ return _s.SeedsIO.load_files(self._logger, self._filepath, self._manifest_cfg)
85
+
86
+ @property
87
+ @_ft.cache
88
+ def _model_files(self) -> dict[str, _m.QueryFile]:
89
+ return _m.ModelsIO.load_files(self._logger, self._filepath)
90
+
91
+ @property
92
+ @_ft.cache
93
+ def _context_func(self) -> _m.ContextFunc:
94
+ return _m.ModelsIO.load_context_func(self._logger, self._filepath)
95
+
96
+ @property
97
+ @_ft.cache
98
+ def _dashboards(self) -> dict[str, _d.DashboardFunction]:
99
+ return _d.DashboardsIO.load_files(self._logger, self._filepath)
100
+
101
+ @property
102
+ @_ft.cache
103
+ def _conn_args(self) -> _cs.ConnectionsArgs:
104
+ return _cs.ConnectionSetIO.load_conn_py_args(self._logger, self._env_cfg, self._manifest_cfg)
105
+
106
+ @property
107
+ def _conn_set(self) -> _cs.ConnectionSet:
108
+ if not hasattr(self, "__conn_set") or self.__conn_set is None:
109
+ self.__conn_set = _cs.ConnectionSetIO.load_from_file(self._logger, self._filepath, self._manifest_cfg, self._conn_args)
110
+ return self.__conn_set
111
+
112
+ @property
113
+ @_ft.cache
114
+ def _authenticator(self) -> _auth.Authenticator:
115
+ token_expiry_minutes = self._manifest_cfg.settings.get(_c.AUTH_TOKEN_EXPIRE_SETTING, 30)
116
+ return _auth.Authenticator(self._filepath, self._env_cfg, self._conn_args, self._conn_set, token_expiry_minutes)
117
+
118
+ @property
119
+ @_ft.cache
120
+ def _param_args(self) -> _ps.ParametersArgs:
121
+ return _ps.ParameterConfigsSetIO.get_param_args(self._conn_args)
122
+
123
+ @property
124
+ @_ft.cache
125
+ def _param_cfg_set(self) -> _ps.ParameterConfigsSet:
126
+ return _ps.ParameterConfigsSetIO.load_from_file(
127
+ self._logger, self._filepath, self._manifest_cfg, self._seeds, self._conn_set, self._param_args
128
+ )
129
+
130
+ @property
131
+ @_ft.cache
132
+ def _j2_env(self) -> _u.EnvironmentWithMacros:
133
+ return _u.EnvironmentWithMacros(self._logger, loader=_u.j2.FileSystemLoader(self._filepath))
134
+
135
+ @property
136
+ @_ft.cache
137
+ def User(self) -> type[_auth.User]:
138
+ """
139
+ A direct reference to the User class in the `auth.py` file (if applicable). If `auth.py` does not exist, then this returns the `squirrels.User` class.
140
+ """
141
+ return self._authenticator.user_cls
142
+
143
+ def close(self) -> None:
144
+ """
145
+ Deliberately close any open resources within the Squirrels project, such as database connections (instead of relying on the garbage collector).
146
+ """
147
+ if hasattr(self, "__conn_set") and self.__conn_set is not None:
148
+ self.__conn_set.dispose()
149
+ self.__conn_set = None
150
+
151
+ def __exit__(self, exc_type, exc_val, traceback):
152
+ self.close()
153
+
154
+ def _generate_dag(self, dataset: str, *, target_model_name: str | None = None, always_pandas: bool = False) -> _m.DAG:
155
+ seeds_dict = self._seeds.get_dataframes()
156
+
157
+ models_dict: dict[str, _m.Referable] = {key: _m.Seed(key, df) for key, df in seeds_dict.items()}
158
+ for key, val in self._model_files.items():
159
+ models_dict[key] = _m.Model(key, val, self._manifest_cfg, self._conn_set, self._logger, j2_env=self._j2_env)
160
+ models_dict[key].needs_pandas = always_pandas
161
+
162
+ dataset_config = self._manifest_cfg.datasets[dataset]
163
+ target_model_name = dataset_config.model if target_model_name is None else target_model_name
164
+ target_model = models_dict[target_model_name]
165
+ target_model.is_target = True
166
+
167
+ return _m.DAG(self._manifest_cfg, dataset_config, target_model, models_dict, self._logger)
168
+
169
+ def _draw_dag(self, dag: _m.DAG, output_folder: _u.Path) -> None:
170
+ color_map = {_m.ModelType.SEED: "green", _m.ModelType.DBVIEW: "red", _m.ModelType.FEDERATE: "skyblue"}
171
+
172
+ G = dag.to_networkx_graph()
173
+
174
+ fig, _ = _plt.subplots()
175
+ pos = _nx.multipartite_layout(G, subset_key="layer")
176
+ colors = [color_map[node[1]] for node in G.nodes(data="model_type")] # type: ignore
177
+ _nx.draw(G, pos=pos, node_shape='^', node_size=1000, node_color=colors, arrowsize=20)
178
+
179
+ y_values = [val[1] for val in pos.values()]
180
+ scale = max(y_values) - min(y_values) if len(y_values) > 0 else 0
181
+ label_pos = {key: (val[0], val[1]-0.002-0.1*scale) for key, val in pos.items()}
182
+ _nx.draw_networkx_labels(G, pos=label_pos, font_size=8)
183
+
184
+ fig.tight_layout()
185
+ _plt.margins(x=0.1, y=0.1)
186
+ fig.savefig(_u.Path(output_folder, "dag.png"))
187
+ _plt.close(fig)
188
+
189
+ async def _write_dataset_outputs_given_test_set(
190
+ self, dataset: str, select: str, test_set: str | None, runquery: bool, recurse: bool
191
+ ) -> _t.Any | None:
192
+ dataset_conf = self._manifest_cfg.datasets[dataset]
193
+ default_test_set_conf = self._manifest_cfg.get_default_test_set(dataset)
194
+ if test_set in self._manifest_cfg.selection_test_sets:
195
+ test_set_conf = self._manifest_cfg.selection_test_sets[test_set]
196
+ elif test_set is None or test_set == default_test_set_conf.name:
197
+ test_set, test_set_conf = default_test_set_conf.name, default_test_set_conf
198
+ else:
199
+ raise _u.ConfigurationError(f"No test set named '{test_set}' was found when compiling dataset '{dataset}'. The test set must be defined if not default for dataset.")
200
+
201
+ error_msg_intro = f"Cannot compile dataset '{dataset}' with test set '{test_set}'."
202
+ if test_set_conf.datasets is not None and dataset not in test_set_conf.datasets:
203
+ raise _u.ConfigurationError(f"{error_msg_intro}\n Applicable datasets for test set '{test_set}' does not include dataset '{dataset}'.")
204
+
205
+ user_attributes = test_set_conf.user_attributes.copy()
206
+ selections = test_set_conf.parameters.copy()
207
+ username, is_internal = user_attributes.pop("username", ""), user_attributes.pop("is_internal", False)
208
+ if test_set_conf.is_authenticated:
209
+ user = self.User.Create(username, is_internal=is_internal, **user_attributes)
210
+ elif dataset_conf.scope == _mf.DatasetScope.PUBLIC:
211
+ user = None
212
+ else:
213
+ raise _u.ConfigurationError(f"{error_msg_intro}\n Non-public datasets require a test set with 'user_attributes' section defined")
214
+
215
+ if dataset_conf.scope == _mf.DatasetScope.PRIVATE and not is_internal:
216
+ raise _u.ConfigurationError(f"{error_msg_intro}\n Private datasets require a test set with user_attribute 'is_internal' set to true")
217
+
218
+ # always_pandas is set to True for creating CSV files from results (when runquery is True)
219
+ dag = self._generate_dag(dataset, target_model_name=select, always_pandas=True)
220
+ placeholders = await dag.execute(self._param_args, self._param_cfg_set, self._context_func, user, selections, runquery=runquery, recurse=recurse)
221
+
222
+ output_folder = _u.Path(self._filepath, _c.TARGET_FOLDER, _c.COMPILE_FOLDER, dataset, test_set)
223
+ if _os.path.exists(output_folder):
224
+ _shutil.rmtree(output_folder)
225
+ _os.makedirs(output_folder, exist_ok=True)
226
+
227
+ def write_placeholders() -> None:
228
+ output_filepath = _u.Path(output_folder, "placeholders.json")
229
+ with open(output_filepath, 'w') as f:
230
+ _json.dump(placeholders, f, indent=4)
231
+
232
+ def write_model_outputs(model: _m.Referable) -> None:
233
+ assert isinstance(model, _m.Model)
234
+ subfolder = _c.DBVIEWS_FOLDER if model.query_file.model_type == _m.ModelType.DBVIEW else _c.FEDERATES_FOLDER
235
+ subpath = _u.Path(output_folder, subfolder)
236
+ _os.makedirs(subpath, exist_ok=True)
237
+ if isinstance(model.compiled_query, _m.SqlModelQuery):
238
+ output_filepath = _u.Path(subpath, model.name+'.sql')
239
+ query = model.compiled_query.query
240
+ with open(output_filepath, 'w') as f:
241
+ f.write(query)
242
+ if runquery and isinstance(model.result, _pd.DataFrame):
243
+ output_filepath = _u.Path(subpath, model.name+'.csv')
244
+ model.result.to_csv(output_filepath, index=False)
245
+
246
+ write_placeholders()
247
+ all_model_names = dag.get_all_query_models()
248
+ coroutines = [_aio.to_thread(write_model_outputs, dag.models_dict[name]) for name in all_model_names]
249
+ await _aio.gather(*coroutines)
250
+
251
+ if recurse:
252
+ self._draw_dag(dag, output_folder)
253
+
254
+ if isinstance(dag.target_model, _m.Model) and dag.target_model.compiled_query is not None:
255
+ return dag.target_model.compiled_query.query # else return None
256
+
257
+ async def compile(
258
+ self, *, dataset: str | None = None, do_all_datasets: bool = False, selected_model: str | None = None, test_set: str | None = None,
259
+ do_all_test_sets: bool = False, runquery: bool = False
260
+ ) -> None:
261
+ """
262
+ Async method to compile the SQL templates into files in the "target/" folder. Same functionality as the "sqrl compile" CLI.
263
+
264
+ Although all arguments are "optional", the "dataset" argument is required if "do_all_datasets" argument is False.
265
+
266
+ Arguments:
267
+ dataset: The name of the dataset to compile. Ignored if "do_all_datasets" argument is True, but required (i.e., cannot be None) if "do_all_datasets" is False. Default is None.
268
+ do_all_datasets: If True, compile all datasets and ignore the "dataset" argument. Default is False.
269
+ selected_model: The name of the model to compile. If specified, the compiled SQL query is also printed in the terminal. If None, all models for the selected dataset are compiled. Default is None.
270
+ test_set: The name of the test set to compile with. If None, the default test set is used (which can vary by dataset). Ignored if `do_all_test_sets` argument is True. Default is None.
271
+ do_all_test_sets: Whether to compile all applicable test sets for the selected dataset(s). If True, the `test_set` argument is ignored. Default is False.
272
+ runquery**: Whether to run all compiled queries and save each result as a CSV file. If True and `selected_model` is specified, all upstream models of the selected model is compiled as well. Default is False.
273
+ """
274
+ recurse = True
275
+ if do_all_datasets:
276
+ selected_models = [(dataset.name, dataset.model) for dataset in self._manifest_cfg.datasets.values()]
277
+ else:
278
+ assert isinstance(dataset, str), "argument 'dataset' must be provided a string value if argument 'do_all_datasets' is False"
279
+ assert dataset in self._manifest_cfg.datasets, f"dataset '{dataset}' not found in {_c.MANIFEST_FILE}"
280
+ if selected_model is None:
281
+ selected_model = self._manifest_cfg.datasets[dataset].model
282
+ else:
283
+ recurse = False
284
+ selected_models = [(dataset, selected_model)]
285
+
286
+ coroutines: list[_t.Coroutine] = []
287
+ for dataset, selected_model in selected_models:
288
+ if do_all_test_sets:
289
+ for test_set_name in self._manifest_cfg.get_applicable_test_sets(dataset):
290
+ coroutine = self._write_dataset_outputs_given_test_set(dataset, selected_model, test_set_name, runquery, recurse)
291
+ coroutines.append(coroutine)
292
+
293
+ coroutine = self._write_dataset_outputs_given_test_set(dataset, selected_model, test_set, runquery, recurse)
294
+ coroutines.append(coroutine)
295
+
296
+ queries = await _aio.gather(*coroutines)
297
+
298
+ print(f"Compiled successfully! See the '{_c.TARGET_FOLDER}/' folder for results.")
299
+ print()
300
+ if not recurse and len(queries) == 1 and isinstance(queries[0], str):
301
+ print(queries[0])
302
+ print()
303
+
304
+ def _permission_error(self, user: _auth.User | None, data_type: str, data_name: str, scope: str) -> PermissionError:
305
+ username = None if user is None else user.username
306
+ return PermissionError(f"User '{username}' does not have permission to access {scope} {data_type}: {data_name}")
307
+
308
+ def seed(self, name: str) -> _pd.DataFrame:
309
+ """
310
+ Method to retrieve a seed as a pandas DataFrame given a seed name.
311
+
312
+ Arguments:
313
+ name: The name of the seed to retrieve
314
+
315
+ Returns:
316
+ The seed as a pandas DataFrame
317
+ """
318
+ seeds_dict = self._seeds.get_dataframes()
319
+ try:
320
+ return seeds_dict[name]
321
+ except KeyError:
322
+ available_seeds = list(seeds_dict.keys())
323
+ raise KeyError(f"Seed '{name}' not found. Available seeds are: {available_seeds}")
324
+
325
+ async def _dataset_helper(
326
+ self, name: str, selections: dict[str, _t.Any], user: _auth.User | None
327
+ ) -> _pd.DataFrame:
328
+ dag = self._generate_dag(name)
329
+ await dag.execute(self._param_args, self._param_cfg_set, self._context_func, user, dict(selections))
330
+ return _pd.DataFrame(dag.target_model.result)
331
+
332
+ async def dataset(
333
+ self, name: str, *, selections: dict[str, _t.Any] = {}, user: _auth.User | None = None
334
+ ) -> _pd.DataFrame:
335
+ """
336
+ Async method to retrieve a dataset as a pandas DataFrame given parameter selections.
337
+
338
+ Arguments:
339
+ name: The name of the dataset to retrieve.
340
+ selections: A dictionary of parameter selections to apply to the dataset. Optional, default is empty dictionary.
341
+ user: The user to use for authentication. If None, no user is used. Optional, default is None.
342
+
343
+ Returns:
344
+ A pandas DataFrame containing the dataset.
345
+ """
346
+ scope = self._manifest_cfg.datasets[name].scope
347
+ if not self._authenticator.can_user_access_scope(user, scope):
348
+ raise self._permission_error(user, "dataset", name, scope.name)
349
+ return await self._dataset_helper(name, selections, user)
350
+
351
+ async def dashboard(
352
+ self, name: str, *, selections: dict[str, _t.Any] = {}, user: _auth.User | None = None, dashboard_type: _t.Type[T] = _dash.Dashboard
353
+ ) -> T:
354
+ """
355
+ Async method to retrieve a dashboard given parameter selections.
356
+
357
+ Arguments:
358
+ name: The name of the dashboard to retrieve.
359
+ selections: A dictionary of parameter selections to apply to the dashboard. Optional, default is empty dictionary.
360
+ user: The user to use for authentication. If None, no user is used. Optional, default is None.
361
+ dashboard_type: Return type of the method (mainly used for type hints). For instance, provide PngDashboard if you want the return type to be a PngDashboard. Optional, default is squirrels.Dashboard.
362
+
363
+ Returns:
364
+ The dashboard type specified by the "dashboard_type" argument.
365
+ """
366
+ scope = self._manifest_cfg.dashboards[name].scope
367
+ if not self._authenticator.can_user_access_scope(user, scope):
368
+ raise self._permission_error(user, "dashboard", name, scope.name)
369
+
370
+ async def get_dataset(dataset_name: str, fixed_params: dict[str, _t.Any]) -> _pd.DataFrame:
371
+ final_selections = {**selections, **fixed_params}
372
+ return await self._dataset_helper(dataset_name, final_selections, user)
373
+
374
+ args = _d.DashboardArgs(self._param_args.proj_vars, self._param_args.env_vars, get_dataset)
375
+ try:
376
+ return await self._dashboards[name].get_dashboard(args, dashboard_type=dashboard_type)
377
+ except KeyError:
378
+ raise KeyError(f"No dashboard file found for: {name}")
squirrels/user_base.py CHANGED
@@ -1,8 +1,7 @@
1
- from typing import Any
2
- from dataclasses import dataclass
1
+ import typing as _t, dataclasses as _dc
3
2
 
4
3
 
5
- @dataclass
4
+ @_dc.dataclass
6
5
  class User:
7
6
  """
8
7
  Base class for extending the custom User model class
@@ -25,12 +24,21 @@ class User:
25
24
 
26
25
  @classmethod
27
26
  def Create(cls, username: str, *, is_internal: bool = False, **kwargs):
27
+ """
28
+ Creates an instance of the User class and calls the `set_attributes` method on the new instance.
29
+
30
+ We may overwrite the `set_attributes` method in `auth.py`. We do not overwrite the constructor to guarantee that `username` and `is_internal` are always set.
31
+
32
+ Arguments:
33
+ username: The identifier for the user
34
+ is_internal: Setting this to True lets the user access "private" datasets. Default is False
35
+ """
28
36
  user = cls(username, is_internal)
29
37
  user.set_attributes(**kwargs)
30
38
  return user
31
39
 
32
40
  @classmethod
33
- def _FromDict(cls, user_obj_as_dict: dict[str, Any]):
41
+ def _FromDict(cls, user_obj_as_dict: dict[str, _t.Any]):
34
42
  username, is_internal = user_obj_as_dict["username"], user_obj_as_dict["is_internal"]
35
43
  user = cls(username=username, is_internal=is_internal)
36
44
  for key, val in user_obj_as_dict.items():
@@ -38,10 +46,10 @@ class User:
38
46
  return user
39
47
 
40
48
 
41
- @dataclass
49
+ @_dc.dataclass
42
50
  class WrongPassword:
43
51
  """
44
52
  Return this object if the username was found but the password was incorrect
45
53
 
46
- This ensures that if the username exists as a real user, we won't continue to use the environcfg.yml file to authenticate
54
+ This ensures that if the username exists as a real user, we won't continue to use the env.yml file to authenticate
47
55
  """
@@ -1,16 +1,15 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: squirrels
3
- Version: 0.3.3
3
+ Version: 0.4.0
4
4
  Summary: Squirrels - API Framework for Data Analytics
5
5
  Home-page: https://squirrels-analytics.github.io
6
6
  License: Apache-2.0
7
7
  Author: Tim Huang
8
8
  Author-email: tim.yuting@hotmail.com
9
- Requires-Python: >=3.9,<4.0
9
+ Requires-Python: >=3.10,<4.0
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: License :: OSI Approved :: Apache Software License
12
12
  Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.9
14
13
  Classifier: Programming Language :: Python :: 3.10
15
14
  Classifier: Programming Language :: Python :: 3.11
16
15
  Classifier: Programming Language :: Python :: 3.12
@@ -27,6 +26,7 @@ Requires-Dist: jinja2 (>=3.1.3,<4.0.0)
27
26
  Requires-Dist: matplotlib (>=3.8.3,<4.0.0)
28
27
  Requires-Dist: networkx (>=3.2.1,<4.0.0)
29
28
  Requires-Dist: pandas (>=2.1.4,<3.0.0)
29
+ Requires-Dist: pydantic (>=2.8.2,<3.0.0)
30
30
  Requires-Dist: pyjwt (>=2.8.0,<3.0.0)
31
31
  Requires-Dist: python-multipart (>=0.0.9,<0.0.10)
32
32
  Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
@@ -77,19 +77,7 @@ The sections below describe how to set up your local environment for squirrels d
77
77
 
78
78
  ### Setup
79
79
 
80
- This project requires python version 3.9 or above to be installed. It also uses the python build tool `poetry` which can be installed as follows.
81
-
82
- **Linux, MacOS, Windows (WSL):**
83
-
84
- ```bash
85
- curl -sSL https://install.python-poetry.org | python3 -
86
- ```
87
-
88
- **Windows (Powershell):**
89
-
90
- ```bash
91
- (Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | py -
92
- ```
80
+ This project requires python version 3.10 or above to be installed. It also uses the python build tool `poetry`. Information on setting up poetry can be found at: https://python-poetry.org/docs/.
93
81
 
94
82
  Then, to install all dependencies, run:
95
83
 
@@ -106,10 +94,10 @@ poetry shell
106
94
  To confirm that the setup worked, run the following to show the help page for all squirrels CLI commands:
107
95
 
108
96
  ```bash
109
- squirrels -h
97
+ sqrl -h
110
98
  ```
111
99
 
112
- You can enter `exit` to exit the virtual environment shell. You can also run `poetry run squirrels -h` to run squirrels commands without activating the virtual environment.
100
+ You can enter `exit` to exit the virtual environment shell. You can also run `poetry run sqrl -h` to run squirrels commands without activating the virtual environment.
113
101
 
114
102
  ### Testing
115
103
 
@@ -121,9 +109,9 @@ From the root of the git repo, the source code can be found in the `squirrels` f
121
109
 
122
110
  To understand what a specific squirrels command is doing, start from the `_command_line.py` file as your entry point.
123
111
 
124
- The library version is maintained in both the `pyproject.toml` and the `squirrels/__init__.py` files.
112
+ The library version is maintained in both the `pyproject.toml` and the `squirrels/_version.py` files.
125
113
 
126
- When a user initializes a squirrels project using `squirrels init`, the files are copied from the `squirrels/package_data/base_project` folder. The contents in the `database` subfolder were constructed from the scripts in the `database_elt` folder.
114
+ When a user initializes a squirrels project using `sqrl init`, the files are copied from the `squirrels/package_data/base_project` folder. The contents in the `database` subfolder were constructed from the scripts in the `database_elt` folder.
127
115
 
128
- For the Squirrels UI activated by `squirrels run`, the HTML, CSS, and Javascript files can be found in the `static` and `templates` subfolders of `squirrels/package_data`.
116
+ For the Squirrels UI activated by `sqrl run`, the HTML, CSS, and Javascript files can be found in the `static` and `templates` subfolders of `squirrels/package_data`. The CSS and Javascript files are minified and built from the source files in this project: https://github.com/squirrels-analytics/squirrels-testing-ui.
129
117
 
@@ -0,0 +1,60 @@
1
+ squirrels/__init__.py,sha256=EbovxLpc9QcDooaME7887ur1lSjouWS36sealX_QOoo,946
2
+ squirrels/_api_response_models.py,sha256=dHATCC80lJyQNRHhnCbG1uMWWOinxPfkq7YIG4GqD1I,9786
3
+ squirrels/_api_server.py,sha256=GLn31Ki7gSxvYIOnVCp6wzZkuDT1MjHKnvhBEsasnB0,30902
4
+ squirrels/_authenticator.py,sha256=21BADaTQ-ZfnTZMXTfNYiZFntTdm8QfRsh7X8tFJ3mA,4038
5
+ squirrels/_command_line.py,sha256=UzD3B6JJY9rBzb2WBCNsOwUkBWaSFz0zZnsaHATp7qU,8115
6
+ squirrels/_connection_set.py,sha256=oH5qp6PZwkLANCUEKkjKASe80V8SLK98-ZBaPtbUCpk,2867
7
+ squirrels/_constants.py,sha256=k0KF2VIIvzeyfR1ycAwDR8uHZYfQJSBho90GbTRnvIM,2983
8
+ squirrels/_dashboards_io.py,sha256=Qf2ow2hI57NRGtNB0rxtXy7XoNVxkvKuAFnVRxslLqY,2417
9
+ squirrels/_environcfg.py,sha256=rAgDpJ4-h4yzFEbSG6gKh2XbQO3pAULB6Vo-66QAYp8,2875
10
+ squirrels/_initializer.py,sha256=OzPEnb3uN_lf4o5bCA_r9I9D3XHD2-v-27d6trEjMjE,9809
11
+ squirrels/_manifest.py,sha256=9DpDidPbKJ5GvXeW31fcg4nL7tRu8hqLmkBRbimep24,7626
12
+ squirrels/_models.py,sha256=1Th1Vl_a1ddxjAGHCwq5sqx7RQsi5wYyyNUpRYvTx18,23641
13
+ squirrels/_package_loader.py,sha256=DugsBkmm6OP8KPcJxmtuAMFnhf7t43rCRPjXXSwHXGs,1041
14
+ squirrels/_parameter_configs.py,sha256=NF7ChgkAVlsTFrL5QkhEbsIbohcSFfuu4IvhxDRao0s,24731
15
+ squirrels/_parameter_sets.py,sha256=YwyDp-8_VuKUPZ3oS9JWC9ynFlWy2sbiRDSXTGDBc4c,9702
16
+ squirrels/_py_module.py,sha256=w4AYGrks121_osRxL9F8hiX0ZuPXC6krnJB8cOqhWBI,2564
17
+ squirrels/_seeds.py,sha256=gLMlDphnwQKzSWLjpQBGYNPy5XtS0xu2PReqkk55na4,1462
18
+ squirrels/_utils.py,sha256=ykV3hjQTxG9-blmAPxCfd78bJXR1YfH_rQdUBsTaRJM,8829
19
+ squirrels/_version.py,sha256=vsjw-CsgmLFBE8OTwqlzxn_6L-jeLXsXgAlWcncGcJs,105
20
+ squirrels/arguments/init_time_args.py,sha256=Rc6n9vVZ7p4WpbyAvryG-tGb4vRQKrRyfRdzy3w5WTc,925
21
+ squirrels/arguments/run_time_args.py,sha256=Dk3H-iPlsy4i3swAVs8Uk6yyQPko7CEb9acfqHnzcvc,7031
22
+ squirrels/dashboards.py,sha256=NeLcBxfIOJV-hlBllQ4mB4sSHFxAF-odjyiioUF19z4,1978
23
+ squirrels/data_sources.py,sha256=qPtH5JFtCd8axjrXa40UlsZHU3geWTzxIkw3UAjj8lA,30019
24
+ squirrels/dateutils.py,sha256=dvbXf9-VEt8PhDnY3rd6kjqFgVkWPOhsfsq8cq9kHdE,16734
25
+ squirrels/package_data/assets/favicon.ico,sha256=FZx26dn50cp0rgYdyBptJJob2TTVNiY0NZ-MeeL_uY0,61022
26
+ squirrels/package_data/assets/index.css,sha256=BJVYY2dse9-vhASMsls6j5gpf33Z9tCzR1VyX1rWkH0,12181
27
+ squirrels/package_data/assets/index.js,sha256=TJDFw8B5OCGdvU9pFbf2BXKVOmtDNMTdSdCY4xK2QUM,265433
28
+ squirrels/package_data/base_project/.gitignore,sha256=x7VR530sbdKJU83mF9OGD8b-nI92MbWqUnMPyB0Xz4U,137
29
+ squirrels/package_data/base_project/assets/expenses.db,sha256=47ichjBqC25th7B0G5rz_yYJ4V3uQeF3cnttekQYa68,28672
30
+ squirrels/package_data/base_project/assets/weather.db,sha256=dsHPO36gQdZ4ULAA726Hg3jp8a1dCdig1DhrGg8wTeg,86016
31
+ squirrels/package_data/base_project/connections.yml,sha256=-rR6kjWl8FyHTzf2RnPqR1JEfhpX_pBrOJUxf5grRQY,259
32
+ squirrels/package_data/base_project/dashboards/dashboard_example.py,sha256=pAvxMdrKT_-7cFcRH7qqVIaQs1oFXCg5PEwiwCi0xdY,1781
33
+ squirrels/package_data/base_project/dashboards.yml,sha256=UEa9YI8ymi3tH15357oCWF5ZynNmyG2yu8B2F5Cr6d4,158
34
+ squirrels/package_data/base_project/docker/.dockerignore,sha256=0BJ26C2eyVJPwbW1e4cmBg1TPADio4tqakd3hdojsq8,176
35
+ squirrels/package_data/base_project/docker/Dockerfile,sha256=Sz4g9liLD40ci2iG2ohTjth3kvmNaCAo2LWZl6mCGR8,474
36
+ squirrels/package_data/base_project/docker/compose.yml,sha256=u-P7Zh7IRfYAcebx4zE119OWn2m_TugGMh1IgPDl_2g,111
37
+ squirrels/package_data/base_project/env.yml,sha256=5BUtjX8JEkVbtp7Xj-V-KcBkBJ6GEV1n05vasDSEYxw,943
38
+ squirrels/package_data/base_project/models/dbviews/dbview_example.py,sha256=Kx32kJFqmwJSu519nqls0cDuN8h_YnuyxAcD_eLeNWk,2036
39
+ squirrels/package_data/base_project/models/dbviews/dbview_example.sql,sha256=BJGTU25fdwCDXSEMFvXjn5Lnfi_Gu0wY2nir-d5VvUo,747
40
+ squirrels/package_data/base_project/models/federates/federate_example.py,sha256=rLPA8UTpQN_vbCQcID22QWfI1Mu7s_Xly76medWq7DQ,712
41
+ squirrels/package_data/base_project/models/federates/federate_example.sql,sha256=bHK1k8Q0uc2tZI21cw1y-w1w6Ah9dKRHRDwWVd80_Tk,75
42
+ squirrels/package_data/base_project/parameters.yml,sha256=UUZoip-_X9axROZ9v_j-CkrUS-0x5lI0Kq02cRAkym8,6397
43
+ squirrels/package_data/base_project/pyconfigs/auth.py,sha256=GyV84-lSfn0uGSP_kjvEFIHJCs3II3m97cYBYYYoQSc,1553
44
+ squirrels/package_data/base_project/pyconfigs/connections.py,sha256=yb46igO8qH64tYczf0SJDtqyqxq5CANutzfKMtNJ96k,753
45
+ squirrels/package_data/base_project/pyconfigs/context.py,sha256=tXQvdiHtESiHJup3yAkaKIuAEgFnV8H-Bz35VlqxIE0,3977
46
+ squirrels/package_data/base_project/pyconfigs/parameters.py,sha256=5liqj98vLUus0kQGo4ae29ncr6zUpopNTXPhcnE3E5M,4940
47
+ squirrels/package_data/base_project/seeds/seed_categories.csv,sha256=jppjf1nOIxy7-bi5lJn5CVqmnLfJHHq0ABgp6UqbXnw,104
48
+ squirrels/package_data/base_project/seeds/seed_subcategories.csv,sha256=aZkBJ6KioyYjEwRunYiA8ec0X1ygiEmLRVicJecFzfY,327
49
+ squirrels/package_data/base_project/squirrels.yml.j2,sha256=OhYjBjDLRtCWhSiAssslGrX5MsHQO8jC6HKz_NFnR_w,3076
50
+ squirrels/package_data/base_project/tmp/.gitignore,sha256=XImoqcWvJY0C0L_TWCx1ljvqU7qh9fUTJmK4ACCmNFI,13
51
+ squirrels/package_data/templates/index.html,sha256=41kyznAFLnE41IdPDCFdIvYLe7MgbIGrPOugk4YNXkQ,617
52
+ squirrels/parameter_options.py,sha256=cWYKNoBUopHq6VfaeBu-nN2V0_IY3OgYpmYhKODNCew,16956
53
+ squirrels/parameters.py,sha256=aSciLRxjY_-07VAv9mGxFQnhtjnuk4XT43MhZwVfoFY,56018
54
+ squirrels/project.py,sha256=LxQbSOjEWdq5ZCj2u-03qMX1WJqKBk1_GPb3nCmsU0U,19256
55
+ squirrels/user_base.py,sha256=gFtdi9K6RJovfKqRU-b-L1bOSdgNYQCQgfez3hz9EOo,1852
56
+ squirrels-0.4.0.dist-info/LICENSE,sha256=CItkBKs5m4J5jhkjXoW7IAnFyOC86x4hyOATpczUMmM,11338
57
+ squirrels-0.4.0.dist-info/METADATA,sha256=33BPbXUJCx3RAqa_yYWYAQw_g5APL2FaA7FdiQI_7rI,5217
58
+ squirrels-0.4.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
59
+ squirrels-0.4.0.dist-info/entry_points.txt,sha256=mYQRuGxbg8X82hjNRJuWiON4S6kE5CPvmXmxNPtYTbg,92
60
+ squirrels-0.4.0.dist-info/RECORD,,
squirrels/_timer.py DELETED
@@ -1,23 +0,0 @@
1
- from datetime import datetime
2
- import time
3
-
4
-
5
- class Timer:
6
- def __init__(self):
7
- self.verbose = False
8
-
9
- def _get_dt_from_timestamp(self, timestamp) -> str:
10
- return datetime.fromtimestamp(timestamp).strftime('%H:%M:%S.%f')
11
-
12
- def add_activity_time(self, activity: str, start_timestamp: float) -> None:
13
- if self.verbose:
14
- end_timestamp = time.time()
15
- time_taken = round((end_timestamp-start_timestamp) * 10**3, 3)
16
- print(f'Time taken for "{activity}": {time_taken}ms')
17
-
18
- start_datetime = self._get_dt_from_timestamp(start_timestamp)
19
- end_datetime = self._get_dt_from_timestamp(end_timestamp)
20
- print(f'--> start time: "{start_datetime}", end time: "{end_datetime}"')
21
- print()
22
-
23
- timer = Timer()
@@ -1,56 +0,0 @@
1
- squirrels/__init__.py,sha256=V22WYW_0xAygoGQF6QrHIURb5GbIstmjT3Cpb6D1eVU,816
2
- squirrels/_api_response_models.py,sha256=QxvdcH2nwUZ2N_2EAyYXoGyt5yOUs2YgdR_65ZHLgi4,3885
3
- squirrels/_api_server.py,sha256=BwtSB8wGNU3LYGQ4mqyYkhFypemSlBypM1TuQBHbJ-0,19296
4
- squirrels/_authenticator.py,sha256=I2ZSYMa2AgdusJsMTFWojD_2Nq-2o0vveUMGBXCG6Fg,3890
5
- squirrels/_command_line.py,sha256=CyOejIrSdRUAq7wuA5__T-n0fbsKPKZNuS0ZwmbZbO4,5955
6
- squirrels/_connection_set.py,sha256=5rNqyu4cl6Y-Ek4cs34CYvmp6tEiR_ZLUK_lDN9scXY,2747
7
- squirrels/_constants.py,sha256=nPZxeb_KXsBf8pxs7Eulv9ztrdk8CXLYdl9Sqa2-_so,4155
8
- squirrels/_environcfg.py,sha256=k1t58aQU-L1tb6VspKm_69Ss2StVpv5NyutJBvTb9M8,3184
9
- squirrels/_initializer.py,sha256=3xIAzqW3tUWFA8ty96fwWH_c3W7KqVf1N8HVDaVhklc,8140
10
- squirrels/_manifest.py,sha256=8wgvloJsGS7CvWgP9RVb22K5pzw5lOsKqc0q7aLLrtc,9328
11
- squirrels/_models.py,sha256=Ly_R6xz2-_6pU7xfmeH7S2_0hCS3BOSQsC1QEoiuwJQ,29511
12
- squirrels/_package_loader.py,sha256=-EZ8qsmp6QEdZyHUKYi5e4OPOx3EnL2rxr1IYe1NwEI,1020
13
- squirrels/_parameter_configs.py,sha256=TbX3A_Av93BXoz9ooguqEJ-bZnc2kvu7XubRViy4VvE,24404
14
- squirrels/_parameter_sets.py,sha256=NDiyJRpD9xld2UyUJdlMockt9QXVGAaPAK6SKtg5U7U,8823
15
- squirrels/_py_module.py,sha256=vgg_LkI0ODfCrbvbdFtnN4jCs0tT6YcaNLCB_d2Udw4,2533
16
- squirrels/_seeds.py,sha256=XXTPcm4LqlZ25P27ScCPGhFLft8jk7krOpjmfC4LwIg,1274
17
- squirrels/_timer.py,sha256=pFgz6dJKypmap07dfzZvwBS686DuBXwJWj0e6PnkWFU,802
18
- squirrels/_utils.py,sha256=gyDJp08pWbST2MhzNo_rTtUN6Otmjz6bSi-eibTNdq0,5354
19
- squirrels/_version.py,sha256=ySOetuKYm0mnm1J9-D3IcEZkf4kFrP8SVDKX97awBxo,109
20
- squirrels/arguments/init_time_args.py,sha256=w7bqlq9-gF85skxzxaVs8U6bk58TA39y7DlfXwUMbW0,783
21
- squirrels/arguments/run_time_args.py,sha256=J4J5-kFkdUHpdn46WoeaPTZTWbyMRjm6ppIjDG2RJxA,4903
22
- squirrels/data_sources.py,sha256=-WWwJ8TXA3Jcs-VQv1JIYZ9VV2Vudi5GTgrWghftBYs,30690
23
- squirrels/dateutils.py,sha256=aAdUeiBCIN13xvYddE-BLjKCbL0mh2vSgR7PW11syCk,16496
24
- squirrels/package_data/assets/favicon.ico,sha256=FZx26dn50cp0rgYdyBptJJob2TTVNiY0NZ-MeeL_uY0,61022
25
- squirrels/package_data/assets/index.css,sha256=-596X48WJj70MwI2gUH7SyHv9jzJcqZ7SuU6-4zhYJc,11832
26
- squirrels/package_data/assets/index.js,sha256=WwZbKCpfnxkifCJfGphfNJjGTcLY4-xCNKQKtrDG3Gw,261989
27
- squirrels/package_data/base_project/.gitignore,sha256=zfhneLPCfKkmpQy5Bn6vR7OsHiPavWgQgpo-cRm7kYY,142
28
- squirrels/package_data/base_project/assets/expenses.db,sha256=47ichjBqC25th7B0G5rz_yYJ4V3uQeF3cnttekQYa68,28672
29
- squirrels/package_data/base_project/assets/weather.db,sha256=dsHPO36gQdZ4ULAA726Hg3jp8a1dCdig1DhrGg8wTeg,86016
30
- squirrels/package_data/base_project/connections.yml,sha256=f7xAUF9QmiRZ7GnMEaqElzHrcs8QpNpGUKPB77t4brE,266
31
- squirrels/package_data/base_project/docker/.dockerignore,sha256=sY6T-_UtTVw1jFBOfiMQ8XFS9191YqEotxYPCEsdNJ4,90
32
- squirrels/package_data/base_project/docker/Dockerfile,sha256=VpanaF8xjeQrSmOyW1ZlbAzfCzvmGjFtgqy5-7ssNMg,484
33
- squirrels/package_data/base_project/docker/compose.yml,sha256=UbChIXPuBm9p_lCiYEE_0t-gCZ_k1WhF7scDv6iEMZ8,125
34
- squirrels/package_data/base_project/env.yml,sha256=qwQ287AedvH9oNMO9QDeqFl4iFy9F1-bNNqgeMAgAC0,931
35
- squirrels/package_data/base_project/models/dbviews/database_view1.py,sha256=vsXNwXkg1rBkMsaMQAReOTkaNKg-aKy4CK-qQkwdO08,1986
36
- squirrels/package_data/base_project/models/dbviews/database_view1.sql,sha256=8ZvEGgBZfT0Gxh35x1G1NmtC2fTW_AfdjxYew5LMWJ0,705
37
- squirrels/package_data/base_project/models/federates/dataset_example.py,sha256=tGMKw2gawxiUdWlJ2ICwapImpNR2V1uNvUz6cORA_GY,710
38
- squirrels/package_data/base_project/models/federates/dataset_example.sql,sha256=eD6-9nbF-AHg0RqYOx8AwT8K9vPQOs--9U-on3MD3zs,75
39
- squirrels/package_data/base_project/parameters.yml,sha256=ZftJmAg1_0m-mk-imQdkcCu5RCb3CZLgACEEmJhJtUc,6405
40
- squirrels/package_data/base_project/pyconfigs/auth.py,sha256=rDfOE6Srdwf18Tt6jv_-lGhCNC-JCcDbH4-4J8W9JHM,1560
41
- squirrels/package_data/base_project/pyconfigs/connections.py,sha256=XqhNnCkZ4af2BOv1DikraCMYoETAhT0o2Og-43ff22U,760
42
- squirrels/package_data/base_project/pyconfigs/context.py,sha256=i9qGd6i3T9RuI3TW6kuhs3jVdP6Tl-hgKxKbBaJyy14,3440
43
- squirrels/package_data/base_project/pyconfigs/parameters.py,sha256=9y4HqtMQThMwV89xCxR_KDgVVjpCe70HOOrjeoFCA8U,4485
44
- squirrels/package_data/base_project/seeds/seed_categories.csv,sha256=jppjf1nOIxy7-bi5lJn5CVqmnLfJHHq0ABgp6UqbXnw,104
45
- squirrels/package_data/base_project/seeds/seed_subcategories.csv,sha256=aZkBJ6KioyYjEwRunYiA8ec0X1ygiEmLRVicJecFzfY,327
46
- squirrels/package_data/base_project/squirrels.yml.j2,sha256=50RiV7wi6Im_6pmfO7ZZOmbLKM8gTwGDjTWZvQy9hUo,2864
47
- squirrels/package_data/base_project/tmp/.gitignore,sha256=XImoqcWvJY0C0L_TWCx1ljvqU7qh9fUTJmK4ACCmNFI,13
48
- squirrels/package_data/templates/index.html,sha256=zMiM1w77Rs6xcYXiGwgh_WksOXlMBmEjpcAbAfhAyDA,572
49
- squirrels/parameter_options.py,sha256=bNC-58xzlXnl_HQGLPVmuhvAmwMwi-MYb6Tw2ZefxcE,16611
50
- squirrels/parameters.py,sha256=XvXiYK96K9VR2t1HYiLSl1TBDhlD0gDKJSTqyv29OIs,48082
51
- squirrels/user_base.py,sha256=WbhLBldOe1CNthAR2Gyvuz_XT0wSslDEuHdw1NzOlyo,1395
52
- squirrels-0.3.3.dist-info/LICENSE,sha256=CItkBKs5m4J5jhkjXoW7IAnFyOC86x4hyOATpczUMmM,11338
53
- squirrels-0.3.3.dist-info/METADATA,sha256=h6acRcSaK-vu9N3MJV8JDe10uCeLvFpGTKWOC9bDLNw,5277
54
- squirrels-0.3.3.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
55
- squirrels-0.3.3.dist-info/entry_points.txt,sha256=mYQRuGxbg8X82hjNRJuWiON4S6kE5CPvmXmxNPtYTbg,92
56
- squirrels-0.3.3.dist-info/RECORD,,