squirrels 0.5.0b4__tar.gz → 0.5.1__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.
- {squirrels-0.5.0b4 → squirrels-0.5.1}/PKG-INFO +28 -13
- {squirrels-0.5.0b4 → squirrels-0.5.1}/README.md +24 -8
- {squirrels-0.5.0b4 → squirrels-0.5.1}/pyproject.toml +13 -14
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/__init__.py +2 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_api_routes/auth.py +83 -74
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_api_routes/base.py +58 -41
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_api_routes/dashboards.py +37 -21
- squirrels-0.5.1/squirrels/_api_routes/data_management.py +148 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_api_routes/datasets.py +107 -84
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_api_routes/oauth2.py +11 -13
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_api_routes/project.py +71 -33
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_api_server.py +130 -63
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_arguments/run_time_args.py +9 -9
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_auth.py +117 -162
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_command_line.py +68 -32
- squirrels-0.5.1/squirrels/_compile_prompts.py +147 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_connection_set.py +11 -2
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_constants.py +22 -8
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_data_sources.py +38 -32
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_dataset_types.py +2 -4
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_initializer.py +1 -1
- squirrels-0.5.1/squirrels/_logging.py +117 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_manifest.py +125 -58
- squirrels-0.5.1/squirrels/_model_builder.py +69 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_models.py +224 -108
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/.env +15 -4
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/.env.example +14 -3
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/connections.yml +4 -3
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/dashboards/dashboard_example.py +2 -2
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/dashboards/dashboard_example.yml +4 -4
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/duckdb_init.sql +1 -0
- squirrels-0.5.1/squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +17 -0
- squirrels-0.5.1/squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +32 -0
- squirrels-0.5.1/squirrels/_package_data/base_project/models/federates/federate_example.py +48 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/models/federates/federate_example.sql +3 -7
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/models/federates/federate_example.yml +1 -1
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/models/sources.yml +5 -6
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/parameters.yml +24 -38
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/pyconfigs/connections.py +5 -1
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/pyconfigs/context.py +23 -12
- squirrels-0.5.1/squirrels/_package_data/base_project/pyconfigs/parameters.py +141 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/pyconfigs/user.py +11 -18
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/seeds/seed_categories.yml +1 -1
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/seeds/seed_subcategories.yml +1 -1
- squirrels-0.5.1/squirrels/_package_data/base_project/squirrels.yml.j2 +61 -0
- squirrels-0.5.1/squirrels/_package_data/templates/squirrels_studio.html +20 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_parameter_configs.py +43 -22
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_parameter_options.py +1 -1
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_parameter_sets.py +8 -10
- squirrels-0.5.1/squirrels/_project.py +722 -0
- squirrels-0.5.1/squirrels/_request_context.py +33 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_schemas/auth_models.py +32 -9
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_schemas/query_param_models.py +9 -1
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_schemas/response_models.py +36 -10
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_seeds.py +1 -1
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_sources.py +23 -19
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_utils.py +83 -35
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_version.py +1 -1
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/arguments.py +5 -0
- squirrels-0.5.1/squirrels/auth.py +4 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/connections.py +2 -0
- squirrels-0.5.1/squirrels/dashboards.py +3 -0
- squirrels-0.5.1/squirrels/data_sources.py +14 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/parameter_options.py +5 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/parameters.py +5 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/types.py +6 -1
- squirrels-0.5.0b4/squirrels/_api_routes/data_management.py +0 -103
- squirrels-0.5.0b4/squirrels/_model_builder.py +0 -113
- squirrels-0.5.0b4/squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +0 -12
- squirrels-0.5.0b4/squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +0 -26
- squirrels-0.5.0b4/squirrels/_package_data/base_project/models/federates/federate_example.py +0 -41
- squirrels-0.5.0b4/squirrels/_package_data/base_project/pyconfigs/parameters.py +0 -106
- squirrels-0.5.0b4/squirrels/_package_data/base_project/squirrels.yml.j2 +0 -71
- squirrels-0.5.0b4/squirrels/_project.py +0 -605
- squirrels-0.5.0b4/squirrels/auth.py +0 -1
- squirrels-0.5.0b4/squirrels/dashboards.py +0 -1
- squirrels-0.5.0b4/squirrels/data_sources.py +0 -8
- {squirrels-0.5.0b4 → squirrels-0.5.1}/.gitignore +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/LICENSE +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/dateutils/__init__.py +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/dateutils/_enums.py +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/dateutils/_implementation.py +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/dateutils/types.py +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_api_routes/__init__.py +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_arguments/__init__.py +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_arguments/init_time_args.py +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_dashboards.py +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_exceptions.py +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_model_configs.py +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_model_queries.py +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/assets/expenses.db +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/assets/weather.db +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/docker/.dockerignore +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/docker/Dockerfile +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/docker/compose.yml +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/gitignore +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/macros/macros_example.sql +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/models/builds/build_example.py +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/models/builds/build_example.sql +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/models/builds/build_example.yml +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/seeds/seed_categories.csv +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/seeds/seed_subcategories.csv +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/tmp/.gitignore +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/templates/dataset_results.html +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/templates/oauth_login.html +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_loader.py +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_parameters.py +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_py_module.py +0 -0
- {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_schemas/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: squirrels
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1
|
|
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
|
|
@@ -15,8 +15,8 @@ Classifier: Typing :: Typed
|
|
|
15
15
|
Requires-Python: ~=3.10
|
|
16
16
|
Requires-Dist: authlib<2,>=1.5.2
|
|
17
17
|
Requires-Dist: bcrypt<5,>=4.0.1
|
|
18
|
-
Requires-Dist: cachetools
|
|
19
|
-
Requires-Dist: duckdb<2,>=1.
|
|
18
|
+
Requires-Dist: cachetools>=5.3.2
|
|
19
|
+
Requires-Dist: duckdb<2,>=1.4.0
|
|
20
20
|
Requires-Dist: fastapi<1,>=0.112.1
|
|
21
21
|
Requires-Dist: gitpython<4,>=3.1.41
|
|
22
22
|
Requires-Dist: inquirer<4,>=3.2.1
|
|
@@ -24,8 +24,7 @@ Requires-Dist: itsdangerous<3,>=2.2.0
|
|
|
24
24
|
Requires-Dist: jinja2<4,>=3.1.3
|
|
25
25
|
Requires-Dist: libpass<2,>=1.9.0
|
|
26
26
|
Requires-Dist: matplotlib<4,>=3.8.3
|
|
27
|
-
Requires-Dist: mcp>=1.
|
|
28
|
-
Requires-Dist: networkx<4,>=3.2.1
|
|
27
|
+
Requires-Dist: mcp>=1.13.1
|
|
29
28
|
Requires-Dist: pandas<3,>=2.1.4
|
|
30
29
|
Requires-Dist: polars<2,>=1.14.0
|
|
31
30
|
Requires-Dist: pyarrow>=19.0.1
|
|
@@ -51,22 +50,38 @@ Squirrels is an API framework that lets you create REST APIs for dynamic data an
|
|
|
51
50
|
|
|
52
51
|
- [Main Features](#main-features)
|
|
53
52
|
- [License](#license)
|
|
54
|
-
- [Contributing to
|
|
53
|
+
- [Contributing to Squirrels](#contributing-to-squirrels)
|
|
55
54
|
- [Setup](#setup)
|
|
56
55
|
- [Testing](#testing)
|
|
57
56
|
- [Project Structure](#project-structure)
|
|
58
57
|
|
|
59
58
|
## Main Features
|
|
60
59
|
|
|
61
|
-
Here are a few of the things that
|
|
60
|
+
Here are a few of the things that Squirrels can do:
|
|
62
61
|
|
|
63
62
|
- Connect to any database by specifying its SQLAlchemy url (in `squirrels.yml`) or by using its native connector library in python (in `connections.py`).
|
|
64
63
|
- Configure API routes for datasets (in `squirrels.yml`) without writing code.
|
|
65
64
|
- Configure parameter widgets (types include single-select, multi-select, date, number, etc.) for your datasets (in `parameters.py`).
|
|
66
|
-
- Use
|
|
65
|
+
- Use SQL templates (templated with Jinja, like dbt) or python functions (that return a Python dataframe in polars or pandas) to define dynamic query logic based on parameter selections.
|
|
67
66
|
- Query multiple databases and join the results together in a final view in one API endpoint/dataset!
|
|
68
|
-
- 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).
|
|
67
|
+
- Test your API endpoints with Squirrels Studio or by a command line that generates rendered sql queries and results as files (for a given set of parameter selections).
|
|
69
68
|
- 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!
|
|
69
|
+
- Serve dataset metadata and results to AI agents via MCP (Model Context Protocol)
|
|
70
|
+
|
|
71
|
+
## Quick Start
|
|
72
|
+
|
|
73
|
+
In a new virtual environment, install `squirrels`. Then, in your project directory, activate the virtual environment and run the following commands:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
sqrl new --use-defaults --curr-dir
|
|
77
|
+
sqrl build
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
To run the API server, simply run:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
sqrl run
|
|
84
|
+
```
|
|
70
85
|
|
|
71
86
|
## License
|
|
72
87
|
|
|
@@ -74,9 +89,9 @@ Squirrels is released under the Apache 2.0 license.
|
|
|
74
89
|
|
|
75
90
|
See the file LICENSE for more details.
|
|
76
91
|
|
|
77
|
-
## Contributing to
|
|
92
|
+
## Contributing to Squirrels
|
|
78
93
|
|
|
79
|
-
The sections below describe how to set up your local environment for
|
|
94
|
+
The sections below describe how to set up your local environment for Squirrels development and run unit tests. A high level overview of the project structure is also provided.
|
|
80
95
|
|
|
81
96
|
### Setup
|
|
82
97
|
|
|
@@ -94,7 +109,7 @@ And activate the virtual environment with:
|
|
|
94
109
|
source .venv/bin/activate
|
|
95
110
|
```
|
|
96
111
|
|
|
97
|
-
To confirm that the setup worked, run the following to show the help page for all
|
|
112
|
+
To confirm that the setup worked, run the following to show the help page for all Squirrels CLI commands:
|
|
98
113
|
|
|
99
114
|
```bash
|
|
100
115
|
sqrl -h
|
|
@@ -108,6 +123,6 @@ Run `uv run pytest`. Or if you have the virtual environment activated, simply ru
|
|
|
108
123
|
|
|
109
124
|
From the root of the git repo, the source code can be found in the `squirrels` folder and unit tests can be found in the `tests` folder.
|
|
110
125
|
|
|
111
|
-
To understand what a specific
|
|
126
|
+
To understand what a specific Squirrels command is doing, start from the `_command_line.py` file as your entry point.
|
|
112
127
|
|
|
113
128
|
The library version is maintained in both the `pyproject.toml` and the `squirrels/_version.py` files.
|
|
@@ -10,22 +10,38 @@ Squirrels is an API framework that lets you create REST APIs for dynamic data an
|
|
|
10
10
|
|
|
11
11
|
- [Main Features](#main-features)
|
|
12
12
|
- [License](#license)
|
|
13
|
-
- [Contributing to
|
|
13
|
+
- [Contributing to Squirrels](#contributing-to-squirrels)
|
|
14
14
|
- [Setup](#setup)
|
|
15
15
|
- [Testing](#testing)
|
|
16
16
|
- [Project Structure](#project-structure)
|
|
17
17
|
|
|
18
18
|
## Main Features
|
|
19
19
|
|
|
20
|
-
Here are a few of the things that
|
|
20
|
+
Here are a few of the things that Squirrels can do:
|
|
21
21
|
|
|
22
22
|
- Connect to any database by specifying its SQLAlchemy url (in `squirrels.yml`) or by using its native connector library in python (in `connections.py`).
|
|
23
23
|
- Configure API routes for datasets (in `squirrels.yml`) without writing code.
|
|
24
24
|
- Configure parameter widgets (types include single-select, multi-select, date, number, etc.) for your datasets (in `parameters.py`).
|
|
25
|
-
- Use
|
|
25
|
+
- Use SQL templates (templated with Jinja, like dbt) or python functions (that return a Python dataframe in 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 Squirrels Studio or by a command line that generates rendered sql queries and results (for a given set of parameter selections).
|
|
27
|
+
- Test your API endpoints with Squirrels Studio or by a command line that generates rendered sql queries and results as files (for a given set of parameter selections).
|
|
28
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
|
+
- Serve dataset metadata and results to AI agents via MCP (Model Context Protocol)
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
In a new virtual environment, install `squirrels`. Then, in your project directory, activate the virtual environment and run the following commands:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
sqrl new --use-defaults --curr-dir
|
|
37
|
+
sqrl build
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
To run the API server, simply run:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
sqrl run
|
|
44
|
+
```
|
|
29
45
|
|
|
30
46
|
## License
|
|
31
47
|
|
|
@@ -33,9 +49,9 @@ Squirrels is released under the Apache 2.0 license.
|
|
|
33
49
|
|
|
34
50
|
See the file LICENSE for more details.
|
|
35
51
|
|
|
36
|
-
## Contributing to
|
|
52
|
+
## Contributing to Squirrels
|
|
37
53
|
|
|
38
|
-
The sections below describe how to set up your local environment for
|
|
54
|
+
The sections below describe how to set up your local environment for Squirrels development and run unit tests. A high level overview of the project structure is also provided.
|
|
39
55
|
|
|
40
56
|
### Setup
|
|
41
57
|
|
|
@@ -53,7 +69,7 @@ And activate the virtual environment with:
|
|
|
53
69
|
source .venv/bin/activate
|
|
54
70
|
```
|
|
55
71
|
|
|
56
|
-
To confirm that the setup worked, run the following to show the help page for all
|
|
72
|
+
To confirm that the setup worked, run the following to show the help page for all Squirrels CLI commands:
|
|
57
73
|
|
|
58
74
|
```bash
|
|
59
75
|
sqrl -h
|
|
@@ -67,6 +83,6 @@ Run `uv run pytest`. Or if you have the virtual environment activated, simply ru
|
|
|
67
83
|
|
|
68
84
|
From the root of the git repo, the source code can be found in the `squirrels` folder and unit tests can be found in the `tests` folder.
|
|
69
85
|
|
|
70
|
-
To understand what a specific
|
|
86
|
+
To understand what a specific Squirrels command is doing, start from the `_command_line.py` file as your entry point.
|
|
71
87
|
|
|
72
88
|
The library version is maintained in both the `pyproject.toml` and the `squirrels/_version.py` files.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "squirrels"
|
|
3
|
-
version = "0.5.
|
|
3
|
+
version = "0.5.1"
|
|
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"
|
|
@@ -13,13 +13,12 @@ classifiers = [
|
|
|
13
13
|
"Typing :: Typed",
|
|
14
14
|
]
|
|
15
15
|
dependencies = [
|
|
16
|
-
"cachetools>=5.3.2
|
|
16
|
+
"cachetools>=5.3.2",
|
|
17
17
|
"fastapi>=0.112.1,<1",
|
|
18
18
|
"gitpython>=3.1.41,<4",
|
|
19
19
|
"inquirer>=3.2.1,<4",
|
|
20
20
|
"jinja2>=3.1.3,<4",
|
|
21
21
|
"matplotlib>=3.8.3,<4",
|
|
22
|
-
"networkx>=3.2.1,<4",
|
|
23
22
|
"pandas>=2.1.4,<3",
|
|
24
23
|
"pydantic>=2.8.2,<3",
|
|
25
24
|
"pyjwt>=2.8.0,<3",
|
|
@@ -29,14 +28,14 @@ dependencies = [
|
|
|
29
28
|
"uvicorn>=0.30.6,<1",
|
|
30
29
|
"polars>=1.14.0,<2",
|
|
31
30
|
"pyarrow>=19.0.1",
|
|
32
|
-
"duckdb>=1.
|
|
31
|
+
"duckdb>=1.4.0,<2",
|
|
33
32
|
"sqlglot>=26.12.1",
|
|
34
33
|
"bcrypt>=4.0.1,<5",
|
|
35
34
|
"python-dotenv>=1.0.1,<2",
|
|
36
35
|
"libpass>=1.9.0,<2",
|
|
37
36
|
"authlib>=1.5.2,<2",
|
|
38
37
|
"itsdangerous>=2.2.0,<3",
|
|
39
|
-
"mcp>=1.
|
|
38
|
+
"mcp>=1.13.1",
|
|
40
39
|
]
|
|
41
40
|
|
|
42
41
|
[project.urls]
|
|
@@ -49,16 +48,16 @@ squirrels = "squirrels._command_line:main"
|
|
|
49
48
|
sqrl = "squirrels._command_line:main"
|
|
50
49
|
|
|
51
50
|
[dependency-groups]
|
|
52
|
-
test = ["pytest>=7.4.4
|
|
51
|
+
test = ["pytest>=7.4.4"]
|
|
53
52
|
dev = [
|
|
54
|
-
"ipykernel>=6.29.4
|
|
55
|
-
"plotly>=5.24.0
|
|
56
|
-
"psycopg2-binary>=2.9.10
|
|
57
|
-
"adbc-driver-postgresql>=1.3.0
|
|
58
|
-
"adbc-driver-sqlite>=1.3.0
|
|
59
|
-
"connectorx>=0.4.0
|
|
60
|
-
"faker>=33.1.0
|
|
61
|
-
"tqdm>=4.67.1
|
|
53
|
+
"ipykernel>=6.29.4",
|
|
54
|
+
"plotly>=5.24.0",
|
|
55
|
+
"psycopg2-binary>=2.9.10",
|
|
56
|
+
"adbc-driver-postgresql>=1.3.0",
|
|
57
|
+
"adbc-driver-sqlite>=1.3.0",
|
|
58
|
+
"connectorx>=0.4.0",
|
|
59
|
+
"faker>=33.1.0",
|
|
60
|
+
"tqdm>=4.67.1",
|
|
62
61
|
]
|
|
63
62
|
|
|
64
63
|
[tool.hatch.build.targets.sdist]
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Authentication and user management routes
|
|
3
3
|
"""
|
|
4
|
-
from typing import Annotated,
|
|
4
|
+
from typing import Annotated, Literal
|
|
5
5
|
from fastapi import FastAPI, Depends, Request, Response, status, Form, APIRouter
|
|
6
6
|
from fastapi.responses import RedirectResponse
|
|
7
7
|
from fastapi.security import HTTPBearer
|
|
8
8
|
from pydantic import BaseModel, Field
|
|
9
9
|
from authlib.integrations.starlette_client import OAuth
|
|
10
10
|
|
|
11
|
-
from .. import _constants as c
|
|
12
11
|
from .._schemas import response_models as rm
|
|
13
12
|
from .._exceptions import InvalidInputError
|
|
14
|
-
from ..
|
|
13
|
+
from .._schemas.auth_models import AbstractUser, RegisteredUser, GuestUser
|
|
15
14
|
from .base import RouteBase
|
|
16
15
|
|
|
17
16
|
|
|
@@ -21,18 +20,19 @@ class AuthRoutes(RouteBase):
|
|
|
21
20
|
def __init__(self, get_bearer_token: HTTPBearer, project, no_cache: bool = False):
|
|
22
21
|
super().__init__(get_bearer_token, project, no_cache)
|
|
23
22
|
|
|
24
|
-
def setup_routes(self, app: FastAPI) -> None:
|
|
23
|
+
def setup_routes(self, app: FastAPI, squirrels_version_path: str) -> None:
|
|
25
24
|
"""Setup all authentication routes"""
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
auth_path = squirrels_version_path + "/auth"
|
|
27
|
+
auth_router = APIRouter(prefix=auth_path)
|
|
28
|
+
user_management_router = APIRouter(prefix=auth_path + "/user-management")
|
|
29
29
|
|
|
30
30
|
# Get expiry configuration
|
|
31
31
|
expiry_mins = self._get_access_token_expiry_minutes()
|
|
32
32
|
|
|
33
33
|
# Create user models
|
|
34
|
-
class UpdateUserModel(self.
|
|
35
|
-
|
|
34
|
+
class UpdateUserModel(self.authenticator.CustomUserFields):
|
|
35
|
+
access_level: Literal["admin", "member"]
|
|
36
36
|
|
|
37
37
|
class UserInfoModel(UpdateUserModel):
|
|
38
38
|
username: str
|
|
@@ -54,14 +54,15 @@ class AuthRoutes(RouteBase):
|
|
|
54
54
|
|
|
55
55
|
# User info endpoint
|
|
56
56
|
@auth_router.get("/userinfo", description="Get the authenticated user's fields", tags=["Authentication"])
|
|
57
|
-
async def get_userinfo(user:
|
|
58
|
-
if user
|
|
59
|
-
raise InvalidInputError(401, "Invalid authorization token
|
|
60
|
-
|
|
57
|
+
async def get_userinfo(user: RegisteredUser | GuestUser = Depends(self.get_current_user)) -> UserInfoModel:
|
|
58
|
+
if isinstance(user, GuestUser):
|
|
59
|
+
raise InvalidInputError(401, "invalid_authorization_token", "Invalid authorization token, no user info found")
|
|
60
|
+
custom_fields = user.custom_fields.model_dump(mode='json')
|
|
61
|
+
return UserInfoModel(username=user.username, access_level=user.access_level, **custom_fields)
|
|
61
62
|
|
|
62
63
|
# Login helper
|
|
63
64
|
def login_helper(
|
|
64
|
-
request: Request, user:
|
|
65
|
+
request: Request, user: RegisteredUser, redirect_url: str | None, *,
|
|
65
66
|
redirect_status_code: int = status.HTTP_307_TEMPORARY_REDIRECT
|
|
66
67
|
):
|
|
67
68
|
access_token, expiry = self.authenticator.create_access_token(user, expiry_minutes=expiry_mins)
|
|
@@ -75,6 +76,8 @@ class AuthRoutes(RouteBase):
|
|
|
75
76
|
302: {"description": "Redirect if redirect URL parameter is specified"},
|
|
76
77
|
})
|
|
77
78
|
async def login(request: Request, username: Annotated[str, Form()], password: Annotated[str, Form()], redirect_url: str | None = None):
|
|
79
|
+
if self.manifest_cfg.authentication.type.value == "external":
|
|
80
|
+
raise InvalidInputError(403, "forbidden_login", "Username/password login is disabled when authentication.type is 'external'")
|
|
78
81
|
user = self.authenticator.get_user(username, password)
|
|
79
82
|
return login_helper(request, user, redirect_url, redirect_status_code=status.HTTP_302_FOUND)
|
|
80
83
|
|
|
@@ -83,10 +86,10 @@ class AuthRoutes(RouteBase):
|
|
|
83
86
|
307: {"description": "Redirect if redirect URL parameter is specified"},
|
|
84
87
|
})
|
|
85
88
|
async def login_with_api_key(
|
|
86
|
-
request: Request, redirect_url: str | None = None, user:
|
|
89
|
+
request: Request, redirect_url: str | None = None, user: RegisteredUser | GuestUser = Depends(self.get_current_user)
|
|
87
90
|
):
|
|
88
|
-
if user
|
|
89
|
-
raise InvalidInputError(401, "Invalid authorization token
|
|
91
|
+
if isinstance(user, GuestUser):
|
|
92
|
+
raise InvalidInputError(401, "invalid_authorization_token", "Invalid authorization token, no user info found")
|
|
90
93
|
return login_helper(request, user, redirect_url)
|
|
91
94
|
|
|
92
95
|
# Provider authentication endpoints
|
|
@@ -109,7 +112,7 @@ class AuthRoutes(RouteBase):
|
|
|
109
112
|
|
|
110
113
|
@auth_router.get(provider_login_path, tags=["Authentication"])
|
|
111
114
|
async def provider_login(request: Request, provider_name: str, redirect_url: str | None = None) -> RedirectResponse:
|
|
112
|
-
"""
|
|
115
|
+
"""Redirect to the login URL for the OAuth provider"""
|
|
113
116
|
client = oauth.create_client(provider_name)
|
|
114
117
|
if client is None:
|
|
115
118
|
raise InvalidInputError(status_code=404, error="provider_not_found", error_description=f"Provider {provider_name} not found or configured.")
|
|
@@ -159,22 +162,23 @@ class AuthRoutes(RouteBase):
|
|
|
159
162
|
302: {"description": "Redirect if redirect URL parameter is specified"},
|
|
160
163
|
})
|
|
161
164
|
async def logout(request: Request, redirect_url: str | None = None):
|
|
165
|
+
"""Logout the current user, and redirect to the specified URL if provided"""
|
|
162
166
|
request.session.pop("access_token", None)
|
|
163
167
|
request.session.pop("access_token_expiry", None)
|
|
164
168
|
if redirect_url:
|
|
165
169
|
return RedirectResponse(url=redirect_url)
|
|
166
170
|
|
|
167
171
|
# Change password endpoint
|
|
168
|
-
change_password_path = '/
|
|
172
|
+
change_password_path = '/password'
|
|
169
173
|
|
|
170
174
|
class ChangePasswordRequest(BaseModel):
|
|
171
175
|
old_password: str
|
|
172
176
|
new_password: str
|
|
173
177
|
|
|
174
178
|
@auth_router.put(change_password_path, description="Change the password for the current user", tags=["Authentication"])
|
|
175
|
-
async def change_password(request: ChangePasswordRequest, user:
|
|
176
|
-
if user
|
|
177
|
-
raise InvalidInputError(401, "
|
|
179
|
+
async def change_password(request: ChangePasswordRequest, user: RegisteredUser | GuestUser = Depends(self.get_current_user)) -> None:
|
|
180
|
+
if isinstance(user, GuestUser):
|
|
181
|
+
raise InvalidInputError(401, "invalid_authorization_token", "Invalid authorization token")
|
|
178
182
|
self.authenticator.change_password(user.username, request.old_password, request.new_password)
|
|
179
183
|
|
|
180
184
|
# API Key endpoints
|
|
@@ -188,17 +192,17 @@ class AuthRoutes(RouteBase):
|
|
|
188
192
|
)
|
|
189
193
|
|
|
190
194
|
@auth_router.post(api_key_path, description="Create a new API key for the user", tags=["Authentication"])
|
|
191
|
-
async def create_api_key(body: ApiKeyRequestBody, user:
|
|
192
|
-
if user
|
|
193
|
-
raise InvalidInputError(401, "Invalid authorization token
|
|
195
|
+
async def create_api_key(body: ApiKeyRequestBody, user: RegisteredUser | GuestUser = Depends(self.get_current_user)) -> rm.ApiKeyResponse:
|
|
196
|
+
if isinstance(user, GuestUser):
|
|
197
|
+
raise InvalidInputError(401, "invalid_authorization_token", "Invalid authorization token, cannot create API key")
|
|
194
198
|
|
|
195
199
|
api_key, _ = self.authenticator.create_access_token(user, expiry_minutes=body.expiry_minutes, title=body.title)
|
|
196
200
|
return rm.ApiKeyResponse(api_key=api_key)
|
|
197
201
|
|
|
198
202
|
@auth_router.get(api_key_path, description="Get all API keys with title for the current user", tags=["Authentication"])
|
|
199
|
-
async def get_all_api_keys(user:
|
|
200
|
-
if user
|
|
201
|
-
raise InvalidInputError(401, "Invalid authorization token
|
|
203
|
+
async def get_all_api_keys(user: RegisteredUser | GuestUser = Depends(self.get_current_user)):
|
|
204
|
+
if isinstance(user, GuestUser):
|
|
205
|
+
raise InvalidInputError(401, "invalid_authorization_token", "Invalid authorization token, cannot get API keys")
|
|
202
206
|
return self.authenticator.get_all_api_keys(user.username)
|
|
203
207
|
|
|
204
208
|
revoke_api_key_path = '/api-key/{api_key_id}'
|
|
@@ -206,57 +210,62 @@ class AuthRoutes(RouteBase):
|
|
|
206
210
|
@auth_router.delete(revoke_api_key_path, description="Revoke an API key", tags=["Authentication"], responses={
|
|
207
211
|
204: { "description": "API key revoked successfully" }
|
|
208
212
|
})
|
|
209
|
-
async def revoke_api_key(api_key_id: str, user:
|
|
210
|
-
if user
|
|
211
|
-
raise InvalidInputError(401, "Invalid authorization token
|
|
213
|
+
async def revoke_api_key(api_key_id: str, user: RegisteredUser | GuestUser = Depends(self.get_current_user)) -> Response:
|
|
214
|
+
if isinstance(user, GuestUser):
|
|
215
|
+
raise InvalidInputError(401, "invalid_authorization_token", "Invalid authorization token, cannot revoke API key")
|
|
212
216
|
self.authenticator.revoke_api_key(user.username, api_key_id)
|
|
213
217
|
return Response(status_code=204)
|
|
214
218
|
|
|
215
|
-
# User management endpoints
|
|
216
|
-
|
|
219
|
+
# User management endpoints (disabled if external auth only)
|
|
220
|
+
if self.manifest_cfg.authentication.type.value == "managed":
|
|
221
|
+
user_fields_path = '/user-fields'
|
|
217
222
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
223
|
+
@user_management_router.get(user_fields_path, description="Get details of the user fields", tags=["User Management"])
|
|
224
|
+
async def get_user_fields():
|
|
225
|
+
return self.authenticator.user_fields
|
|
226
|
+
|
|
227
|
+
add_user_path = '/users'
|
|
228
|
+
|
|
229
|
+
@user_management_router.post(add_user_path, description="Add a new user by providing details for username, password, and user fields", tags=["User Management"])
|
|
230
|
+
async def add_user(
|
|
231
|
+
new_user: AddUserModel, user: AbstractUser = Depends(self.get_current_user)
|
|
232
|
+
) -> None:
|
|
233
|
+
if user.access_level != "admin":
|
|
234
|
+
raise InvalidInputError(403, "unauthorized_to_add_user", "Current user cannot add new users")
|
|
235
|
+
self.authenticator.add_user(new_user.username, new_user.model_dump(mode='json', exclude={"username"}))
|
|
236
|
+
|
|
237
|
+
update_user_path = '/users/{username}'
|
|
238
|
+
|
|
239
|
+
@user_management_router.put(update_user_path, description="Update the user of the given username given the new user details", tags=["User Management"])
|
|
240
|
+
async def update_user(
|
|
241
|
+
username: str, updated_user: UpdateUserModel, user: AbstractUser = Depends(self.get_current_user)
|
|
242
|
+
) -> None:
|
|
243
|
+
if user.access_level != "admin":
|
|
244
|
+
raise InvalidInputError(403, "unauthorized_to_update_user", "Current user cannot update users")
|
|
245
|
+
self.authenticator.add_user(username, updated_user.model_dump(mode='json'), update_user=True)
|
|
246
|
+
|
|
247
|
+
list_users_path = '/users'
|
|
248
|
+
|
|
249
|
+
@user_management_router.get(list_users_path, tags=["User Management"])
|
|
250
|
+
async def list_all_users() -> list[UserInfoModel]:
|
|
251
|
+
registered_users = self.authenticator.get_all_users()
|
|
252
|
+
return [
|
|
253
|
+
UserInfoModel(username=user.username, access_level=user.access_level, **user.custom_fields.model_dump(mode='json'))
|
|
254
|
+
for user in registered_users
|
|
255
|
+
]
|
|
256
|
+
|
|
257
|
+
delete_user_path = '/users/{username}'
|
|
258
|
+
|
|
259
|
+
@user_management_router.delete(delete_user_path, tags=["User Management"], responses={
|
|
260
|
+
204: { "description": "User deleted successfully" }
|
|
261
|
+
})
|
|
262
|
+
async def delete_user(username: str, user: AbstractUser = Depends(self.get_current_user)) -> Response:
|
|
263
|
+
if user.access_level != "admin":
|
|
264
|
+
raise InvalidInputError(403, "unauthorized_to_delete_user", "Current user cannot delete users")
|
|
265
|
+
if username == user.username:
|
|
266
|
+
raise InvalidInputError(403, "cannot_delete_own_user", "Cannot delete your own user")
|
|
267
|
+
self.authenticator.delete_user(username)
|
|
268
|
+
return Response(status_code=204)
|
|
260
269
|
|
|
261
270
|
app.include_router(auth_router)
|
|
262
271
|
app.include_router(user_management_router)
|