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.

Files changed (74) hide show
  1. squirrels/__init__.py +10 -16
  2. squirrels/_api_server.py +234 -80
  3. squirrels/_authenticator.py +84 -0
  4. squirrels/_command_line.py +60 -72
  5. squirrels/_connection_set.py +96 -0
  6. squirrels/_constants.py +114 -33
  7. squirrels/_environcfg.py +77 -0
  8. squirrels/_initializer.py +126 -67
  9. squirrels/_manifest.py +195 -168
  10. squirrels/_models.py +495 -0
  11. squirrels/_package_loader.py +26 -0
  12. squirrels/_parameter_configs.py +401 -0
  13. squirrels/_parameter_sets.py +188 -0
  14. squirrels/_py_module.py +60 -0
  15. squirrels/_timer.py +36 -0
  16. squirrels/_utils.py +81 -49
  17. squirrels/_version.py +2 -2
  18. squirrels/arguments/init_time_args.py +32 -0
  19. squirrels/arguments/run_time_args.py +82 -0
  20. squirrels/data_sources.py +380 -155
  21. squirrels/dateutils.py +86 -57
  22. squirrels/package_data/base_project/Dockerfile +15 -0
  23. squirrels/package_data/base_project/connections.yml +7 -0
  24. squirrels/package_data/base_project/database/{sample_database.db → expenses.db} +0 -0
  25. squirrels/package_data/base_project/environcfg.yml +29 -0
  26. squirrels/package_data/base_project/ignores/.dockerignore +8 -0
  27. squirrels/package_data/base_project/ignores/.gitignore +7 -0
  28. squirrels/package_data/base_project/models/dbviews/database_view1.py +36 -0
  29. squirrels/package_data/base_project/models/dbviews/database_view1.sql +15 -0
  30. squirrels/package_data/base_project/models/federates/dataset_example.py +20 -0
  31. squirrels/package_data/base_project/models/federates/dataset_example.sql +3 -0
  32. squirrels/package_data/base_project/parameters.yml +109 -0
  33. squirrels/package_data/base_project/pyconfigs/auth.py +47 -0
  34. squirrels/package_data/base_project/pyconfigs/connections.py +28 -0
  35. squirrels/package_data/base_project/pyconfigs/context.py +45 -0
  36. squirrels/package_data/base_project/pyconfigs/parameters.py +55 -0
  37. squirrels/package_data/base_project/seeds/mocks/category.csv +3 -0
  38. squirrels/package_data/base_project/seeds/mocks/max_filter.csv +2 -0
  39. squirrels/package_data/base_project/seeds/mocks/subcategory.csv +6 -0
  40. squirrels/package_data/base_project/squirrels.yml.j2 +57 -0
  41. squirrels/package_data/base_project/tmp/.gitignore +2 -0
  42. squirrels/package_data/static/script.js +159 -63
  43. squirrels/package_data/static/style.css +79 -15
  44. squirrels/package_data/static/widgets.js +133 -0
  45. squirrels/package_data/templates/index.html +65 -23
  46. squirrels/package_data/templates/index2.html +22 -0
  47. squirrels/parameter_options.py +216 -119
  48. squirrels/parameters.py +407 -478
  49. squirrels/user_base.py +58 -0
  50. squirrels-0.2.0.dev0.dist-info/METADATA +126 -0
  51. squirrels-0.2.0.dev0.dist-info/RECORD +56 -0
  52. {squirrels-0.1.1.post1.dist-info → squirrels-0.2.0.dev0.dist-info}/WHEEL +1 -2
  53. squirrels-0.2.0.dev0.dist-info/entry_points.txt +3 -0
  54. squirrels/_credentials_manager.py +0 -87
  55. squirrels/_module_loader.py +0 -37
  56. squirrels/_parameter_set.py +0 -151
  57. squirrels/_renderer.py +0 -286
  58. squirrels/_timed_imports.py +0 -37
  59. squirrels/connection_set.py +0 -126
  60. squirrels/package_data/base_project/.gitignore +0 -4
  61. squirrels/package_data/base_project/connections.py +0 -20
  62. squirrels/package_data/base_project/datasets/sample_dataset/context.py +0 -22
  63. squirrels/package_data/base_project/datasets/sample_dataset/database_view1.py +0 -29
  64. squirrels/package_data/base_project/datasets/sample_dataset/database_view1.sql.j2 +0 -12
  65. squirrels/package_data/base_project/datasets/sample_dataset/final_view.py +0 -11
  66. squirrels/package_data/base_project/datasets/sample_dataset/final_view.sql.j2 +0 -3
  67. squirrels/package_data/base_project/datasets/sample_dataset/parameters.py +0 -47
  68. squirrels/package_data/base_project/datasets/sample_dataset/selections.cfg +0 -9
  69. squirrels/package_data/base_project/squirrels.yaml +0 -22
  70. squirrels-0.1.1.post1.dist-info/METADATA +0 -67
  71. squirrels-0.1.1.post1.dist-info/RECORD +0 -40
  72. squirrels-0.1.1.post1.dist-info/entry_points.txt +0 -2
  73. squirrels-0.1.1.post1.dist-info/top_level.txt +0 -1
  74. {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 List, Dict, Optional, Union, Any
2
- from types import ModuleType
1
+ from typing import Sequence, Optional, Union, Any, TypeVar, Callable
3
2
  from pathlib import Path
4
- from importlib.machinery import SourceFileLoader
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
- # Custom Exceptions
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 AbstractMethodCallError(NotImplementedError):
20
- def __init__(self, cls, method, more_message = ""):
21
- message = f"Abstract method {method}() not implemented in {cls.__name__}."
22
- super().__init__(message + more_message)
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
- # Utility functions/variables
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
- def import_file_as_module(filepath: Optional[FilePath]) -> ModuleType:
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
- Imports a python file as a module.
48
+ Given a template string, render it with the given keyword arguments
32
49
 
33
50
  Parameters:
34
- filepath: The path to the file to import.
51
+ raw_str: The template string
52
+ kwargs: The keyword arguments
35
53
 
36
54
  Returns:
37
- The imported module.
55
+ The rendered string
38
56
  """
39
- filepath = str(filepath) if filepath is not None else None
40
- return SourceFileLoader(filepath, filepath).load_module() if filepath is not None else None
57
+ template = _j2_env.from_string(raw_str)
58
+ return template.render(kwargs)
41
59
 
42
60
 
43
- def join_paths(*paths: FilePath) -> Path:
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
- Joins paths together.
72
+ Reads a file and return its content if required
46
73
 
47
74
  Parameters:
48
- paths: The paths to join.
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
- The joined path.
79
+ Content of the file, or None if doesn't exist and not required
52
80
  """
53
- return Path(*paths)
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 get_row_value(row: pd.Series, value: str) -> Any:
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 {"response_version": 0, "schema": out_schema, "data": in_df_json["data"]}
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) -> List[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 [] if input_str == "" else input_str.split(",")
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
- __version__ = '0.1.1'
1
+ from . import __version__
2
2
 
3
- major_version, minor_version, patch_version = __version__.split('.')
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()