squirrels 0.1.1.post1__tar.gz → 0.2.0.dev0__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.2.0.dev0/PKG-INFO +126 -0
- squirrels-0.2.0.dev0/README.md +90 -0
- squirrels-0.2.0.dev0/pyproject.toml +45 -0
- squirrels-0.2.0.dev0/squirrels/__init__.py +12 -0
- squirrels-0.2.0.dev0/squirrels/_api_server.py +288 -0
- squirrels-0.2.0.dev0/squirrels/_authenticator.py +84 -0
- squirrels-0.2.0.dev0/squirrels/_command_line.py +95 -0
- squirrels-0.2.0.dev0/squirrels/_connection_set.py +96 -0
- squirrels-0.2.0.dev0/squirrels/_constants.py +145 -0
- squirrels-0.2.0.dev0/squirrels/_environcfg.py +77 -0
- squirrels-0.2.0.dev0/squirrels/_initializer.py +169 -0
- squirrels-0.2.0.dev0/squirrels/_manifest.py +214 -0
- squirrels-0.2.0.dev0/squirrels/_models.py +495 -0
- squirrels-0.2.0.dev0/squirrels/_package_loader.py +26 -0
- squirrels-0.2.0.dev0/squirrels/_parameter_configs.py +401 -0
- squirrels-0.2.0.dev0/squirrels/_parameter_sets.py +188 -0
- squirrels-0.2.0.dev0/squirrels/_py_module.py +60 -0
- squirrels-0.2.0.dev0/squirrels/_timer.py +36 -0
- squirrels-0.2.0.dev0/squirrels/_utils.py +181 -0
- squirrels-0.2.0.dev0/squirrels/_version.py +3 -0
- squirrels-0.2.0.dev0/squirrels/arguments/init_time_args.py +32 -0
- squirrels-0.2.0.dev0/squirrels/arguments/run_time_args.py +82 -0
- squirrels-0.2.0.dev0/squirrels/data_sources.py +515 -0
- {squirrels-0.1.1.post1 → squirrels-0.2.0.dev0}/squirrels/dateutils.py +86 -57
- squirrels-0.2.0.dev0/squirrels/package_data/base_project/Dockerfile +15 -0
- squirrels-0.2.0.dev0/squirrels/package_data/base_project/connections.yml +7 -0
- squirrels-0.1.1.post1/squirrels/package_data/base_project/database/sample_database.db → squirrels-0.2.0.dev0/squirrels/package_data/base_project/database/expenses.db +0 -0
- squirrels-0.2.0.dev0/squirrels/package_data/base_project/environcfg.yml +29 -0
- squirrels-0.2.0.dev0/squirrels/package_data/base_project/ignores/.dockerignore +8 -0
- squirrels-0.2.0.dev0/squirrels/package_data/base_project/ignores/.gitignore +7 -0
- squirrels-0.2.0.dev0/squirrels/package_data/base_project/models/dbviews/database_view1.py +36 -0
- squirrels-0.2.0.dev0/squirrels/package_data/base_project/models/dbviews/database_view1.sql +15 -0
- squirrels-0.2.0.dev0/squirrels/package_data/base_project/models/federates/dataset_example.py +20 -0
- squirrels-0.2.0.dev0/squirrels/package_data/base_project/models/federates/dataset_example.sql +3 -0
- squirrels-0.2.0.dev0/squirrels/package_data/base_project/parameters.yml +109 -0
- squirrels-0.2.0.dev0/squirrels/package_data/base_project/pyconfigs/auth.py +47 -0
- squirrels-0.2.0.dev0/squirrels/package_data/base_project/pyconfigs/connections.py +28 -0
- squirrels-0.2.0.dev0/squirrels/package_data/base_project/pyconfigs/context.py +45 -0
- squirrels-0.2.0.dev0/squirrels/package_data/base_project/pyconfigs/parameters.py +55 -0
- squirrels-0.2.0.dev0/squirrels/package_data/base_project/seeds/mocks/category.csv +3 -0
- squirrels-0.2.0.dev0/squirrels/package_data/base_project/seeds/mocks/max_filter.csv +2 -0
- squirrels-0.2.0.dev0/squirrels/package_data/base_project/seeds/mocks/subcategory.csv +6 -0
- squirrels-0.2.0.dev0/squirrels/package_data/base_project/squirrels.yml.j2 +57 -0
- squirrels-0.2.0.dev0/squirrels/package_data/base_project/tmp/.gitignore +2 -0
- {squirrels-0.1.1.post1 → squirrels-0.2.0.dev0}/squirrels/package_data/static/script.js +159 -63
- {squirrels-0.1.1.post1 → squirrels-0.2.0.dev0}/squirrels/package_data/static/style.css +79 -15
- squirrels-0.2.0.dev0/squirrels/package_data/static/widgets.js +133 -0
- squirrels-0.2.0.dev0/squirrels/package_data/templates/index.html +74 -0
- squirrels-0.2.0.dev0/squirrels/package_data/templates/index2.html +22 -0
- squirrels-0.2.0.dev0/squirrels/parameter_options.py +330 -0
- squirrels-0.2.0.dev0/squirrels/parameters.py +755 -0
- squirrels-0.2.0.dev0/squirrels/user_base.py +58 -0
- squirrels-0.1.1.post1/PKG-INFO +0 -56
- squirrels-0.1.1.post1/README.md +0 -46
- squirrels-0.1.1.post1/setup.cfg +0 -4
- squirrels-0.1.1.post1/setup.py +0 -42
- squirrels-0.1.1.post1/squirrels/__init__.py +0 -18
- squirrels-0.1.1.post1/squirrels/_api_server.py +0 -134
- squirrels-0.1.1.post1/squirrels/_command_line.py +0 -107
- squirrels-0.1.1.post1/squirrels/_constants.py +0 -64
- squirrels-0.1.1.post1/squirrels/_credentials_manager.py +0 -87
- squirrels-0.1.1.post1/squirrels/_initializer.py +0 -110
- squirrels-0.1.1.post1/squirrels/_manifest.py +0 -187
- squirrels-0.1.1.post1/squirrels/_module_loader.py +0 -37
- squirrels-0.1.1.post1/squirrels/_parameter_set.py +0 -151
- squirrels-0.1.1.post1/squirrels/_renderer.py +0 -286
- squirrels-0.1.1.post1/squirrels/_timed_imports.py +0 -37
- squirrels-0.1.1.post1/squirrels/_utils.py +0 -149
- squirrels-0.1.1.post1/squirrels/_version.py +0 -3
- squirrels-0.1.1.post1/squirrels/connection_set.py +0 -126
- squirrels-0.1.1.post1/squirrels/data_sources.py +0 -290
- squirrels-0.1.1.post1/squirrels/package_data/base_project/.gitignore +0 -4
- squirrels-0.1.1.post1/squirrels/package_data/base_project/connections.py +0 -20
- squirrels-0.1.1.post1/squirrels/package_data/base_project/datasets/sample_dataset/context.py +0 -22
- squirrels-0.1.1.post1/squirrels/package_data/base_project/datasets/sample_dataset/database_view1.py +0 -29
- squirrels-0.1.1.post1/squirrels/package_data/base_project/datasets/sample_dataset/database_view1.sql.j2 +0 -12
- squirrels-0.1.1.post1/squirrels/package_data/base_project/datasets/sample_dataset/final_view.py +0 -11
- squirrels-0.1.1.post1/squirrels/package_data/base_project/datasets/sample_dataset/final_view.sql.j2 +0 -3
- squirrels-0.1.1.post1/squirrels/package_data/base_project/datasets/sample_dataset/parameters.py +0 -47
- squirrels-0.1.1.post1/squirrels/package_data/base_project/datasets/sample_dataset/selections.cfg +0 -9
- squirrels-0.1.1.post1/squirrels/package_data/base_project/squirrels.yaml +0 -22
- squirrels-0.1.1.post1/squirrels/package_data/templates/index.html +0 -32
- squirrels-0.1.1.post1/squirrels/parameter_options.py +0 -233
- squirrels-0.1.1.post1/squirrels/parameters.py +0 -826
- squirrels-0.1.1.post1/squirrels.egg-info/PKG-INFO +0 -56
- squirrels-0.1.1.post1/squirrels.egg-info/SOURCES.txt +0 -43
- squirrels-0.1.1.post1/squirrels.egg-info/dependency_links.txt +0 -1
- squirrels-0.1.1.post1/squirrels.egg-info/entry_points.txt +0 -2
- squirrels-0.1.1.post1/squirrels.egg-info/requires.txt +0 -11
- squirrels-0.1.1.post1/squirrels.egg-info/top_level.txt +0 -1
- {squirrels-0.1.1.post1 → squirrels-0.2.0.dev0}/LICENSE +0 -0
- {squirrels-0.1.1.post1 → squirrels-0.2.0.dev0}/squirrels/package_data/base_project/database/seattle_weather.db +0 -0
- {squirrels-0.1.1.post1 → squirrels-0.2.0.dev0}/squirrels/package_data/static/favicon.ico +0 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: squirrels
|
|
3
|
+
Version: 0.2.0.dev0
|
|
4
|
+
Summary: Squirrels - API Framework for Data Analytics
|
|
5
|
+
Home-page: https://squirrels-nest.github.io/squirrels-docs/
|
|
6
|
+
License: MIT
|
|
7
|
+
Author: Tim Huang
|
|
8
|
+
Author-email: tim.yuting@hotmail.com
|
|
9
|
+
Requires-Python: >=3.9,<4.0
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Dist: cachetools (>=5.3.2,<6.0.0)
|
|
21
|
+
Requires-Dist: cryptography (>=41.0.7,<42.0.0)
|
|
22
|
+
Requires-Dist: fastapi (>=0.104.1,<0.105.0)
|
|
23
|
+
Requires-Dist: gitpython (>=3.1.40,<4.0.0)
|
|
24
|
+
Requires-Dist: inquirer (>=3.1.4,<4.0.0)
|
|
25
|
+
Requires-Dist: jinja2 (>=3.1.2,<4.0.0)
|
|
26
|
+
Requires-Dist: pandas (>=2.1.4,<3.0.0)
|
|
27
|
+
Requires-Dist: python-jose (>=3.3.0,<4.0.0)
|
|
28
|
+
Requires-Dist: python-multipart (>=0.0.6,<0.0.7)
|
|
29
|
+
Requires-Dist: pyyaml (>=6.0.1,<7.0.0)
|
|
30
|
+
Requires-Dist: sqlalchemy (>=2.0.23,<3.0.0)
|
|
31
|
+
Requires-Dist: uvicorn (>=0.24.0.post1,<0.25.0)
|
|
32
|
+
Project-URL: Documentation, https://squirrels-nest.github.io/squirrels-docs/
|
|
33
|
+
Project-URL: Repository, https://github.com/squirrels-nest/squirrels
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# Squirrels
|
|
37
|
+
|
|
38
|
+
Squirrels is an API framework that lets you create REST APIs for dynamic data analytics!
|
|
39
|
+
|
|
40
|
+
**Documentation**: <a href="https://squirrels-nest.github.io/squirrels-docs" target="_blank">https://squirrels-nest.github.io/squirrels-docs</a>
|
|
41
|
+
|
|
42
|
+
**Source Code**: <a href="https://github.com/squirrels-nest/squirrels" target="_blank">https://github.com/squirrels-nest/squirrels</a>
|
|
43
|
+
|
|
44
|
+
## Table of Contents
|
|
45
|
+
|
|
46
|
+
- [Main Features](#main-features)
|
|
47
|
+
- [License](#license)
|
|
48
|
+
- [Contributing to squirrels](#contributing-to-squirrels)
|
|
49
|
+
- [Setup](#setup)
|
|
50
|
+
- [Testing](#testing)
|
|
51
|
+
- [Project Structure](#project-structure)
|
|
52
|
+
|
|
53
|
+
## Main Features
|
|
54
|
+
|
|
55
|
+
Here are a few of the things that squirrels can do:
|
|
56
|
+
|
|
57
|
+
- Connect to any database by specifying its sqlalchemy url without code (in `squirrels.yml`) or by using its native connector library in python (in `connections.py`).
|
|
58
|
+
- Configure API routes without code (in `squirrels.yml`) for all datasets.
|
|
59
|
+
- Configure parameter widgets (types include single-select, multi-select, date, number, etc.) for your datasets (in `parameters.py`).
|
|
60
|
+
- Use Jinja SQL templates (just like dbt!) or python functions (that return a pandas dataframe) to define dynamic query logic based on parameter selections.
|
|
61
|
+
- Query multiple databases and join the results together in a final view in one API endpoint/dataset!
|
|
62
|
+
- 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).
|
|
63
|
+
- 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!
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
Squirrels is released under the MIT license.
|
|
68
|
+
|
|
69
|
+
See the file LICENSE for more details.
|
|
70
|
+
|
|
71
|
+
## Contributing to squirrels
|
|
72
|
+
|
|
73
|
+
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.
|
|
74
|
+
|
|
75
|
+
### Setup
|
|
76
|
+
|
|
77
|
+
This project requires python version 3.9 or above to be installed. It also uses the python build tool `poetry` which can be installed as follows.
|
|
78
|
+
|
|
79
|
+
**Linux, MacOS, Windows (WSL):**
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
curl -sSL https://install.python-poetry.org | python3 -
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Windows (Powershell):**
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | py -
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Then, to install all dependencies, run:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
poetry install
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
And activate the virtual environment created by poetry with:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
poetry shell
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
To confirm that the setup worked, run the following to show the help page for all squirrels CLI commands:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
squirrels -h
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
You can enter `exit` to exit the virtual environment shell. You can also run `poetry run squirrels -h` to run squirrels commands without activating the virtual environment.
|
|
110
|
+
|
|
111
|
+
### Testing
|
|
112
|
+
|
|
113
|
+
In poetry's virtual environment, run `pytest`.
|
|
114
|
+
|
|
115
|
+
### Project Structure
|
|
116
|
+
|
|
117
|
+
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.
|
|
118
|
+
|
|
119
|
+
To understand what a specific squirrels command is doing, start from the `_command_line.py` file as your entry point.
|
|
120
|
+
|
|
121
|
+
The library version is maintained in both the `pyproject.toml` and the `squirrels/__init__.py` files.
|
|
122
|
+
|
|
123
|
+
When a user initializes a squirrels project using `squirrels init`, the files are copied from the `squirrels/package_data/base_project` folder. The contents in the `database` subfolder were constructed from the scripts in the `database_elt` folder.
|
|
124
|
+
|
|
125
|
+
For the Squirrels UI activated by `squirrels run`, the HTML, CSS, and Javascript files can be found in the `static` and `templates` subfolders of `squirrels/package_data`.
|
|
126
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Squirrels
|
|
2
|
+
|
|
3
|
+
Squirrels is an API framework that lets you create REST APIs for dynamic data analytics!
|
|
4
|
+
|
|
5
|
+
**Documentation**: <a href="https://squirrels-nest.github.io/squirrels-docs" target="_blank">https://squirrels-nest.github.io/squirrels-docs</a>
|
|
6
|
+
|
|
7
|
+
**Source Code**: <a href="https://github.com/squirrels-nest/squirrels" target="_blank">https://github.com/squirrels-nest/squirrels</a>
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Main Features](#main-features)
|
|
12
|
+
- [License](#license)
|
|
13
|
+
- [Contributing to squirrels](#contributing-to-squirrels)
|
|
14
|
+
- [Setup](#setup)
|
|
15
|
+
- [Testing](#testing)
|
|
16
|
+
- [Project Structure](#project-structure)
|
|
17
|
+
|
|
18
|
+
## Main Features
|
|
19
|
+
|
|
20
|
+
Here are a few of the things that squirrels can do:
|
|
21
|
+
|
|
22
|
+
- Connect to any database by specifying its sqlalchemy url without code (in `squirrels.yml`) or by using its native connector library in python (in `connections.py`).
|
|
23
|
+
- Configure API routes without code (in `squirrels.yml`) for all datasets.
|
|
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 pandas dataframe) to define dynamic query logic based on parameter selections.
|
|
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!
|
|
29
|
+
|
|
30
|
+
## License
|
|
31
|
+
|
|
32
|
+
Squirrels is released under the MIT license.
|
|
33
|
+
|
|
34
|
+
See the file LICENSE for more details.
|
|
35
|
+
|
|
36
|
+
## Contributing to squirrels
|
|
37
|
+
|
|
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.
|
|
39
|
+
|
|
40
|
+
### Setup
|
|
41
|
+
|
|
42
|
+
This project requires python version 3.9 or above to be installed. It also uses the python build tool `poetry` which can be installed as follows.
|
|
43
|
+
|
|
44
|
+
**Linux, MacOS, Windows (WSL):**
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
curl -sSL https://install.python-poetry.org | python3 -
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
**Windows (Powershell):**
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
(Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | py -
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Then, to install all dependencies, run:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
poetry install
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
And activate the virtual environment created by poetry with:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
poetry shell
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
To confirm that the setup worked, run the following to show the help page for all squirrels CLI commands:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
squirrels -h
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
You can enter `exit` to exit the virtual environment shell. You can also run `poetry run squirrels -h` to run squirrels commands without activating the virtual environment.
|
|
75
|
+
|
|
76
|
+
### Testing
|
|
77
|
+
|
|
78
|
+
In poetry's virtual environment, run `pytest`.
|
|
79
|
+
|
|
80
|
+
### Project Structure
|
|
81
|
+
|
|
82
|
+
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.
|
|
83
|
+
|
|
84
|
+
To understand what a specific squirrels command is doing, start from the `_command_line.py` file as your entry point.
|
|
85
|
+
|
|
86
|
+
The library version is maintained in both the `pyproject.toml` and the `squirrels/__init__.py` files.
|
|
87
|
+
|
|
88
|
+
When a user initializes a squirrels project using `squirrels init`, the files are copied from the `squirrels/package_data/base_project` folder. The contents in the `database` subfolder were constructed from the scripts in the `database_elt` folder.
|
|
89
|
+
|
|
90
|
+
For the Squirrels UI activated by `squirrels run`, the HTML, CSS, and Javascript files can be found in the `static` and `templates` subfolders of `squirrels/package_data`.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "squirrels"
|
|
3
|
+
version = "0.2.0.dev0"
|
|
4
|
+
description = "Squirrels - API Framework for Data Analytics"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
authors = ["Tim Huang <tim.yuting@hotmail.com>"]
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
homepage = "https://squirrels-nest.github.io/squirrels-docs/"
|
|
9
|
+
repository = "https://github.com/squirrels-nest/squirrels"
|
|
10
|
+
documentation = "https://squirrels-nest.github.io/squirrels-docs/"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Topic :: Software Development :: Libraries :: Application Frameworks",
|
|
14
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
15
|
+
"Typing :: Typed",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[tool.poetry.scripts]
|
|
19
|
+
squirrels = "squirrels._command_line:main"
|
|
20
|
+
|
|
21
|
+
[tool.poetry.dependencies]
|
|
22
|
+
python = "^3.9"
|
|
23
|
+
cachetools = "^5.3.2"
|
|
24
|
+
cryptography = "^41.0.7"
|
|
25
|
+
fastapi = "^0.104.1"
|
|
26
|
+
gitpython = "^3.1.40"
|
|
27
|
+
inquirer = "^3.1.4"
|
|
28
|
+
jinja2 = "^3.1.2"
|
|
29
|
+
pandas = "^2.1.4"
|
|
30
|
+
python-jose = "^3.3.0"
|
|
31
|
+
python-multipart = "^0.0.6"
|
|
32
|
+
pyyaml = "^6.0.1"
|
|
33
|
+
sqlalchemy = "^2.0.23"
|
|
34
|
+
uvicorn = "^0.24.0.post1"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
[tool.poetry.group.test.dependencies]
|
|
38
|
+
pytest = "*"
|
|
39
|
+
|
|
40
|
+
[tool.poetry.group.dev.dependencies]
|
|
41
|
+
ipykernel = "*"
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["poetry-core"]
|
|
45
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
__version__ = '0.2.0'
|
|
2
|
+
|
|
3
|
+
from typing import Union
|
|
4
|
+
from sqlalchemy import Engine, Pool
|
|
5
|
+
from pandas import DataFrame
|
|
6
|
+
|
|
7
|
+
from .arguments.init_time_args import ConnectionsArgs, ParametersArgs
|
|
8
|
+
from .arguments.run_time_args import ContextArgs, ModelDepsArgs, ModelArgs
|
|
9
|
+
from .parameter_options import SelectParameterOption, DateParameterOption, DateRangeParameterOption, NumberParameterOption, NumberRangeParameterOption
|
|
10
|
+
from .parameters import Parameter, SingleSelectParameter, MultiSelectParameter, DateParameter, DateRangeParameter, NumberParameter, NumberRangeParameter
|
|
11
|
+
from .data_sources import SingleSelectDataSource, MultiSelectDataSource, DateDataSource, DateRangeDataSource, NumberDataSource, NumberRangeDataSource
|
|
12
|
+
from .user_base import User, WrongPassword
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
from typing import Iterable, Optional, Mapping, Callable, Coroutine, TypeVar, Any
|
|
2
|
+
from fastapi import Depends, FastAPI, Request, HTTPException, status
|
|
3
|
+
from fastapi.responses import HTMLResponse, JSONResponse
|
|
4
|
+
from fastapi.templating import Jinja2Templates
|
|
5
|
+
from fastapi.staticfiles import StaticFiles
|
|
6
|
+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
|
7
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
8
|
+
from cachetools import TTLCache
|
|
9
|
+
import os, traceback, pandas as pd
|
|
10
|
+
|
|
11
|
+
from . import _constants as c, _utils as u
|
|
12
|
+
from ._version import sq_major_version
|
|
13
|
+
from ._manifest import ManifestIO
|
|
14
|
+
from ._authenticator import User, Authenticator
|
|
15
|
+
from ._timer import timer, time
|
|
16
|
+
from ._parameter_sets import ParameterSet
|
|
17
|
+
from ._models import ModelsIO
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ApiServer:
|
|
21
|
+
def __init__(self, no_cache: bool, debug: bool) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Constructor for ApiServer
|
|
24
|
+
|
|
25
|
+
Parameters:
|
|
26
|
+
no_cache (bool): Whether to disable caching
|
|
27
|
+
debug (bool): Set to True to show "hidden" parameters in the /parameters endpoint response
|
|
28
|
+
"""
|
|
29
|
+
self.no_cache = no_cache
|
|
30
|
+
self.debug = debug
|
|
31
|
+
self.dataset_configs = ManifestIO.obj.datasets
|
|
32
|
+
|
|
33
|
+
token_expiry_minutes = ManifestIO.obj.settings.get(c.AUTH_TOKEN_EXPIRE_SETTING, 30)
|
|
34
|
+
self.authenticator = Authenticator(token_expiry_minutes)
|
|
35
|
+
|
|
36
|
+
def run(self, uvicorn_args: list[str]) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Runs the API server with uvicorn for CLI "squirrels run"
|
|
39
|
+
|
|
40
|
+
Parameters:
|
|
41
|
+
uvicorn_args: List of arguments to pass to uvicorn.run. Currently only supports "host" and "port"
|
|
42
|
+
"""
|
|
43
|
+
start = time.time()
|
|
44
|
+
app = FastAPI()
|
|
45
|
+
|
|
46
|
+
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
|
|
47
|
+
|
|
48
|
+
squirrels_version_path = f'/squirrels-v{sq_major_version}'
|
|
49
|
+
partial_base_path = f'/{ManifestIO.obj.project_variables.get_name()}/v{ManifestIO.obj.project_variables.get_major_version()}'
|
|
50
|
+
base_path = squirrels_version_path + u.normalize_name_for_api(partial_base_path)
|
|
51
|
+
|
|
52
|
+
static_dir = u.join_paths(os.path.dirname(__file__), c.PACKAGE_DATA_FOLDER, c.STATIC_FOLDER)
|
|
53
|
+
app.mount('/static', StaticFiles(directory=static_dir), name='static')
|
|
54
|
+
|
|
55
|
+
templates_dir = u.join_paths(os.path.dirname(__file__), c.PACKAGE_DATA_FOLDER, c.TEMPLATES_FOLDER)
|
|
56
|
+
templates = Jinja2Templates(directory=templates_dir)
|
|
57
|
+
|
|
58
|
+
# Exception handlers
|
|
59
|
+
@app.exception_handler(u.InvalidInputError)
|
|
60
|
+
async def invalid_input_error_handler(request: Request, exc: u.InvalidInputError):
|
|
61
|
+
traceback.print_exc()
|
|
62
|
+
return JSONResponse(status_code=status.HTTP_400_BAD_REQUEST,
|
|
63
|
+
content={"message": f"Invalid user input: {str(exc)}"})
|
|
64
|
+
|
|
65
|
+
@app.exception_handler(u.ConfigurationError)
|
|
66
|
+
async def configuration_error_handler(request: Request, exc: u.InvalidInputError):
|
|
67
|
+
traceback.print_exc()
|
|
68
|
+
return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
69
|
+
content={"message": f"Squirrels configuration error: {str(exc)}"})
|
|
70
|
+
|
|
71
|
+
@app.exception_handler(NotImplementedError)
|
|
72
|
+
async def not_implemented_error_handler(request: Request, exc: u.InvalidInputError):
|
|
73
|
+
traceback.print_exc()
|
|
74
|
+
return JSONResponse(status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
|
75
|
+
content={"message": f"Not implemented error: {str(exc)}"})
|
|
76
|
+
|
|
77
|
+
# Helpers
|
|
78
|
+
T = TypeVar('T')
|
|
79
|
+
|
|
80
|
+
def get_versioning_request_header(headers: Mapping, header_key: str):
|
|
81
|
+
header_value = headers.get(header_key)
|
|
82
|
+
if header_value is None:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
result = int(header_value)
|
|
87
|
+
except ValueError:
|
|
88
|
+
raise u.InvalidInputError(f"Request header '{header_key}' must be an integer. Got '{header_value}'")
|
|
89
|
+
|
|
90
|
+
if result < 0 or result > int(sq_major_version):
|
|
91
|
+
raise u.InvalidInputError(f"Request header '{header_key}' not in valid range. Got '{result}'")
|
|
92
|
+
|
|
93
|
+
return result
|
|
94
|
+
|
|
95
|
+
REQUEST_VERSION_REQUEST_HEADER = "squirrels-request-version"
|
|
96
|
+
def get_request_version_header(headers: Mapping):
|
|
97
|
+
return get_versioning_request_header(headers, REQUEST_VERSION_REQUEST_HEADER)
|
|
98
|
+
|
|
99
|
+
RESPONSE_VERSION_REQUEST_HEADER = "squirrels-response-version"
|
|
100
|
+
def process_based_on_response_version_header(headers: Mapping, processes: dict[str, Callable[[], T]]) -> T:
|
|
101
|
+
response_version = get_versioning_request_header(headers, RESPONSE_VERSION_REQUEST_HEADER)
|
|
102
|
+
if response_version is None or response_version >= 0:
|
|
103
|
+
return processes[0]()
|
|
104
|
+
else:
|
|
105
|
+
raise u.InvalidInputError(f'Invalid value for "{RESPONSE_VERSION_REQUEST_HEADER}" header: {response_version}')
|
|
106
|
+
|
|
107
|
+
def can_user_access_dataset(user: Optional[User], dataset: str):
|
|
108
|
+
dataset_scope = self.dataset_configs[dataset].scope
|
|
109
|
+
return self.authenticator.can_user_access_scope(user, dataset_scope)
|
|
110
|
+
|
|
111
|
+
async def apply_dataset_api_function(
|
|
112
|
+
api_function: Callable[..., Coroutine[Any, Any, T]], user: Optional[User], dataset: str, headers: Mapping, params: Mapping
|
|
113
|
+
) -> T:
|
|
114
|
+
dataset_normalized = u.normalize_name(dataset)
|
|
115
|
+
if not can_user_access_dataset(user, dataset_normalized):
|
|
116
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
|
|
117
|
+
detail="Could not validate credentials",
|
|
118
|
+
headers={"WWW-Authenticate": "Bearer"})
|
|
119
|
+
|
|
120
|
+
request_version = get_request_version_header(headers)
|
|
121
|
+
|
|
122
|
+
# Changing selections into a cachable "frozenset" that will later be converted to dictionary
|
|
123
|
+
selections = set()
|
|
124
|
+
for key, val in params.items():
|
|
125
|
+
if not isinstance(val, str):
|
|
126
|
+
val = tuple(val)
|
|
127
|
+
selections.add((u.normalize_name(key), val))
|
|
128
|
+
selections = frozenset(selections)
|
|
129
|
+
|
|
130
|
+
return await api_function(user, dataset_normalized, selections, request_version)
|
|
131
|
+
|
|
132
|
+
# Login
|
|
133
|
+
token_path = base_path + '/token'
|
|
134
|
+
|
|
135
|
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl=token_path, auto_error=False)
|
|
136
|
+
|
|
137
|
+
@app.post(token_path)
|
|
138
|
+
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
|
|
139
|
+
user: Optional[User] = self.authenticator.authenticate_user(form_data.username, form_data.password)
|
|
140
|
+
if not user:
|
|
141
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
|
|
142
|
+
detail="Incorrect username or password",
|
|
143
|
+
headers={"WWW-Authenticate": "Bearer"})
|
|
144
|
+
access_token, expiry = self.authenticator.create_access_token(user)
|
|
145
|
+
return {
|
|
146
|
+
"access_token": access_token,
|
|
147
|
+
"token_type": "bearer",
|
|
148
|
+
"username": user.username,
|
|
149
|
+
"expiry_time": expiry
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async def get_current_user(token: str = Depends(oauth2_scheme)) -> Optional[User]:
|
|
153
|
+
user = self.authenticator.get_user_from_token(token)
|
|
154
|
+
return user
|
|
155
|
+
|
|
156
|
+
async def do_cachable_action(cache: TTLCache, action: Callable[..., Coroutine[Any, Any, T]], *args) -> T:
|
|
157
|
+
cache_key = tuple(args)
|
|
158
|
+
result = cache.get(cache_key)
|
|
159
|
+
if result is None:
|
|
160
|
+
result = await action(*args)
|
|
161
|
+
cache[cache_key] = result
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
# Parameters API
|
|
165
|
+
parameters_path = base_path + '/{dataset}/parameters'
|
|
166
|
+
|
|
167
|
+
parameters_cache_size = ManifestIO.obj.settings.get(c.PARAMETERS_CACHE_SIZE_SETTING, 1024)
|
|
168
|
+
parameters_cache_ttl = ManifestIO.obj.settings.get(c.PARAMETERS_CACHE_TTL_SETTING, 0)
|
|
169
|
+
|
|
170
|
+
async def get_parameters_helper(
|
|
171
|
+
user: Optional[User], dataset: str, selections: Iterable[tuple[str, str]], request_version: Optional[int]
|
|
172
|
+
) -> ParameterSet:
|
|
173
|
+
if len(selections) > 1:
|
|
174
|
+
raise u.InvalidInputError(f"The /parameters endpoint takes at most 1 query parameter. Got {dict(selections)}")
|
|
175
|
+
dag = ModelsIO.GenerateDAG(dataset)
|
|
176
|
+
dag.apply_selections(user, dict(selections), request_version=request_version)
|
|
177
|
+
return dag.parameter_set
|
|
178
|
+
|
|
179
|
+
params_cache = TTLCache(maxsize=parameters_cache_size, ttl=parameters_cache_ttl*60)
|
|
180
|
+
|
|
181
|
+
async def get_parameters_cachable(*args) -> T:
|
|
182
|
+
return await do_cachable_action(params_cache, get_parameters_helper, *args)
|
|
183
|
+
|
|
184
|
+
async def get_parameters_definition(dataset: str, user: Optional[User], headers: Mapping, params: Mapping):
|
|
185
|
+
api_function = get_parameters_helper if self.no_cache else get_parameters_cachable
|
|
186
|
+
result = await apply_dataset_api_function(api_function, user, dataset, headers, params)
|
|
187
|
+
return process_based_on_response_version_header(headers, {
|
|
188
|
+
0: result.to_json_dict0
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
@app.get(parameters_path, response_class=JSONResponse)
|
|
192
|
+
async def get_parameters(dataset: str, request: Request, user: Optional[User] = Depends(get_current_user)):
|
|
193
|
+
start = time.time()
|
|
194
|
+
result = await get_parameters_definition(dataset, user, request.headers, request.query_params)
|
|
195
|
+
timer.add_activity_time("GET REQUEST total time for PARAMETERS", start)
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
@app.post(parameters_path, response_class=JSONResponse)
|
|
199
|
+
async def get_parameters_with_post(dataset: str, request: Request, user: Optional[User] = Depends(get_current_user)):
|
|
200
|
+
start = time.time()
|
|
201
|
+
request_body = await request.json()
|
|
202
|
+
result = await get_parameters_definition(dataset, user, request.headers, request_body)
|
|
203
|
+
timer.add_activity_time("POST REQUEST total time for PARAMETERS", start)
|
|
204
|
+
return result
|
|
205
|
+
|
|
206
|
+
# Results API
|
|
207
|
+
results_path = base_path + '/{dataset}'
|
|
208
|
+
|
|
209
|
+
results_cache_size = ManifestIO.obj.settings.get(c.RESULTS_CACHE_SIZE_SETTING, 128)
|
|
210
|
+
results_cache_ttl = ManifestIO.obj.settings.get(c.RESULTS_CACHE_TTL_SETTING, 0)
|
|
211
|
+
|
|
212
|
+
async def get_results_helper(
|
|
213
|
+
user: Optional[User], dataset: str, selections: Iterable[tuple[str, str]], request_version: Optional[int]
|
|
214
|
+
) -> pd.DataFrame:
|
|
215
|
+
dag = ModelsIO.GenerateDAG(dataset)
|
|
216
|
+
await dag.execute(ModelsIO.context_func, user, dict(selections), request_version=request_version)
|
|
217
|
+
return dag.target_model.result
|
|
218
|
+
|
|
219
|
+
results_cache = TTLCache(maxsize=results_cache_size, ttl=results_cache_ttl*60)
|
|
220
|
+
|
|
221
|
+
async def get_results_cachable(*args):
|
|
222
|
+
return await do_cachable_action(results_cache, get_results_helper, *args)
|
|
223
|
+
|
|
224
|
+
async def get_results_definition(dataset: str, user: Optional[User], headers: Mapping, params: Mapping):
|
|
225
|
+
api_function = get_results_helper if self.no_cache else get_results_cachable
|
|
226
|
+
result = await apply_dataset_api_function(api_function, user, dataset, headers, params)
|
|
227
|
+
return process_based_on_response_version_header(headers, {
|
|
228
|
+
0: lambda: u.df_to_json0(result)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
@app.get(results_path, response_class=JSONResponse)
|
|
232
|
+
async def get_results(dataset: str, request: Request, user: Optional[User] = Depends(get_current_user)):
|
|
233
|
+
start = time.time()
|
|
234
|
+
result = await get_results_definition(dataset, user, request.headers, request.query_params)
|
|
235
|
+
timer.add_activity_time("GET REQUEST total time for DATASET", start)
|
|
236
|
+
return result
|
|
237
|
+
|
|
238
|
+
@app.post(results_path, response_class=JSONResponse)
|
|
239
|
+
async def get_results_with_post(dataset: str, request: Request, user: Optional[User] = Depends(get_current_user)):
|
|
240
|
+
start = time.time()
|
|
241
|
+
request_body = await request.json()
|
|
242
|
+
result = await get_results_definition(dataset, user, request.headers, request_body)
|
|
243
|
+
timer.add_activity_time("POST REQUEST total time for DATASET", start)
|
|
244
|
+
return result
|
|
245
|
+
|
|
246
|
+
# Catalog API
|
|
247
|
+
def get_catalog0(user: Optional[User]):
|
|
248
|
+
datasets_info = []
|
|
249
|
+
for dataset_name, dataset_config in self.dataset_configs.items():
|
|
250
|
+
if can_user_access_dataset(user, dataset_name):
|
|
251
|
+
dataset_normalized = u.normalize_name_for_api(dataset_name)
|
|
252
|
+
datasets_info.append({
|
|
253
|
+
'name': dataset_name,
|
|
254
|
+
'label': dataset_config.label,
|
|
255
|
+
'parameters_path': parameters_path.format(dataset=dataset_normalized),
|
|
256
|
+
'result_path': results_path.format(dataset=dataset_normalized),
|
|
257
|
+
'first_minor_version': 0
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
'products': [{
|
|
262
|
+
'name': ManifestIO.obj.project_variables.get_name(),
|
|
263
|
+
'label': ManifestIO.obj.project_variables.get_label(),
|
|
264
|
+
'versions': [{
|
|
265
|
+
'major_version': ManifestIO.obj.project_variables.get_major_version(),
|
|
266
|
+
'latest_minor_version': ManifestIO.obj.project_variables.get_minor_version(),
|
|
267
|
+
'datasets': datasets_info
|
|
268
|
+
}]
|
|
269
|
+
}]
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
@app.get(squirrels_version_path, response_class=JSONResponse)
|
|
273
|
+
async def get_catalog(request: Request, user: Optional[User] = Depends(get_current_user)):
|
|
274
|
+
return process_based_on_response_version_header(request.headers, {
|
|
275
|
+
0: lambda: get_catalog0(user)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
# Squirrels UI
|
|
279
|
+
@app.get('/', response_class=HTMLResponse)
|
|
280
|
+
async def get_ui(request: Request):
|
|
281
|
+
return templates.TemplateResponse('index.html', {
|
|
282
|
+
'request': request, 'catalog_path': squirrels_version_path, 'token_path': token_path
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
# Run API server
|
|
286
|
+
import uvicorn
|
|
287
|
+
timer.add_activity_time("creating app for api server", start)
|
|
288
|
+
uvicorn.run(app, host=uvicorn_args.host, port=uvicorn_args.port)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from datetime import datetime, timedelta, timezone
|
|
3
|
+
from jose import JWTError, jwt
|
|
4
|
+
import secrets
|
|
5
|
+
|
|
6
|
+
from . import _utils as u, _constants as c
|
|
7
|
+
from ._py_module import PyModule
|
|
8
|
+
from .user_base import User, WrongPassword
|
|
9
|
+
from ._environcfg import EnvironConfigIO
|
|
10
|
+
from ._manifest import DatasetScope
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Authenticator:
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def get_auth_helper(cls, default_auth_helper = None):
|
|
17
|
+
auth_module_path = u.join_paths(c.PYCONFIG_FOLDER, c.AUTH_FILE)
|
|
18
|
+
return PyModule(auth_module_path, default_class=default_auth_helper)
|
|
19
|
+
|
|
20
|
+
def __init__(self, token_expiry_minutes: int, auth_helper = None) -> None:
|
|
21
|
+
self.token_expiry_minutes = token_expiry_minutes
|
|
22
|
+
self.auth_helper = self.get_auth_helper(auth_helper)
|
|
23
|
+
self.secret_key = self._get_secret_key()
|
|
24
|
+
self.algorithm = "HS256"
|
|
25
|
+
|
|
26
|
+
def _get_secret_key(self):
|
|
27
|
+
secret_key = EnvironConfigIO.obj.get_secret(c.JWT_SECRET_KEY, default_factory=lambda: secrets.token_hex(32))
|
|
28
|
+
return secret_key
|
|
29
|
+
|
|
30
|
+
def authenticate_user(self, username: str, password: str) -> Optional[User]:
|
|
31
|
+
if self.auth_helper:
|
|
32
|
+
user_cls = self.auth_helper.get_func_or_class("User", default_attr=User)
|
|
33
|
+
get_user = self.auth_helper.get_func_or_class(c.GET_USER_FUNC)
|
|
34
|
+
try:
|
|
35
|
+
real_user = get_user(username, password)
|
|
36
|
+
except Exception as e:
|
|
37
|
+
raise u.FileExecutionError(f'Failed to run "{c.GET_USER_FUNC}" in {c.AUTH_FILE}', e)
|
|
38
|
+
else:
|
|
39
|
+
user_cls = User
|
|
40
|
+
real_user = None
|
|
41
|
+
|
|
42
|
+
if isinstance(real_user, User):
|
|
43
|
+
return real_user
|
|
44
|
+
|
|
45
|
+
if not isinstance(real_user, WrongPassword):
|
|
46
|
+
fake_users = EnvironConfigIO.obj.get_users()
|
|
47
|
+
if username in fake_users and secrets.compare_digest(fake_users[username][c.USER_PWD_KEY], password):
|
|
48
|
+
is_internal = fake_users[username].get("is_internal", False)
|
|
49
|
+
user = user_cls(username, is_internal=is_internal)
|
|
50
|
+
try:
|
|
51
|
+
return user.with_attributes(fake_users[username])
|
|
52
|
+
except Exception as e:
|
|
53
|
+
raise u.FileExecutionError(f'Failed to create user from User model in {c.AUTH_FILE}', e)
|
|
54
|
+
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def create_access_token(self, user: User) -> str:
|
|
58
|
+
expire = datetime.now(timezone.utc) + timedelta(minutes=self.token_expiry_minutes)
|
|
59
|
+
to_encode = {**vars(user), "exp": expire}
|
|
60
|
+
encoded_jwt = jwt.encode(to_encode, self.secret_key, algorithm=self.algorithm)
|
|
61
|
+
return encoded_jwt, expire
|
|
62
|
+
|
|
63
|
+
def get_user_from_token(self, token: Optional[str]) -> Optional[User]:
|
|
64
|
+
if token is not None:
|
|
65
|
+
try:
|
|
66
|
+
payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
|
|
67
|
+
payload.pop("exp")
|
|
68
|
+
if self.auth_helper is not None:
|
|
69
|
+
user_cls: User = self.auth_helper.get_func_or_class("User", default_attr=User)
|
|
70
|
+
return user_cls._FromDict(payload)
|
|
71
|
+
else:
|
|
72
|
+
return User._FromDict(payload)
|
|
73
|
+
except JWTError:
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def can_user_access_scope(self, user: Optional[User], scope: DatasetScope) -> bool:
|
|
77
|
+
if user is None:
|
|
78
|
+
user_level = DatasetScope.PUBLIC
|
|
79
|
+
elif not user.is_internal:
|
|
80
|
+
user_level = DatasetScope.PROTECTED
|
|
81
|
+
else:
|
|
82
|
+
user_level = DatasetScope.PRIVATE
|
|
83
|
+
|
|
84
|
+
return user_level.value >= scope.value
|