squirrels 0.1.1.post1__py3-none-any.whl → 0.2.0.dev0__py3-none-any.whl
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/__init__.py +10 -16
- squirrels/_api_server.py +234 -80
- squirrels/_authenticator.py +84 -0
- squirrels/_command_line.py +60 -72
- squirrels/_connection_set.py +96 -0
- squirrels/_constants.py +114 -33
- squirrels/_environcfg.py +77 -0
- squirrels/_initializer.py +126 -67
- squirrels/_manifest.py +195 -168
- squirrels/_models.py +495 -0
- squirrels/_package_loader.py +26 -0
- squirrels/_parameter_configs.py +401 -0
- squirrels/_parameter_sets.py +188 -0
- squirrels/_py_module.py +60 -0
- squirrels/_timer.py +36 -0
- squirrels/_utils.py +81 -49
- squirrels/_version.py +2 -2
- squirrels/arguments/init_time_args.py +32 -0
- squirrels/arguments/run_time_args.py +82 -0
- squirrels/data_sources.py +380 -155
- squirrels/dateutils.py +86 -57
- squirrels/package_data/base_project/Dockerfile +15 -0
- squirrels/package_data/base_project/connections.yml +7 -0
- squirrels/package_data/base_project/database/{sample_database.db → expenses.db} +0 -0
- squirrels/package_data/base_project/environcfg.yml +29 -0
- squirrels/package_data/base_project/ignores/.dockerignore +8 -0
- squirrels/package_data/base_project/ignores/.gitignore +7 -0
- squirrels/package_data/base_project/models/dbviews/database_view1.py +36 -0
- squirrels/package_data/base_project/models/dbviews/database_view1.sql +15 -0
- squirrels/package_data/base_project/models/federates/dataset_example.py +20 -0
- squirrels/package_data/base_project/models/federates/dataset_example.sql +3 -0
- squirrels/package_data/base_project/parameters.yml +109 -0
- squirrels/package_data/base_project/pyconfigs/auth.py +47 -0
- squirrels/package_data/base_project/pyconfigs/connections.py +28 -0
- squirrels/package_data/base_project/pyconfigs/context.py +45 -0
- squirrels/package_data/base_project/pyconfigs/parameters.py +55 -0
- squirrels/package_data/base_project/seeds/mocks/category.csv +3 -0
- squirrels/package_data/base_project/seeds/mocks/max_filter.csv +2 -0
- squirrels/package_data/base_project/seeds/mocks/subcategory.csv +6 -0
- squirrels/package_data/base_project/squirrels.yml.j2 +57 -0
- squirrels/package_data/base_project/tmp/.gitignore +2 -0
- squirrels/package_data/static/script.js +159 -63
- squirrels/package_data/static/style.css +79 -15
- squirrels/package_data/static/widgets.js +133 -0
- squirrels/package_data/templates/index.html +65 -23
- squirrels/package_data/templates/index2.html +22 -0
- squirrels/parameter_options.py +216 -119
- squirrels/parameters.py +407 -478
- squirrels/user_base.py +58 -0
- squirrels-0.2.0.dev0.dist-info/METADATA +126 -0
- squirrels-0.2.0.dev0.dist-info/RECORD +56 -0
- {squirrels-0.1.1.post1.dist-info → squirrels-0.2.0.dev0.dist-info}/WHEEL +1 -2
- squirrels-0.2.0.dev0.dist-info/entry_points.txt +3 -0
- squirrels/_credentials_manager.py +0 -87
- squirrels/_module_loader.py +0 -37
- squirrels/_parameter_set.py +0 -151
- squirrels/_renderer.py +0 -286
- squirrels/_timed_imports.py +0 -37
- squirrels/connection_set.py +0 -126
- squirrels/package_data/base_project/.gitignore +0 -4
- squirrels/package_data/base_project/connections.py +0 -20
- squirrels/package_data/base_project/datasets/sample_dataset/context.py +0 -22
- squirrels/package_data/base_project/datasets/sample_dataset/database_view1.py +0 -29
- squirrels/package_data/base_project/datasets/sample_dataset/database_view1.sql.j2 +0 -12
- squirrels/package_data/base_project/datasets/sample_dataset/final_view.py +0 -11
- squirrels/package_data/base_project/datasets/sample_dataset/final_view.sql.j2 +0 -3
- squirrels/package_data/base_project/datasets/sample_dataset/parameters.py +0 -47
- squirrels/package_data/base_project/datasets/sample_dataset/selections.cfg +0 -9
- squirrels/package_data/base_project/squirrels.yaml +0 -22
- squirrels-0.1.1.post1.dist-info/METADATA +0 -67
- squirrels-0.1.1.post1.dist-info/RECORD +0 -40
- squirrels-0.1.1.post1.dist-info/entry_points.txt +0 -2
- squirrels-0.1.1.post1.dist-info/top_level.txt +0 -1
- {squirrels-0.1.1.post1.dist-info → squirrels-0.2.0.dev0.dist-info}/LICENSE +0 -0
squirrels/_utils.py
CHANGED
|
@@ -1,56 +1,87 @@
|
|
|
1
|
-
from typing import
|
|
2
|
-
from types import ModuleType
|
|
1
|
+
from typing import Sequence, Optional, Union, Any, TypeVar, Callable
|
|
3
2
|
from pathlib import Path
|
|
4
|
-
from
|
|
5
|
-
import json
|
|
6
|
-
|
|
7
|
-
from squirrels._timed_imports import jinja2 as j2, pandas as pd, pd_types
|
|
3
|
+
from pandas.api import types as pd_types
|
|
4
|
+
import json, jinja2 as j2, pandas as pd
|
|
8
5
|
|
|
9
6
|
FilePath = Union[str, Path]
|
|
10
7
|
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
## Custom Exceptions
|
|
10
|
+
|
|
13
11
|
class InvalidInputError(Exception):
|
|
12
|
+
"""
|
|
13
|
+
Use this exception when the error is due to providing invalid inputs to the REST API
|
|
14
|
+
"""
|
|
14
15
|
pass
|
|
15
16
|
|
|
16
17
|
class ConfigurationError(Exception):
|
|
18
|
+
"""
|
|
19
|
+
Use this exception when the server error is due to errors in the squirrels project instead of the squirrels framework/library
|
|
20
|
+
"""
|
|
17
21
|
pass
|
|
18
22
|
|
|
19
|
-
class
|
|
20
|
-
def __init__(self,
|
|
21
|
-
|
|
22
|
-
super().__init__(
|
|
23
|
+
class FileExecutionError(ConfigurationError):
|
|
24
|
+
def __init__(self, message: str, error: Exception, *args) -> None:
|
|
25
|
+
new_message = message + f"\n... Produced error message `{error}` (see above for more details)"
|
|
26
|
+
super().__init__(new_message, *args)
|
|
23
27
|
|
|
24
28
|
|
|
25
|
-
|
|
26
|
-
j2_env = j2.Environment(loader=j2.FileSystemLoader('.'))
|
|
29
|
+
## Utility functions/variables
|
|
27
30
|
|
|
31
|
+
def join_paths(*paths: FilePath) -> Path:
|
|
32
|
+
"""
|
|
33
|
+
Joins paths together.
|
|
34
|
+
|
|
35
|
+
Parameters:
|
|
36
|
+
paths (str | pathlib.Path): The paths to join.
|
|
28
37
|
|
|
29
|
-
|
|
38
|
+
Returns:
|
|
39
|
+
(pathlib.Path) The joined path.
|
|
40
|
+
"""
|
|
41
|
+
return Path(*paths)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_j2_env = j2.Environment(loader=j2.FileSystemLoader('.'))
|
|
45
|
+
|
|
46
|
+
def render_string(raw_str: str, kwargs: dict) -> str:
|
|
30
47
|
"""
|
|
31
|
-
|
|
48
|
+
Given a template string, render it with the given keyword arguments
|
|
32
49
|
|
|
33
50
|
Parameters:
|
|
34
|
-
|
|
51
|
+
raw_str: The template string
|
|
52
|
+
kwargs: The keyword arguments
|
|
35
53
|
|
|
36
54
|
Returns:
|
|
37
|
-
The
|
|
55
|
+
The rendered string
|
|
38
56
|
"""
|
|
39
|
-
|
|
40
|
-
return
|
|
57
|
+
template = _j2_env.from_string(raw_str)
|
|
58
|
+
return template.render(kwargs)
|
|
41
59
|
|
|
42
60
|
|
|
43
|
-
|
|
61
|
+
T = TypeVar('T')
|
|
62
|
+
def __process_file_handler(file_handler: Callable[[FilePath], T], filepath: FilePath, is_required: bool) -> Optional[T]:
|
|
63
|
+
try:
|
|
64
|
+
return file_handler(filepath)
|
|
65
|
+
except FileNotFoundError as e:
|
|
66
|
+
if is_required:
|
|
67
|
+
raise ConfigurationError(f"Required file not found: '{str(filepath)}'") from e
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def read_file(filepath: FilePath, *, is_required: bool = True) -> Optional[str]:
|
|
44
71
|
"""
|
|
45
|
-
|
|
72
|
+
Reads a file and return its content if required
|
|
46
73
|
|
|
47
74
|
Parameters:
|
|
48
|
-
|
|
75
|
+
filepath (str | pathlib.Path): The path to the file to read
|
|
76
|
+
is_required: If true, throw error if file doesn't exist
|
|
49
77
|
|
|
50
78
|
Returns:
|
|
51
|
-
|
|
79
|
+
Content of the file, or None if doesn't exist and not required
|
|
52
80
|
"""
|
|
53
|
-
|
|
81
|
+
def file_handler(filepath: FilePath):
|
|
82
|
+
with open(filepath, 'r') as f:
|
|
83
|
+
return f.read()
|
|
84
|
+
return __process_file_handler(file_handler, filepath, is_required)
|
|
54
85
|
|
|
55
86
|
|
|
56
87
|
def normalize_name(name: str) -> str:
|
|
@@ -79,28 +110,7 @@ def normalize_name_for_api(name: str) -> str:
|
|
|
79
110
|
return name.replace('_', '-')
|
|
80
111
|
|
|
81
112
|
|
|
82
|
-
def
|
|
83
|
-
"""
|
|
84
|
-
Gets the value of a row from a pandas Series.
|
|
85
|
-
|
|
86
|
-
Parameters:
|
|
87
|
-
row: The row to get the value from.
|
|
88
|
-
value: The name of the column to get the value from.
|
|
89
|
-
|
|
90
|
-
Returns:
|
|
91
|
-
The value of the column.
|
|
92
|
-
|
|
93
|
-
Raises:
|
|
94
|
-
ConfigurationError: If the column does not exist.
|
|
95
|
-
"""
|
|
96
|
-
try:
|
|
97
|
-
result = row[value]
|
|
98
|
-
except KeyError as e:
|
|
99
|
-
raise ConfigurationError(f'Column name "{value}" does not exist') from e
|
|
100
|
-
return result
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def df_to_json(df: pd.DataFrame, dimensions: List[str] = None) -> Dict[str, Any]:
|
|
113
|
+
def df_to_json0(df: pd.DataFrame, dimensions: list[str] = None) -> dict[str, Any]:
|
|
104
114
|
"""
|
|
105
115
|
Convert a pandas DataFrame to the same JSON format that the dataset result API of Squirrels outputs.
|
|
106
116
|
|
|
@@ -124,10 +134,10 @@ def df_to_json(df: pd.DataFrame, dimensions: List[str] = None) -> Dict[str, Any]
|
|
|
124
134
|
|
|
125
135
|
out_dimensions = non_numeric_fields if dimensions is None else dimensions
|
|
126
136
|
out_schema = {"fields": out_fields, "dimensions": out_dimensions}
|
|
127
|
-
return {"
|
|
137
|
+
return {"schema": out_schema, "data": in_df_json["data"]}
|
|
128
138
|
|
|
129
139
|
|
|
130
|
-
def load_json_or_comma_delimited_str_as_list(input_str: str) ->
|
|
140
|
+
def load_json_or_comma_delimited_str_as_list(input_str: Union[str, Sequence]) -> Sequence[str]:
|
|
131
141
|
"""
|
|
132
142
|
Given a string, load it as a list either by json string or comma delimited value
|
|
133
143
|
|
|
@@ -137,6 +147,9 @@ def load_json_or_comma_delimited_str_as_list(input_str: str) -> List[str]:
|
|
|
137
147
|
Returns:
|
|
138
148
|
The list representation of the input string
|
|
139
149
|
"""
|
|
150
|
+
if not isinstance(input_str, str):
|
|
151
|
+
return (input_str)
|
|
152
|
+
|
|
140
153
|
output = None
|
|
141
154
|
try:
|
|
142
155
|
output = json.loads(input_str)
|
|
@@ -145,5 +158,24 @@ def load_json_or_comma_delimited_str_as_list(input_str: str) -> List[str]:
|
|
|
145
158
|
|
|
146
159
|
if isinstance(output, list):
|
|
147
160
|
return output
|
|
161
|
+
elif input_str == "":
|
|
162
|
+
return []
|
|
148
163
|
else:
|
|
149
|
-
return [
|
|
164
|
+
return [x.strip() for x in input_str.split(",")]
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
X, Y = TypeVar('X'), TypeVar('Y')
|
|
168
|
+
def process_if_not_none(input_val: Optional[X], processor: Callable[[X], Y]) -> Optional[Y]:
|
|
169
|
+
"""
|
|
170
|
+
Given a input value and a function that processes the value, return the output of the function unless input is None
|
|
171
|
+
|
|
172
|
+
Parameters:
|
|
173
|
+
input_val: The input value
|
|
174
|
+
processor: The function that processes the input value
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
The output type of "processor" or None if input value if None
|
|
178
|
+
"""
|
|
179
|
+
if input_val is None:
|
|
180
|
+
return None
|
|
181
|
+
return processor(input_val)
|
squirrels/_version.py
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
from . import __version__
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
sq_major_version, sq_minor_version, sq_patch_version = __version__.split('.')[:3]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from typing import Callable, Any
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class BaseArguments:
|
|
7
|
+
proj_vars: dict[str, Any]
|
|
8
|
+
env_vars: dict[str, Any]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class ConnectionsArgs(BaseArguments):
|
|
13
|
+
_get_credential: Callable[[str], tuple[str, str]]
|
|
14
|
+
|
|
15
|
+
def __post_init__(self):
|
|
16
|
+
self.get_credential = self._get_credential
|
|
17
|
+
|
|
18
|
+
def get_credential(self, key: str) -> tuple[str, str]:
|
|
19
|
+
"""
|
|
20
|
+
Return (username, password) tuple configured for credentials key in environcfg.yaml
|
|
21
|
+
|
|
22
|
+
Parameters:
|
|
23
|
+
key: The credentials key
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
A tuple of strings of size 2
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ParametersArgs(BaseArguments):
|
|
32
|
+
pass
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from typing import Union, Callable, Any
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from sqlalchemy import Engine, Pool
|
|
4
|
+
import pandas as pd, sqlite3
|
|
5
|
+
|
|
6
|
+
from .init_time_args import ParametersArgs
|
|
7
|
+
from ..user_base import User
|
|
8
|
+
from ..parameters import Parameter
|
|
9
|
+
from .._connection_set import ConnectionSetIO
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ContextArgs(ParametersArgs):
|
|
14
|
+
user: User
|
|
15
|
+
prms: dict[str, Parameter]
|
|
16
|
+
args: dict[str, Any]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ModelDepsArgs(ContextArgs):
|
|
21
|
+
ctx: dict[str, Any]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ModelArgs(ModelDepsArgs):
|
|
26
|
+
connection_name: str
|
|
27
|
+
connections: dict[str, Union[Engine, Pool]]
|
|
28
|
+
_ref: Callable[[str], pd.DataFrame]
|
|
29
|
+
dependencies: set[str]
|
|
30
|
+
|
|
31
|
+
def __post_init__(self):
|
|
32
|
+
self.ref = self._ref
|
|
33
|
+
|
|
34
|
+
def ref(self, model: str) -> pd.DataFrame:
|
|
35
|
+
"""
|
|
36
|
+
Returns the result (as pandas DataFrame) of a dependent model (predefined in "dependencies" function)
|
|
37
|
+
|
|
38
|
+
Note: This is different behaviour than the "ref" function for SQL models, which figures out the dependent models for you,
|
|
39
|
+
and returns a string for the table/view name in SQLite instead of a pandas DataFrame.
|
|
40
|
+
|
|
41
|
+
Parameters:
|
|
42
|
+
model: The model name
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
A pandas DataFrame
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def run_external_sql(self, sql: str, *, connection_name: str = None, **kwargs) -> pd.DataFrame:
|
|
49
|
+
"""
|
|
50
|
+
Runs a SQL query against an external database, with option to specify the connection name
|
|
51
|
+
|
|
52
|
+
Parameters:
|
|
53
|
+
sql: The SQL query
|
|
54
|
+
connection_name: The connection name for the database. If None, uses the one configured for the model
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
The query result as a pandas DataFrame
|
|
58
|
+
"""
|
|
59
|
+
connection_name = self.connection_name if connection_name is None else connection_name
|
|
60
|
+
return ConnectionSetIO.obj.run_sql_query_from_conn_name(sql, connection_name)
|
|
61
|
+
|
|
62
|
+
def run_sql_on_dataframes(self, query: str, *, dataframes: dict[str, pd.DataFrame] = None, **kwargs) -> pd.DataFrame:
|
|
63
|
+
"""
|
|
64
|
+
Uses a dictionary of dataframes to execute a SQL query in an in-memory sqlite database
|
|
65
|
+
|
|
66
|
+
Parameters:
|
|
67
|
+
query: The SQL query to run using sqlite
|
|
68
|
+
dataframes: A dictionary of table names to their pandas Dataframe
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
The result as a pandas Dataframe from running the query
|
|
72
|
+
"""
|
|
73
|
+
if dataframes is None:
|
|
74
|
+
dataframes = {x: self.ref(x) for x in self.dependencies}
|
|
75
|
+
|
|
76
|
+
conn = sqlite3.connect(":memory:")
|
|
77
|
+
try:
|
|
78
|
+
for name, df in dataframes.items():
|
|
79
|
+
df.to_sql(name, conn, index=False)
|
|
80
|
+
return pd.read_sql(query, conn)
|
|
81
|
+
finally:
|
|
82
|
+
conn.close()
|