squirrels 0.5.0b1__tar.gz → 0.5.0b2__tar.gz

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 (98) hide show
  1. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/PKG-INFO +11 -17
  2. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/README.md +10 -16
  3. squirrels-0.5.0b1/squirrels/dateutils.py → squirrels-0.5.0b2/dateutils/__init__.py +3 -5
  4. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/pyproject.toml +7 -1
  5. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_command_line.py +13 -9
  6. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_initializer.py +52 -33
  7. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_model_builder.py +2 -0
  8. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_model_configs.py +1 -1
  9. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_models.py +4 -2
  10. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_utils.py +1 -1
  11. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/models/builds/build_example.yml +2 -0
  12. squirrels-0.5.0b1/.cursorignore +0 -2
  13. squirrels-0.5.0b1/.github/workflows/python-publish.yml +0 -45
  14. squirrels-0.5.0b1/database_elt/expenses/.gitignore +0 -1
  15. squirrels-0.5.0b1/database_elt/expenses/create-expenses.py +0 -90
  16. squirrels-0.5.0b1/database_elt/expenses/create-lookups.py +0 -55
  17. squirrels-0.5.0b1/database_elt/seattle_weather/create_db.py +0 -13
  18. squirrels-0.5.0b1/database_elt/seattle_weather/seattle-weather.csv +0 -1462
  19. squirrels-0.5.0b1/tests/__init__.py +0 -0
  20. squirrels-0.5.0b1/tests/_auth_test.py +0 -166
  21. squirrels-0.5.0b1/tests/_connection_set_test.py +0 -46
  22. squirrels-0.5.0b1/tests/_manifest_test.py +0 -290
  23. squirrels-0.5.0b1/tests/_model_builder_test.py +0 -168
  24. squirrels-0.5.0b1/tests/_model_configs_test.py +0 -16
  25. squirrels-0.5.0b1/tests/_models_basic_test.py +0 -152
  26. squirrels-0.5.0b1/tests/_models_test.py +0 -167
  27. squirrels-0.5.0b1/tests/_seeds_test.py +0 -124
  28. squirrels-0.5.0b1/tests/_sources_test.py +0 -151
  29. squirrels-0.5.0b1/tests/_utils_test.py +0 -22
  30. squirrels-0.5.0b1/tests/arguments/run_time_args_test.py +0 -16
  31. squirrels-0.5.0b1/tests/conftest.py +0 -19
  32. squirrels-0.5.0b1/tests/data_sources_test.py +0 -206
  33. squirrels-0.5.0b1/tests/dateutils_test.py +0 -132
  34. squirrels-0.5.0b1/tests/parameter_configs_tests/_parameter_configs_test.py +0 -184
  35. squirrels-0.5.0b1/tests/parameter_configs_tests/_parameter_sets_test.py +0 -237
  36. squirrels-0.5.0b1/tests/parameter_configs_tests/conftest.py +0 -111
  37. squirrels-0.5.0b1/tests/parameter_options_test.py +0 -122
  38. squirrels-0.5.0b1/tests/parameters_test.py +0 -386
  39. squirrels-0.5.0b1/uv.lock +0 -2164
  40. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/.gitignore +0 -0
  41. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/LICENSE +0 -0
  42. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/__init__.py +0 -0
  43. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_api_response_models.py +0 -0
  44. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_api_server.py +0 -0
  45. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_auth.py +0 -0
  46. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_connection_set.py +0 -0
  47. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_constants.py +0 -0
  48. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_dashboards_io.py +0 -0
  49. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_exceptions.py +0 -0
  50. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_manifest.py +0 -0
  51. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_model_queries.py +0 -0
  52. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_package_loader.py +0 -0
  53. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_parameter_configs.py +0 -0
  54. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_parameter_sets.py +0 -0
  55. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_project.py +0 -0
  56. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_py_module.py +0 -0
  57. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_seeds.py +0 -0
  58. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_sources.py +0 -0
  59. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/_version.py +0 -0
  60. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/arguments/init_time_args.py +0 -0
  61. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/arguments/run_time_args.py +0 -0
  62. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/dashboards.py +0 -0
  63. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/data_sources.py +0 -0
  64. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/dataset_result.py +0 -0
  65. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/.env +0 -0
  66. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/.env.example +0 -0
  67. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/.gitignore +0 -0
  68. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/assets/expenses.db +0 -0
  69. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/assets/weather.db +0 -0
  70. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/connections.yml +0 -0
  71. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/dashboards/dashboard_example.py +0 -0
  72. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/dashboards/dashboard_example.yml +0 -0
  73. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/docker/.dockerignore +0 -0
  74. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/docker/Dockerfile +0 -0
  75. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/docker/compose.yml +0 -0
  76. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/duckdb_init.sql +0 -0
  77. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/macros/macros_example.sql +0 -0
  78. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/models/builds/build_example.py +0 -0
  79. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/models/builds/build_example.sql +0 -0
  80. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/models/dbviews/dbview_example.sql +0 -0
  81. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/models/dbviews/dbview_example.yml +0 -0
  82. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/models/federates/federate_example.py +0 -0
  83. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/models/federates/federate_example.sql +0 -0
  84. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/models/federates/federate_example.yml +0 -0
  85. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/models/sources.yml +0 -0
  86. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/parameters.yml +0 -0
  87. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/pyconfigs/connections.py +0 -0
  88. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/pyconfigs/context.py +0 -0
  89. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/pyconfigs/parameters.py +0 -0
  90. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/pyconfigs/user.py +0 -0
  91. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/seeds/seed_categories.csv +0 -0
  92. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/seeds/seed_categories.yml +0 -0
  93. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/seeds/seed_subcategories.csv +0 -0
  94. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/seeds/seed_subcategories.yml +0 -0
  95. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/squirrels.yml.j2 +0 -0
  96. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/package_data/base_project/tmp/.gitignore +0 -0
  97. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/parameter_options.py +0 -0
  98. {squirrels-0.5.0b1 → squirrels-0.5.0b2}/squirrels/parameters.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: squirrels
3
- Version: 0.5.0b1
3
+ Version: 0.5.0b2
4
4
  Summary: Squirrels - API Framework for Data Analytics
5
5
  Project-URL: Homepage, https://squirrels-analytics.github.io
6
6
  Project-URL: Repository, https://github.com/squirrels-analytics/squirrels
@@ -62,8 +62,8 @@ Here are a few of the things that squirrels can do:
62
62
  - Configure parameter widgets (types include single-select, multi-select, date, number, etc.) for your datasets (in `parameters.py`).
63
63
  - Use Jinja SQL templates (just like dbt!) or python functions (that return a Python dataframe such as polars or pandas) to define dynamic query logic based on parameter selections.
64
64
  - Query multiple databases and join the results together in a final view in one API endpoint/dataset!
65
- - Test your API endpoints with an interactive UI or by a command line that generates rendered sql queries and results (for a given set of parameter selections).
66
- - Define authentication logic (in `auth.py`) and authorize privacy scope per dataset (in `squirrels.yml`). The user's attributes can even be used in your query logic!
65
+ - Test your API endpoints with Squirrels Studio or by a command line that generates rendered sql queries and results (for a given set of parameter selections).
66
+ - Define User model (in `user.py`) and authorize privacy scope per dataset (in `squirrels.yml`). The user's attributes can even be used in your query logic!
67
67
 
68
68
  ## License
69
69
 
@@ -77,18 +77,18 @@ 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.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/.
80
+ This project requires python version 3.10 or above to be installed. It also uses the python package manager `uv`. Information on setting up poetry can be found at: https://docs.astral.sh/uv/getting-started/installation/.
81
81
 
82
- Then, to install all dependencies, run:
82
+ Then, to install all dependencies in a virtual environment, run:
83
83
 
84
- ```
85
- poetry install
84
+ ```bash
85
+ uv sync
86
86
  ```
87
87
 
88
- And activate the virtual environment created by poetry with:
88
+ And activate the virtual environment with:
89
89
 
90
- ```
91
- poetry shell
90
+ ```bash
91
+ source .venv/bin/activate
92
92
  ```
93
93
 
94
94
  To confirm that the setup worked, run the following to show the help page for all squirrels CLI commands:
@@ -97,11 +97,9 @@ To confirm that the setup worked, run the following to show the help page for al
97
97
  sqrl -h
98
98
  ```
99
99
 
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.
101
-
102
100
  ### Testing
103
101
 
104
- In poetry's virtual environment, run `pytest`.
102
+ Run `uv run pytest`. Or if you have the virtual environment activated, simply run `pytest`.
105
103
 
106
104
  ### Project Structure
107
105
 
@@ -110,7 +108,3 @@ From the root of the git repo, the source code can be found in the `squirrels` f
110
108
  To understand what a specific squirrels command is doing, start from the `_command_line.py` file as your entry point.
111
109
 
112
110
  The library version is maintained in both the `pyproject.toml` and the `squirrels/_version.py` files.
113
-
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.
115
-
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.
@@ -24,8 +24,8 @@ Here are a few of the things that squirrels can do:
24
24
  - Configure parameter widgets (types include single-select, multi-select, date, number, etc.) for your datasets (in `parameters.py`).
25
25
  - Use Jinja SQL templates (just like dbt!) or python functions (that return a Python dataframe such as polars or pandas) to define dynamic query logic based on parameter selections.
26
26
  - Query multiple databases and join the results together in a final view in one API endpoint/dataset!
27
- - Test your API endpoints with an interactive UI or by a command line that generates rendered sql queries and results (for a given set of parameter selections).
28
- - Define authentication logic (in `auth.py`) and authorize privacy scope per dataset (in `squirrels.yml`). The user's attributes can even be used in your query logic!
27
+ - Test your API endpoints with Squirrels Studio or by a command line that generates rendered sql queries and results (for a given set of parameter selections).
28
+ - Define User model (in `user.py`) and authorize privacy scope per dataset (in `squirrels.yml`). The user's attributes can even be used in your query logic!
29
29
 
30
30
  ## License
31
31
 
@@ -39,18 +39,18 @@ The sections below describe how to set up your local environment for squirrels d
39
39
 
40
40
  ### Setup
41
41
 
42
- 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/.
42
+ This project requires python version 3.10 or above to be installed. It also uses the python package manager `uv`. Information on setting up poetry can be found at: https://docs.astral.sh/uv/getting-started/installation/.
43
43
 
44
- Then, to install all dependencies, run:
44
+ Then, to install all dependencies in a virtual environment, run:
45
45
 
46
- ```
47
- poetry install
46
+ ```bash
47
+ uv sync
48
48
  ```
49
49
 
50
- And activate the virtual environment created by poetry with:
50
+ And activate the virtual environment with:
51
51
 
52
- ```
53
- poetry shell
52
+ ```bash
53
+ source .venv/bin/activate
54
54
  ```
55
55
 
56
56
  To confirm that the setup worked, run the following to show the help page for all squirrels CLI commands:
@@ -59,11 +59,9 @@ To confirm that the setup worked, run the following to show the help page for al
59
59
  sqrl -h
60
60
  ```
61
61
 
62
- 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.
63
-
64
62
  ### Testing
65
63
 
66
- In poetry's virtual environment, run `pytest`.
64
+ Run `uv run pytest`. Or if you have the virtual environment activated, simply run `pytest`.
67
65
 
68
66
  ### Project Structure
69
67
 
@@ -72,7 +70,3 @@ From the root of the git repo, the source code can be found in the `squirrels` f
72
70
  To understand what a specific squirrels command is doing, start from the `_command_line.py` file as your entry point.
73
71
 
74
72
  The library version is maintained in both the `pyproject.toml` and the `squirrels/_version.py` files.
75
-
76
- 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.
77
-
78
- 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.
@@ -5,8 +5,6 @@ from dateutil.relativedelta import relativedelta
5
5
  from abc import ABCMeta, abstractmethod
6
6
  from enum import Enum
7
7
 
8
- from . import _utils as u
9
-
10
8
 
11
9
  class DayOfWeek(Enum):
12
10
  Sunday = 0
@@ -62,7 +60,7 @@ class _DayIdxOfCalendarUnit(DateModifier):
62
60
  super().__init__()
63
61
  self.idx = idx
64
62
  if self.idx == 0:
65
- raise u.ConfigurationError(f"For constructors of class names that start with DayIdxOf_, idx cannot be zero")
63
+ raise ValueError(f"For constructors of class names that start with DayIdxOf_, idx cannot be zero")
66
64
  self.incr = self.idx - 1 if self.idx > 0 else self.idx
67
65
 
68
66
 
@@ -84,7 +82,7 @@ class DayIdxOfMonthsCycle(_DayIdxOfCalendarUnit):
84
82
  self._num_months_in_cycle = num_months_in_cycle
85
83
  self._first_month_of_cycle = first_month_of_cycle
86
84
  if 12 % self._num_months_in_cycle != 0:
87
- raise u.ConfigurationError(f"Value X must fit evenly in 12")
85
+ raise ValueError(f"Value X must fit evenly in 12")
88
86
  self.first_month_of_first_cycle = (self._first_month_of_cycle.value - 1) % self._num_months_in_cycle + 1
89
87
 
90
88
  def modify(self, date: Date) -> Date:
@@ -302,7 +300,7 @@ class DateModPipeline(DateModifier):
302
300
  """
303
301
  assert isinstance(step, _OffsetUnits)
304
302
  if step.offset == 0:
305
- raise u.ConfigurationError(f"The length of 'step' must not be zero")
303
+ raise ValueError(f"The length of 'step' must not be zero")
306
304
 
307
305
  output: Sequence[Date] = []
308
306
  end_date = self.modify(start_date)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "squirrels"
3
- version = "0.5.0b1"
3
+ version = "0.5.0b2"
4
4
  description = "Squirrels - API Framework for Data Analytics"
5
5
  authors = [{ name = "Tim Huang", email = "tim.yuting@hotmail.com" }]
6
6
  requires-python = "~=3.10"
@@ -58,6 +58,12 @@ dev = [
58
58
  "tqdm>=4.67.1,<5",
59
59
  ]
60
60
 
61
+ [tool.hatch.build.targets.sdist]
62
+ include = ["squirrels", "dateutils"]
63
+
64
+ [tool.hatch.build.targets.wheel]
65
+ include = ["squirrels", "dateutils"]
66
+
61
67
  [tool.uv]
62
68
  default-groups = [
63
69
  "test",
@@ -1,5 +1,5 @@
1
1
  from argparse import ArgumentParser, _SubParsersAction
2
- import sys, asyncio, traceback, io, os, subprocess
2
+ import sys, asyncio, traceback, io, subprocess
3
3
 
4
4
  sys.path.append('.')
5
5
 
@@ -11,10 +11,12 @@ from ._project import SquirrelsProject
11
11
  from . import _constants as c, _utils as u
12
12
 
13
13
 
14
- def _run_duckdb_cli(project: SquirrelsProject):
14
+ def _run_duckdb_cli(project: SquirrelsProject, ui: bool):
15
15
  _, target_init_path = u._read_duckdb_init_sql()
16
16
  init_args = f"-init {target_init_path}" if target_init_path else ""
17
17
  command = ['duckdb']
18
+ if ui:
19
+ command.append('-ui')
18
20
  if init_args:
19
21
  command.extend(init_args.split())
20
22
  command.extend(['-readonly', project._duckdb_venv_path])
@@ -48,21 +50,22 @@ def main():
48
50
 
49
51
  init_parser = add_subparser(subparsers, c.INIT_CMD, 'Create a new squirrels project')
50
52
 
51
- init_parser.add_argument('name', nargs='?', type=str, help='The name of the project')
52
- init_parser.add_argument('-o', '--overwrite', action='store_true', help="Overwrite files that already exist")
53
- init_parser.add_argument('--core', action='store_true', help='Include all core files')
53
+ init_parser.add_argument('name', nargs='?', type=str, help='The name of the project folder to create. Ignored if --curr-dir is used')
54
+ init_parser.add_argument('--curr-dir', action='store_true', help='Create the project in the current directory')
55
+ init_parser.add_argument('--use-defaults', action='store_true', help='Use default values for unspecified options (except project folder name) instead of prompting for input')
54
56
  init_parser.add_argument('--connections', type=str, choices=c.CONF_FORMAT_CHOICES, help=f'Configure database connections as yaml (default) or python')
55
57
  init_parser.add_argument('--parameters', type=str, choices=c.CONF_FORMAT_CHOICES, help=f'Configure parameters as python (default) or yaml')
56
58
  init_parser.add_argument('--build', type=str, choices=c.FILE_TYPE_CHOICES, help='Create build model as sql (default) or python file')
57
59
  init_parser.add_argument('--federate', type=str, choices=c.FILE_TYPE_CHOICES, help='Create federated model as sql (default) or python file')
58
- init_parser.add_argument('--dashboard', action='store_true', help=f'Include a sample dashboard file')
60
+ init_parser.add_argument('--dashboard', type=str, choices=['y', 'n'], help=f'Include (y) or exclude (n, default) a sample dashboard file')
61
+ init_parser.add_argument('--admin-password', type=str, help='The password for the admin user. If --use-defaults is used, then a random password is generated')
59
62
 
60
63
  def with_file_format_options(parser: ArgumentParser):
61
64
  help_text = "Create model as sql (default) or python file"
62
65
  parser.add_argument('--format', type=str, choices=c.FILE_TYPE_CHOICES, default=c.SQL_FILE_TYPE, help=help_text)
63
66
  return parser
64
67
 
65
- get_file_help_text = "Get a sample file for the squirrels project. If the file name already exists, it will be prefixed with a timestamp."
68
+ get_file_help_text = "Get a sample file for the squirrels project. If the file name already exists, it will be suffixed with a timestamp."
66
69
  get_file_parser = add_subparser(subparsers, c.GET_FILE_CMD, get_file_help_text)
67
70
  get_file_subparsers = get_file_parser.add_subparsers(title='file_name', dest='file_name')
68
71
  add_subparser(get_file_subparsers, c.DOTENV_FILE, f'Get sample {c.DOTENV_FILE} and {c.DOTENV_FILE}.example files')
@@ -104,6 +107,7 @@ def main():
104
107
  build_parser.add_argument('--stage', type=str, help='If the venv file is in use, stage the duckdb file to replace the venv later')
105
108
 
106
109
  duckdb_parser = add_subparser(subparsers, c.DUCKDB_CMD, 'Run the duckdb command line tool')
110
+ duckdb_parser.add_argument('--ui', action='store_true', help='Run the duckdb local UI')
107
111
 
108
112
  run_parser = add_subparser(subparsers, c.RUN_CMD, 'Run the API server')
109
113
  run_parser.add_argument('--build', action='store_true', help='Build the virtual data environment (with duckdb) first before running the API server')
@@ -116,7 +120,7 @@ def main():
116
120
  if args.version:
117
121
  print(__version__)
118
122
  elif args.command == c.INIT_CMD:
119
- Initializer(project_name=args.name, overwrite=args.overwrite).init_project(args)
123
+ Initializer(project_name=args.name, use_curr_dir=args.curr_dir).init_project(args)
120
124
  elif args.command == c.GET_FILE_CMD:
121
125
  Initializer().get_file(args)
122
126
  elif args.command is None:
@@ -131,7 +135,7 @@ def main():
131
135
  asyncio.run(task)
132
136
  print()
133
137
  elif args.command == c.DUCKDB_CMD:
134
- _run_duckdb_cli(project)
138
+ _run_duckdb_cli(project, args.ui)
135
139
  elif args.command == c.RUN_CMD:
136
140
  if args.build:
137
141
  task = project.build(full_refresh=True)
@@ -10,9 +10,9 @@ TMP_FOLDER = "tmp"
10
10
 
11
11
 
12
12
  class Initializer:
13
- def __init__(self, *, project_name: Optional[str] = None, overwrite: bool = False):
14
- self.project_name = project_name
15
- self.overwrite = overwrite
13
+ def __init__(self, *, project_name: Optional[str] = None, use_curr_dir: bool = False):
14
+ self.project_name = project_name if not use_curr_dir else None
15
+ self.use_curr_dir = use_curr_dir
16
16
 
17
17
  def _path_exists(self, filepath: u.Path) -> bool:
18
18
  return os.path.exists(filepath)
@@ -40,8 +40,6 @@ class Initializer:
40
40
  if self._files_have_same_content(src_path, filepath2):
41
41
  perform_copy = False
42
42
  extra_msg = "Skipping... file contents is same as source"
43
- elif self.overwrite:
44
- extra_msg = "Overwriting file..."
45
43
  else:
46
44
  filepath2 = self._add_timestamp_to_filename(old_filepath)
47
45
  extra_msg = f'Creating file as "{filepath2}" instead...'
@@ -116,42 +114,63 @@ class Initializer:
116
114
  self._copy_file(u.Path(c.DOTENV_FILE + ".example"))
117
115
 
118
116
  def init_project(self, args):
119
- options = ["core", "connections", "parameters", "build", "federate", "dashboard"]
120
- _, CONNECTIONS, PARAMETERS, BUILD, FEDERATE, DASHBOARD = options
117
+ options = ["connections", "parameters", "build", "federate", "dashboard", "admin_password"]
118
+ CONNECTIONS, PARAMETERS, BUILD, FEDERATE, DASHBOARD, ADMIN_PASSWORD = options
121
119
 
122
120
  # Add project name prompt if not provided
123
- if self.project_name is None:
121
+ if self.project_name is None and not args.curr_dir:
124
122
  questions = [
125
- inquirer.Text('project_name', message="What is your project name? (leave blank to create in current directory)")
123
+ inquirer.Text('project_name', message="What is your project folder name? (leave blank to create in current directory)")
126
124
  ]
127
125
  answers = inquirer.prompt(questions)
128
126
  assert isinstance(answers, dict)
129
127
  self.project_name = answers['project_name']
130
128
 
131
129
  answers = { x: getattr(args, x) for x in options }
132
- if not any(answers.values()):
133
- questions = [
134
- inquirer.List(
135
- CONNECTIONS, message=f"How would you like to configure the database connections?", choices=c.CONF_FORMAT_CHOICES
136
- ),
137
- inquirer.List(
138
- PARAMETERS, message=f"How would you like to configure the parameters?", choices=c.CONF_FORMAT_CHOICES2
139
- ),
140
- inquirer.List(
141
- BUILD, message="What's the file format for the build model?", choices=c.FILE_TYPE_CHOICES
142
- ),
143
- inquirer.List(
144
- FEDERATE, message="What's the file format for the federated model?", choices=c.FILE_TYPE_CHOICES
145
- ),
146
- inquirer.Confirm(
147
- DASHBOARD, message=f"Do you want to include a dashboard example?", default=False
148
- ),
149
- inquirer.Password(
150
- "admin_password", message="What's the admin password? (leave blank to generate a random one)"
151
- ),
152
- ]
153
- answers = inquirer.prompt(questions)
154
- assert isinstance(answers, dict)
130
+ if DASHBOARD in answers:
131
+ answers[DASHBOARD] = (answers[DASHBOARD] == 'y') # convert 'y' or 'n' to boolean
132
+
133
+ if not args.use_defaults:
134
+ questions = []
135
+ if answers.get(CONNECTIONS) is None:
136
+ questions.append(
137
+ inquirer.List(
138
+ CONNECTIONS, message=f"How would you like to configure the database connections?", choices=c.CONF_FORMAT_CHOICES
139
+ ),
140
+ )
141
+ if answers.get(PARAMETERS) is None:
142
+ questions.append(
143
+ inquirer.List(
144
+ PARAMETERS, message=f"How would you like to configure the parameters?", choices=c.CONF_FORMAT_CHOICES2
145
+ ),
146
+ )
147
+ if answers.get(BUILD) is None:
148
+ questions.append(
149
+ inquirer.List(
150
+ BUILD, message="What's the file format for the build model?", choices=c.FILE_TYPE_CHOICES
151
+ ),
152
+ )
153
+ if answers.get(FEDERATE) is None:
154
+ questions.append(
155
+ inquirer.List(
156
+ FEDERATE, message="What's the file format for the federated model?", choices=c.FILE_TYPE_CHOICES
157
+ ),
158
+ )
159
+ if answers.get(DASHBOARD) is None:
160
+ questions.append(
161
+ inquirer.Confirm(
162
+ DASHBOARD, message=f"Do you want to include a dashboard example?", default=False
163
+ ),
164
+ )
165
+ if answers.get(ADMIN_PASSWORD) is None:
166
+ questions.append(
167
+ inquirer.Password(
168
+ "admin_password", message="What's the admin password? (leave blank to generate a random one)"
169
+ ),
170
+ )
171
+ more_answers = inquirer.prompt(questions)
172
+ assert isinstance(more_answers, dict)
173
+ answers.update(more_answers)
155
174
 
156
175
  def get_answer(key, default):
157
176
  """
@@ -177,7 +196,7 @@ class Initializer:
177
196
  parameters_use_py = (parameters_format == c.PYTHON_FORMAT)
178
197
 
179
198
  build_config_file = c.BUILD_FILE_STEM + ".yml"
180
- build_format = get_answer(BUILD, c.PYTHON_FILE_TYPE)
199
+ build_format = get_answer(BUILD, c.SQL_FILE_TYPE)
181
200
  if build_format == c.SQL_FILE_TYPE:
182
201
  build_file = c.BUILD_FILE_STEM + ".sql"
183
202
  elif build_format == c.PYTHON_FILE_TYPE:
@@ -79,6 +79,8 @@ class ModelBuilder:
79
79
  duckdb_stg_path.replace(duckdb_dev_path)
80
80
  elif duckdb_path.exists():
81
81
  shutil.copy(duckdb_path, duckdb_dev_path)
82
+ else:
83
+ duckdb_dev_path.unlink(missing_ok=True) # delete any lingering development copy to create a fresh one later
82
84
 
83
85
  self._logger.log_activity_time("creating development copy of virtual data environment", start)
84
86
 
@@ -47,7 +47,7 @@ class QueryModelConfig(ModelConfig):
47
47
 
48
48
 
49
49
  class BuildModelConfig(QueryModelConfig):
50
- materialization: str = Field(default="TABLE", description="The materialization of the model (ignored if Python model which is always a table)")
50
+ materialization: str = Field(default="VIEW", description="The materialization of the model (ignored if Python model which is always a table)")
51
51
 
52
52
  def get_sql_for_build(self, model_name: str, select_query: str) -> str:
53
53
  if self.materialization.upper() == "TABLE":
@@ -476,15 +476,17 @@ class DbviewModel(QueryModel):
476
476
  connection_props = self.conn_set.get_connection(connection_name)
477
477
 
478
478
  if self.model_config.translate_to_duckdb and isinstance(connection_props, ConnectionProperties):
479
+ source_func = lambda source_name: "venv." + source_name
479
480
  macros = {
480
- "source": lambda source_name: "venv." + source_name
481
+ "source": source_func, "ref": source_func
481
482
  }
482
483
  compiled_query2 = self._get_compiled_sql_query_str(compiled_query_str, macros)
483
484
  compiled_query_str = self._get_duckdb_query(connection_props.dialect, compiled_query2)
484
485
  is_duckdb = True
485
486
  else:
487
+ source_func = lambda source_name: self.sources[source_name].get_table()
486
488
  macros = {
487
- "source": lambda source_name: self.sources[source_name].get_table()
489
+ "source": source_func, "ref": source_func
488
490
  }
489
491
  compiled_query_str = self._get_compiled_sql_query_str(compiled_query_str, macros)
490
492
  is_duckdb = False
@@ -20,7 +20,7 @@ polars_dtypes_to_sqrl_dtypes: dict[type[pl.DataType], list[str]] = {
20
20
  pl.Int32: ["integer", "int", "int4"],
21
21
  pl.Int64: ["bigint", "long", "int8"],
22
22
  pl.Float32: ["float", "float4", "real"],
23
- pl.Float64: ["double", "float8"],
23
+ pl.Float64: ["double", "float8", "decimal"], # Note: Polars Decimal type is considered unstable, so we use Float64 for "decimal"
24
24
  pl.Boolean: ["boolean", "bool", "logical"],
25
25
  pl.Date: ["date"],
26
26
  pl.Time: ["time"],
@@ -1,6 +1,8 @@
1
1
  description: |
2
2
  This is an example of a build model. It adds a new column called "month" to the source table "src_transactions".
3
3
 
4
+ materialization: TABLE # optional - defaults to "VIEW" for SQL models, ignored and always a "TABLE" for Python models
5
+
4
6
  depends_on: # optional for SQL models - the "ref" macro also adds to this set
5
7
  - src_transactions
6
8
  - seed_categories
@@ -1,2 +0,0 @@
1
- database_elt/
2
- poetry.lock
@@ -1,45 +0,0 @@
1
- name: Publish Python Package
2
-
3
- on:
4
- push:
5
- tags:
6
- - 'v*'
7
-
8
- permissions:
9
- contents: read
10
-
11
- jobs:
12
- deploy:
13
- runs-on: ubuntu-latest
14
- steps:
15
- - uses: actions/checkout@v3
16
-
17
- - name: Set up Python
18
- uses: actions/setup-python@v4
19
- with:
20
- python-version: '3.x'
21
-
22
- - name: Extract version from tag
23
- id: get_version
24
- run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
25
-
26
- - name: Update version in pyproject.toml
27
- run: |
28
- # Use sed to replace the version in pyproject.toml
29
- sed -i "s/^version = \".*\"/version = \"${{ steps.get_version.outputs.VERSION }}\"/" pyproject.toml
30
- echo "Updated version in pyproject.toml to ${{ steps.get_version.outputs.VERSION }}"
31
-
32
- - name: Install build dependencies
33
- run: |
34
- python -m pip install --upgrade pip
35
- pip install build twine
36
-
37
- - name: Build package
38
- run: python -m build
39
-
40
- - name: Publish to PyPI
41
- env:
42
- TWINE_USERNAME: __token__
43
- TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
44
- run: |
45
- python -m twine upload --username $TWINE_USERNAME --password $TWINE_PASSWORD dist/*
@@ -1 +0,0 @@
1
- expenses.db
@@ -1,90 +0,0 @@
1
- import sqlite3, random, polars as pl, tqdm, numpy as np
2
-
3
- def generate_expense_description(vendors=None, items=None, adjectives=None):
4
- """
5
- Generates a random expense description.
6
-
7
- Args:
8
- categories: A list of expense categories (e.g., ["Groceries", "Dining", "Travel", "Utilities"]).
9
- vendors: A list of vendor names (e.g., ["Walmart", "Starbucks", "Amazon", "Gas Station"]).
10
- items: A list of items purchased (e.g., ["Milk", "Coffee", "Laptop", "Gas"]).
11
- adjectives: A list of adjectives to add variety(e.g., ["Monthly", "Quick", "Online", "Unexpected"]).
12
-
13
- Returns:
14
- A randomly generated expense description string.
15
- """
16
-
17
- if vendors is None:
18
- vendors = ["Vendor A", "Vendor B", "Vendor C", "Vendor D", "Vendor E", "Online Store", "Local Shop", "Restaurant"]
19
- if items is None:
20
- items = ["Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Service", "Subscription", "Purchase", "Merchandise", "Goods"]
21
- if adjectives is None:
22
- adjectives = ["Daily", "Monthly", "Quick", "Online", "Unexpected", "Recurring", "Personal", "Business"]
23
-
24
- description_parts = []
25
-
26
- # Choose a category, vendor, and item
27
- adjective = random.choice(adjectives)
28
- vendor = random.choice(vendors)
29
- item = random.choice(items)
30
-
31
- # Build the description
32
- description_parts.append(f"{adjective} {item} - {vendor}")
33
-
34
- return " ".join(description_parts).strip()
35
-
36
- # Define the number of transactions to generate
37
- num_transactions = 1_000
38
- batches = 100
39
- batch_size = num_transactions // batches
40
-
41
- # Generate the data
42
- descriptions_df = pl.DataFrame({
43
- 'description': [generate_expense_description() for _ in range(10**4)]
44
- })
45
-
46
- df_list: list[pl.DataFrame] = []
47
- rng = np.random.default_rng()
48
- for _ in tqdm.tqdm(range(batches)):
49
- df_current = descriptions_df.sample(batch_size, with_replacement=True, shuffle=True)
50
- df_current = df_current.with_columns(
51
- pl.lit(rng.integers(
52
- np.datetime64('2024-01-01').astype(int),
53
- np.datetime64('2025-01-01').astype(int),
54
- size=batch_size
55
- ).astype('datetime64[D]')).alias('date'),
56
- pl.lit(rng.integers(0, 14, size=batch_size)).alias('subcategory_id'),
57
- pl.lit(rng.exponential(30, size=batch_size).round(2)).alias('amount'),
58
- )
59
- df_list.append(df_current)
60
-
61
- df = pl.concat(df_list).select('date', 'subcategory_id', 'amount', 'description')
62
- df = df.sort('date').with_row_index(name='id', offset=1)
63
-
64
- # Connect to SQLite database
65
- conn = sqlite3.connect('expenses.db')
66
-
67
- try:
68
- # Create the expenses table
69
- conn.execute("DROP TABLE IF EXISTS expenses")
70
- conn.execute('''
71
- CREATE TABLE IF NOT EXISTS expenses (
72
- id INTEGER PRIMARY KEY,
73
- date DATE,
74
- subcategory_id INTEGER,
75
- amount DECIMAL(10,2),
76
- description TEXT
77
- )
78
- ''')
79
-
80
- # Convert DataFrame to records and insert into database
81
- records = df.to_numpy().tolist()
82
- conn.executemany(
83
- 'INSERT INTO expenses (id, date, subcategory_id, amount, description) VALUES (?, ?, ?, ?, ?)',
84
- records
85
- )
86
-
87
- # Commit changes and close connection
88
- conn.commit()
89
- finally:
90
- conn.close()
@@ -1,55 +0,0 @@
1
- import sqlite3
2
- import csv
3
- from io import StringIO
4
-
5
- category_id_mapping = """
6
- "category_id","subcategory_id"
7
- 0,0
8
- 0,1
9
- 1,2
10
- 2,3
11
- 3,4
12
- 1,5
13
- 2,6
14
- 1,7
15
- 4,8
16
- 4,9
17
- 2,10
18
- 3,11
19
- 2,12
20
- 4,13
21
- """
22
-
23
- # Connect to the SQLite database
24
- conn = sqlite3.connect('expenses.db')
25
- try:
26
- cursor = conn.cursor()
27
-
28
- # Create the category_mapping table
29
- cursor.execute('''
30
- DROP TABLE IF EXISTS category_mapping
31
- ''')
32
- cursor.execute('''
33
- CREATE TABLE category_mapping (
34
- category_id INTEGER,
35
- subcategory_id INTEGER,
36
- PRIMARY KEY (subcategory_id)
37
- )
38
- ''')
39
-
40
- # Parse the CSV string and insert data
41
- csv_file = StringIO(category_id_mapping.strip())
42
- csv_reader = csv.DictReader(csv_file)
43
-
44
- # Insert the data
45
- for row in csv_reader:
46
- cursor.execute('''
47
- INSERT OR REPLACE INTO category_mapping (category_id, subcategory_id)
48
- VALUES (?, ?)
49
- ''', (int(row['category_id']), int(row['subcategory_id'])))
50
-
51
- # Commit the changes and close the connection
52
- conn.commit()
53
-
54
- finally:
55
- conn.close()
@@ -1,13 +0,0 @@
1
- import pandas as pd
2
- import sqlite3, os
3
-
4
- os.chdir(os.path.dirname(__file__))
5
-
6
- df = pd.read_csv('seattle-weather.csv')
7
-
8
- # Connect to SQLite database
9
- conn = sqlite3.connect('weather.db')
10
- try:
11
- df.to_sql('weather', conn, index=False, if_exists='replace')
12
- finally:
13
- conn.close()