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.

Files changed (109) hide show
  1. {squirrels-0.5.0b4 → squirrels-0.5.1}/PKG-INFO +28 -13
  2. {squirrels-0.5.0b4 → squirrels-0.5.1}/README.md +24 -8
  3. {squirrels-0.5.0b4 → squirrels-0.5.1}/pyproject.toml +13 -14
  4. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/__init__.py +2 -0
  5. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_api_routes/auth.py +83 -74
  6. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_api_routes/base.py +58 -41
  7. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_api_routes/dashboards.py +37 -21
  8. squirrels-0.5.1/squirrels/_api_routes/data_management.py +148 -0
  9. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_api_routes/datasets.py +107 -84
  10. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_api_routes/oauth2.py +11 -13
  11. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_api_routes/project.py +71 -33
  12. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_api_server.py +130 -63
  13. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_arguments/run_time_args.py +9 -9
  14. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_auth.py +117 -162
  15. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_command_line.py +68 -32
  16. squirrels-0.5.1/squirrels/_compile_prompts.py +147 -0
  17. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_connection_set.py +11 -2
  18. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_constants.py +22 -8
  19. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_data_sources.py +38 -32
  20. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_dataset_types.py +2 -4
  21. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_initializer.py +1 -1
  22. squirrels-0.5.1/squirrels/_logging.py +117 -0
  23. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_manifest.py +125 -58
  24. squirrels-0.5.1/squirrels/_model_builder.py +69 -0
  25. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_models.py +224 -108
  26. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/.env +15 -4
  27. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/.env.example +14 -3
  28. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/connections.yml +4 -3
  29. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/dashboards/dashboard_example.py +2 -2
  30. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/dashboards/dashboard_example.yml +4 -4
  31. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/duckdb_init.sql +1 -0
  32. squirrels-0.5.1/squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +17 -0
  33. squirrels-0.5.1/squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +32 -0
  34. squirrels-0.5.1/squirrels/_package_data/base_project/models/federates/federate_example.py +48 -0
  35. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/models/federates/federate_example.sql +3 -7
  36. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/models/federates/federate_example.yml +1 -1
  37. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/models/sources.yml +5 -6
  38. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/parameters.yml +24 -38
  39. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/pyconfigs/connections.py +5 -1
  40. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/pyconfigs/context.py +23 -12
  41. squirrels-0.5.1/squirrels/_package_data/base_project/pyconfigs/parameters.py +141 -0
  42. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/pyconfigs/user.py +11 -18
  43. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/seeds/seed_categories.yml +1 -1
  44. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/seeds/seed_subcategories.yml +1 -1
  45. squirrels-0.5.1/squirrels/_package_data/base_project/squirrels.yml.j2 +61 -0
  46. squirrels-0.5.1/squirrels/_package_data/templates/squirrels_studio.html +20 -0
  47. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_parameter_configs.py +43 -22
  48. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_parameter_options.py +1 -1
  49. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_parameter_sets.py +8 -10
  50. squirrels-0.5.1/squirrels/_project.py +722 -0
  51. squirrels-0.5.1/squirrels/_request_context.py +33 -0
  52. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_schemas/auth_models.py +32 -9
  53. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_schemas/query_param_models.py +9 -1
  54. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_schemas/response_models.py +36 -10
  55. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_seeds.py +1 -1
  56. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_sources.py +23 -19
  57. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_utils.py +83 -35
  58. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_version.py +1 -1
  59. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/arguments.py +5 -0
  60. squirrels-0.5.1/squirrels/auth.py +4 -0
  61. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/connections.py +2 -0
  62. squirrels-0.5.1/squirrels/dashboards.py +3 -0
  63. squirrels-0.5.1/squirrels/data_sources.py +14 -0
  64. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/parameter_options.py +5 -0
  65. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/parameters.py +5 -0
  66. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/types.py +6 -1
  67. squirrels-0.5.0b4/squirrels/_api_routes/data_management.py +0 -103
  68. squirrels-0.5.0b4/squirrels/_model_builder.py +0 -113
  69. squirrels-0.5.0b4/squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +0 -12
  70. squirrels-0.5.0b4/squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +0 -26
  71. squirrels-0.5.0b4/squirrels/_package_data/base_project/models/federates/federate_example.py +0 -41
  72. squirrels-0.5.0b4/squirrels/_package_data/base_project/pyconfigs/parameters.py +0 -106
  73. squirrels-0.5.0b4/squirrels/_package_data/base_project/squirrels.yml.j2 +0 -71
  74. squirrels-0.5.0b4/squirrels/_project.py +0 -605
  75. squirrels-0.5.0b4/squirrels/auth.py +0 -1
  76. squirrels-0.5.0b4/squirrels/dashboards.py +0 -1
  77. squirrels-0.5.0b4/squirrels/data_sources.py +0 -8
  78. {squirrels-0.5.0b4 → squirrels-0.5.1}/.gitignore +0 -0
  79. {squirrels-0.5.0b4 → squirrels-0.5.1}/LICENSE +0 -0
  80. {squirrels-0.5.0b4 → squirrels-0.5.1}/dateutils/__init__.py +0 -0
  81. {squirrels-0.5.0b4 → squirrels-0.5.1}/dateutils/_enums.py +0 -0
  82. {squirrels-0.5.0b4 → squirrels-0.5.1}/dateutils/_implementation.py +0 -0
  83. {squirrels-0.5.0b4 → squirrels-0.5.1}/dateutils/types.py +0 -0
  84. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_api_routes/__init__.py +0 -0
  85. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_arguments/__init__.py +0 -0
  86. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_arguments/init_time_args.py +0 -0
  87. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_dashboards.py +0 -0
  88. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_exceptions.py +0 -0
  89. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_model_configs.py +0 -0
  90. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_model_queries.py +0 -0
  91. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/assets/expenses.db +0 -0
  92. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/assets/weather.db +0 -0
  93. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/docker/.dockerignore +0 -0
  94. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/docker/Dockerfile +0 -0
  95. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/docker/compose.yml +0 -0
  96. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/gitignore +0 -0
  97. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/macros/macros_example.sql +0 -0
  98. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/models/builds/build_example.py +0 -0
  99. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/models/builds/build_example.sql +0 -0
  100. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/models/builds/build_example.yml +0 -0
  101. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/seeds/seed_categories.csv +0 -0
  102. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/seeds/seed_subcategories.csv +0 -0
  103. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/base_project/tmp/.gitignore +0 -0
  104. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/templates/dataset_results.html +0 -0
  105. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_data/templates/oauth_login.html +0 -0
  106. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_package_loader.py +0 -0
  107. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_parameters.py +0 -0
  108. {squirrels-0.5.0b4 → squirrels-0.5.1}/squirrels/_py_module.py +0 -0
  109. {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.0b4
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<6,>=5.3.2
19
- Requires-Dist: duckdb<2,>=1.1.3
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.9.2
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 squirrels](#contributing-to-squirrels)
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 squirrels can do:
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 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.
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 squirrels
92
+ ## Contributing to Squirrels
78
93
 
79
- 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.
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 squirrels CLI commands:
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 squirrels command is doing, start from the `_command_line.py` file as your entry point.
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 squirrels](#contributing-to-squirrels)
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 squirrels can do:
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 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.
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 squirrels
52
+ ## Contributing to Squirrels
37
53
 
38
- 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.
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 squirrels CLI commands:
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 squirrels command is doing, start from the `_command_line.py` file as your entry point.
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.0b4"
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,<6",
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.1.3,<2",
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.9.2",
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,<8"]
51
+ test = ["pytest>=7.4.4"]
53
52
  dev = [
54
- "ipykernel>=6.29.4,<7",
55
- "plotly>=5.24.0,<6",
56
- "psycopg2-binary>=2.9.10,<3",
57
- "adbc-driver-postgresql>=1.3.0,<2",
58
- "adbc-driver-sqlite>=1.3.0,<2",
59
- "connectorx>=0.4.0,<0.5",
60
- "faker>=33.1.0,<34",
61
- "tqdm>=4.67.1,<5",
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]
@@ -17,3 +17,5 @@ from .dashboards import *
17
17
  from .types import *
18
18
 
19
19
  from ._project import SquirrelsProject
20
+
21
+ __all__ = ["SquirrelsProject"]
@@ -1,17 +1,16 @@
1
1
  """
2
2
  Authentication and user management routes
3
3
  """
4
- from typing import Annotated, Callable
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 .._auth import BaseUser
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
- auth_router = APIRouter(prefix="/api/auth")
28
- user_management_router = APIRouter(prefix="/api/auth/user-management")
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.UserModel):
35
- is_admin: bool
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: UserInfoModel | None = Depends(self.get_current_user)) -> UserInfoModel:
58
- if user is None:
59
- raise InvalidInputError(401, "Invalid authorization token", "Invalid authorization token")
60
- return user
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: BaseUser, redirect_url: str | None, *,
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: UserInfoModel | None = Depends(self.get_current_user)
89
+ request: Request, redirect_url: str | None = None, user: RegisteredUser | GuestUser = Depends(self.get_current_user)
87
90
  ):
88
- if user is None:
89
- raise InvalidInputError(401, "Invalid authorization token", "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
- """Get OAuth login URL for the provider"""
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 = '/change-password'
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: UserInfoModel | None = Depends(self.get_current_user)) -> None:
176
- if user is None:
177
- raise InvalidInputError(401, "Invalid authorization token", "Invalid authorization token")
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: UserInfoModel | None = Depends(self.get_current_user)) -> rm.ApiKeyResponse:
192
- if user is None:
193
- raise InvalidInputError(401, "Invalid authorization token", "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: UserInfoModel | None = Depends(self.get_current_user)):
200
- if user is None:
201
- raise InvalidInputError(401, "Invalid authorization token", "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: UserInfoModel | None = Depends(self.get_current_user)) -> Response:
210
- if user is None:
211
- raise InvalidInputError(401, "Invalid authorization token", "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
- user_fields_path = '/user-fields'
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
- @user_management_router.get(user_fields_path, description="Get details of the user fields", tags=["User Management"])
219
- async def get_user_fields():
220
- return self.authenticator.user_fields
221
-
222
- add_user_path = '/users'
223
-
224
- @user_management_router.post(add_user_path, description="Add a new user by providing details for username, password, and user fields", tags=["User Management"])
225
- async def add_user(
226
- new_user: AddUserModel, user: UserInfoModel | None = Depends(self.get_current_user)
227
- ) -> None:
228
- if user is None or not user.is_admin:
229
- raise InvalidInputError(403, "Forbidden to add user", "Authorized user is forbidden to add new users")
230
- self.authenticator.add_user(new_user.username, new_user.model_dump(mode='json', exclude={"username"}))
231
-
232
- update_user_path = '/users/{username}'
233
-
234
- @user_management_router.put(update_user_path, description="Update the user of the given username given the new user details", tags=["User Management"])
235
- async def update_user(
236
- username: str, updated_user: UpdateUserModel, user: UserInfoModel | None = Depends(self.get_current_user)
237
- ) -> None:
238
- if user is None or not user.is_admin:
239
- raise InvalidInputError(403, "Forbidden to update user", "Authorized user is forbidden to update users")
240
- self.authenticator.add_user(username, updated_user.model_dump(mode='json'), update_user=True)
241
-
242
- list_users_path = '/users'
243
-
244
- @user_management_router.get(list_users_path, tags=["User Management"])
245
- async def list_all_users():
246
- return self.authenticator.get_all_users()
247
-
248
- delete_user_path = '/users/{username}'
249
-
250
- @user_management_router.delete(delete_user_path, tags=["User Management"], responses={
251
- 204: { "description": "User deleted successfully" }
252
- })
253
- async def delete_user(username: str, user: UserInfoModel | None = Depends(self.get_current_user)) -> Response:
254
- if user is None or not user.is_admin:
255
- raise InvalidInputError(403, "Forbidden to delete user", "Authorized user is forbidden to delete users")
256
- if username == user.username:
257
- raise InvalidInputError(403, "Cannot delete your own user", "Cannot delete your own user")
258
- self.authenticator.delete_user(username)
259
- return Response(status_code=204)
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)