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.

Files changed (93) hide show
  1. squirrels-0.2.0.dev0/PKG-INFO +126 -0
  2. squirrels-0.2.0.dev0/README.md +90 -0
  3. squirrels-0.2.0.dev0/pyproject.toml +45 -0
  4. squirrels-0.2.0.dev0/squirrels/__init__.py +12 -0
  5. squirrels-0.2.0.dev0/squirrels/_api_server.py +288 -0
  6. squirrels-0.2.0.dev0/squirrels/_authenticator.py +84 -0
  7. squirrels-0.2.0.dev0/squirrels/_command_line.py +95 -0
  8. squirrels-0.2.0.dev0/squirrels/_connection_set.py +96 -0
  9. squirrels-0.2.0.dev0/squirrels/_constants.py +145 -0
  10. squirrels-0.2.0.dev0/squirrels/_environcfg.py +77 -0
  11. squirrels-0.2.0.dev0/squirrels/_initializer.py +169 -0
  12. squirrels-0.2.0.dev0/squirrels/_manifest.py +214 -0
  13. squirrels-0.2.0.dev0/squirrels/_models.py +495 -0
  14. squirrels-0.2.0.dev0/squirrels/_package_loader.py +26 -0
  15. squirrels-0.2.0.dev0/squirrels/_parameter_configs.py +401 -0
  16. squirrels-0.2.0.dev0/squirrels/_parameter_sets.py +188 -0
  17. squirrels-0.2.0.dev0/squirrels/_py_module.py +60 -0
  18. squirrels-0.2.0.dev0/squirrels/_timer.py +36 -0
  19. squirrels-0.2.0.dev0/squirrels/_utils.py +181 -0
  20. squirrels-0.2.0.dev0/squirrels/_version.py +3 -0
  21. squirrels-0.2.0.dev0/squirrels/arguments/init_time_args.py +32 -0
  22. squirrels-0.2.0.dev0/squirrels/arguments/run_time_args.py +82 -0
  23. squirrels-0.2.0.dev0/squirrels/data_sources.py +515 -0
  24. {squirrels-0.1.1.post1 → squirrels-0.2.0.dev0}/squirrels/dateutils.py +86 -57
  25. squirrels-0.2.0.dev0/squirrels/package_data/base_project/Dockerfile +15 -0
  26. squirrels-0.2.0.dev0/squirrels/package_data/base_project/connections.yml +7 -0
  27. 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
  28. squirrels-0.2.0.dev0/squirrels/package_data/base_project/environcfg.yml +29 -0
  29. squirrels-0.2.0.dev0/squirrels/package_data/base_project/ignores/.dockerignore +8 -0
  30. squirrels-0.2.0.dev0/squirrels/package_data/base_project/ignores/.gitignore +7 -0
  31. squirrels-0.2.0.dev0/squirrels/package_data/base_project/models/dbviews/database_view1.py +36 -0
  32. squirrels-0.2.0.dev0/squirrels/package_data/base_project/models/dbviews/database_view1.sql +15 -0
  33. squirrels-0.2.0.dev0/squirrels/package_data/base_project/models/federates/dataset_example.py +20 -0
  34. squirrels-0.2.0.dev0/squirrels/package_data/base_project/models/federates/dataset_example.sql +3 -0
  35. squirrels-0.2.0.dev0/squirrels/package_data/base_project/parameters.yml +109 -0
  36. squirrels-0.2.0.dev0/squirrels/package_data/base_project/pyconfigs/auth.py +47 -0
  37. squirrels-0.2.0.dev0/squirrels/package_data/base_project/pyconfigs/connections.py +28 -0
  38. squirrels-0.2.0.dev0/squirrels/package_data/base_project/pyconfigs/context.py +45 -0
  39. squirrels-0.2.0.dev0/squirrels/package_data/base_project/pyconfigs/parameters.py +55 -0
  40. squirrels-0.2.0.dev0/squirrels/package_data/base_project/seeds/mocks/category.csv +3 -0
  41. squirrels-0.2.0.dev0/squirrels/package_data/base_project/seeds/mocks/max_filter.csv +2 -0
  42. squirrels-0.2.0.dev0/squirrels/package_data/base_project/seeds/mocks/subcategory.csv +6 -0
  43. squirrels-0.2.0.dev0/squirrels/package_data/base_project/squirrels.yml.j2 +57 -0
  44. squirrels-0.2.0.dev0/squirrels/package_data/base_project/tmp/.gitignore +2 -0
  45. {squirrels-0.1.1.post1 → squirrels-0.2.0.dev0}/squirrels/package_data/static/script.js +159 -63
  46. {squirrels-0.1.1.post1 → squirrels-0.2.0.dev0}/squirrels/package_data/static/style.css +79 -15
  47. squirrels-0.2.0.dev0/squirrels/package_data/static/widgets.js +133 -0
  48. squirrels-0.2.0.dev0/squirrels/package_data/templates/index.html +74 -0
  49. squirrels-0.2.0.dev0/squirrels/package_data/templates/index2.html +22 -0
  50. squirrels-0.2.0.dev0/squirrels/parameter_options.py +330 -0
  51. squirrels-0.2.0.dev0/squirrels/parameters.py +755 -0
  52. squirrels-0.2.0.dev0/squirrels/user_base.py +58 -0
  53. squirrels-0.1.1.post1/PKG-INFO +0 -56
  54. squirrels-0.1.1.post1/README.md +0 -46
  55. squirrels-0.1.1.post1/setup.cfg +0 -4
  56. squirrels-0.1.1.post1/setup.py +0 -42
  57. squirrels-0.1.1.post1/squirrels/__init__.py +0 -18
  58. squirrels-0.1.1.post1/squirrels/_api_server.py +0 -134
  59. squirrels-0.1.1.post1/squirrels/_command_line.py +0 -107
  60. squirrels-0.1.1.post1/squirrels/_constants.py +0 -64
  61. squirrels-0.1.1.post1/squirrels/_credentials_manager.py +0 -87
  62. squirrels-0.1.1.post1/squirrels/_initializer.py +0 -110
  63. squirrels-0.1.1.post1/squirrels/_manifest.py +0 -187
  64. squirrels-0.1.1.post1/squirrels/_module_loader.py +0 -37
  65. squirrels-0.1.1.post1/squirrels/_parameter_set.py +0 -151
  66. squirrels-0.1.1.post1/squirrels/_renderer.py +0 -286
  67. squirrels-0.1.1.post1/squirrels/_timed_imports.py +0 -37
  68. squirrels-0.1.1.post1/squirrels/_utils.py +0 -149
  69. squirrels-0.1.1.post1/squirrels/_version.py +0 -3
  70. squirrels-0.1.1.post1/squirrels/connection_set.py +0 -126
  71. squirrels-0.1.1.post1/squirrels/data_sources.py +0 -290
  72. squirrels-0.1.1.post1/squirrels/package_data/base_project/.gitignore +0 -4
  73. squirrels-0.1.1.post1/squirrels/package_data/base_project/connections.py +0 -20
  74. squirrels-0.1.1.post1/squirrels/package_data/base_project/datasets/sample_dataset/context.py +0 -22
  75. squirrels-0.1.1.post1/squirrels/package_data/base_project/datasets/sample_dataset/database_view1.py +0 -29
  76. squirrels-0.1.1.post1/squirrels/package_data/base_project/datasets/sample_dataset/database_view1.sql.j2 +0 -12
  77. squirrels-0.1.1.post1/squirrels/package_data/base_project/datasets/sample_dataset/final_view.py +0 -11
  78. squirrels-0.1.1.post1/squirrels/package_data/base_project/datasets/sample_dataset/final_view.sql.j2 +0 -3
  79. squirrels-0.1.1.post1/squirrels/package_data/base_project/datasets/sample_dataset/parameters.py +0 -47
  80. squirrels-0.1.1.post1/squirrels/package_data/base_project/datasets/sample_dataset/selections.cfg +0 -9
  81. squirrels-0.1.1.post1/squirrels/package_data/base_project/squirrels.yaml +0 -22
  82. squirrels-0.1.1.post1/squirrels/package_data/templates/index.html +0 -32
  83. squirrels-0.1.1.post1/squirrels/parameter_options.py +0 -233
  84. squirrels-0.1.1.post1/squirrels/parameters.py +0 -826
  85. squirrels-0.1.1.post1/squirrels.egg-info/PKG-INFO +0 -56
  86. squirrels-0.1.1.post1/squirrels.egg-info/SOURCES.txt +0 -43
  87. squirrels-0.1.1.post1/squirrels.egg-info/dependency_links.txt +0 -1
  88. squirrels-0.1.1.post1/squirrels.egg-info/entry_points.txt +0 -2
  89. squirrels-0.1.1.post1/squirrels.egg-info/requires.txt +0 -11
  90. squirrels-0.1.1.post1/squirrels.egg-info/top_level.txt +0 -1
  91. {squirrels-0.1.1.post1 → squirrels-0.2.0.dev0}/LICENSE +0 -0
  92. {squirrels-0.1.1.post1 → squirrels-0.2.0.dev0}/squirrels/package_data/base_project/database/seattle_weather.db +0 -0
  93. {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