squirrels 0.5.0b2__py3-none-any.whl → 0.5.0b4__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.
- dateutils/__init__.py +6 -460
- dateutils/_enums.py +25 -0
- dateutils/_implementation.py +409 -0
- dateutils/types.py +6 -0
- squirrels/__init__.py +9 -13
- squirrels/_api_routes/__init__.py +5 -0
- squirrels/_api_routes/auth.py +262 -0
- squirrels/_api_routes/base.py +154 -0
- squirrels/_api_routes/dashboards.py +142 -0
- squirrels/_api_routes/data_management.py +103 -0
- squirrels/_api_routes/datasets.py +242 -0
- squirrels/_api_routes/oauth2.py +300 -0
- squirrels/_api_routes/project.py +214 -0
- squirrels/_api_server.py +145 -748
- squirrels/_arguments/__init__.py +0 -0
- squirrels/{arguments → _arguments}/init_time_args.py +7 -2
- squirrels/{arguments → _arguments}/run_time_args.py +4 -26
- squirrels/_auth.py +646 -93
- squirrels/_connection_set.py +5 -5
- squirrels/_constants.py +7 -1
- squirrels/{_dashboards_io.py → _dashboards.py} +87 -6
- squirrels/_data_sources.py +564 -0
- squirrels/_exceptions.py +9 -37
- squirrels/_initializer.py +31 -26
- squirrels/_manifest.py +5 -5
- squirrels/_model_builder.py +1 -1
- squirrels/_model_configs.py +2 -2
- squirrels/_model_queries.py +1 -1
- squirrels/_models.py +40 -27
- squirrels/{package_data → _package_data}/base_project/.env +1 -0
- squirrels/{package_data → _package_data}/base_project/.env.example +1 -0
- squirrels/{package_data → _package_data}/base_project/dashboards/dashboard_example.py +4 -4
- squirrels/{package_data → _package_data}/base_project/dashboards/dashboard_example.yml +2 -2
- squirrels/_package_data/base_project/macros/macros_example.sql +17 -0
- squirrels/{package_data → _package_data}/base_project/models/builds/build_example.py +2 -2
- squirrels/{package_data → _package_data}/base_project/models/builds/build_example.sql +1 -1
- squirrels/{package_data → _package_data}/base_project/models/dbviews/dbview_example.sql +1 -1
- squirrels/_package_data/base_project/models/federates/federate_example.py +41 -0
- squirrels/_package_data/base_project/models/federates/federate_example.sql +25 -0
- squirrels/{package_data → _package_data}/base_project/models/federates/federate_example.yml +6 -6
- squirrels/{package_data → _package_data}/base_project/parameters.yml +9 -8
- squirrels/_package_data/base_project/pyconfigs/connections.py +14 -0
- squirrels/{package_data → _package_data}/base_project/pyconfigs/context.py +14 -16
- squirrels/_package_data/base_project/pyconfigs/parameters.py +106 -0
- squirrels/_package_data/base_project/pyconfigs/user.py +51 -0
- squirrels/_package_data/templates/dataset_results.html +112 -0
- squirrels/_package_data/templates/oauth_login.html +271 -0
- squirrels/_parameter_configs.py +35 -35
- squirrels/_parameter_options.py +348 -0
- squirrels/_parameter_sets.py +47 -37
- squirrels/_parameters.py +1664 -0
- squirrels/_project.py +76 -32
- squirrels/_py_module.py +3 -2
- squirrels/_schemas/__init__.py +0 -0
- squirrels/_schemas/auth_models.py +144 -0
- squirrels/_schemas/query_param_models.py +67 -0
- squirrels/{_api_response_models.py → _schemas/response_models.py} +12 -8
- squirrels/_utils.py +38 -4
- squirrels/arguments.py +2 -0
- squirrels/auth.py +1 -0
- squirrels/connections.py +1 -0
- squirrels/dashboards.py +1 -82
- squirrels/data_sources.py +8 -563
- squirrels/parameter_options.py +8 -348
- squirrels/parameters.py +9 -1266
- squirrels/types.py +11 -0
- {squirrels-0.5.0b2.dist-info → squirrels-0.5.0b4.dist-info}/METADATA +4 -1
- squirrels-0.5.0b4.dist-info/RECORD +94 -0
- squirrels/package_data/base_project/macros/macros_example.sql +0 -15
- squirrels/package_data/base_project/models/federates/federate_example.py +0 -44
- squirrels/package_data/base_project/models/federates/federate_example.sql +0 -17
- squirrels/package_data/base_project/pyconfigs/connections.py +0 -14
- squirrels/package_data/base_project/pyconfigs/parameters.py +0 -93
- squirrels/package_data/base_project/pyconfigs/user.py +0 -23
- squirrels-0.5.0b2.dist-info/RECORD +0 -70
- /squirrels/{dataset_result.py → _dataset_types.py} +0 -0
- /squirrels/{package_data → _package_data}/base_project/assets/expenses.db +0 -0
- /squirrels/{package_data → _package_data}/base_project/assets/weather.db +0 -0
- /squirrels/{package_data → _package_data}/base_project/connections.yml +0 -0
- /squirrels/{package_data → _package_data}/base_project/docker/.dockerignore +0 -0
- /squirrels/{package_data → _package_data}/base_project/docker/Dockerfile +0 -0
- /squirrels/{package_data → _package_data}/base_project/docker/compose.yml +0 -0
- /squirrels/{package_data → _package_data}/base_project/duckdb_init.sql +0 -0
- /squirrels/{package_data/base_project/.gitignore → _package_data/base_project/gitignore} +0 -0
- /squirrels/{package_data → _package_data}/base_project/models/builds/build_example.yml +0 -0
- /squirrels/{package_data → _package_data}/base_project/models/dbviews/dbview_example.yml +0 -0
- /squirrels/{package_data → _package_data}/base_project/models/sources.yml +0 -0
- /squirrels/{package_data → _package_data}/base_project/seeds/seed_categories.csv +0 -0
- /squirrels/{package_data → _package_data}/base_project/seeds/seed_categories.yml +0 -0
- /squirrels/{package_data → _package_data}/base_project/seeds/seed_subcategories.csv +0 -0
- /squirrels/{package_data → _package_data}/base_project/seeds/seed_subcategories.yml +0 -0
- /squirrels/{package_data → _package_data}/base_project/squirrels.yml.j2 +0 -0
- /squirrels/{package_data → _package_data}/base_project/tmp/.gitignore +0 -0
- {squirrels-0.5.0b2.dist-info → squirrels-0.5.0b4.dist-info}/WHEEL +0 -0
- {squirrels-0.5.0b2.dist-info → squirrels-0.5.0b4.dist-info}/entry_points.txt +0 -0
- {squirrels-0.5.0b2.dist-info → squirrels-0.5.0b4.dist-info}/licenses/LICENSE +0 -0
|
@@ -7,26 +7,27 @@ parameters:
|
|
|
7
7
|
description: Dimension(s) to aggregate by ## optional, default is empty string
|
|
8
8
|
user_attribute: role ## optional, default is null
|
|
9
9
|
all_options:
|
|
10
|
-
- id:
|
|
10
|
+
- id: trans
|
|
11
11
|
label: Transaction
|
|
12
|
-
columns: ["date", "category", "subcategory", "description"] ## custom field
|
|
12
|
+
columns: ["id", "date", "category", "subcategory", "description"] ## custom field
|
|
13
|
+
aliases: ["_id", "date", "category", "subcategory", "description"] ## custom field (any alias starting with "_" will not be selected - see context.py for implementation)
|
|
13
14
|
is_default: false ## optional, default, exists for SingleSelect or MultiSelect options only
|
|
14
15
|
user_groups: ["manager"] ## optional, default is empty list
|
|
15
16
|
parent_option_ids: [] ## optional, default, exists for all parameter options
|
|
16
|
-
- id:
|
|
17
|
+
- id: day
|
|
17
18
|
label: Day
|
|
18
19
|
columns: [date]
|
|
19
|
-
aliases: [day]
|
|
20
|
+
aliases: [day]
|
|
20
21
|
user_groups: ["manager", "employee"]
|
|
21
|
-
- id:
|
|
22
|
+
- id: month
|
|
22
23
|
label: Month
|
|
23
24
|
columns: [month]
|
|
24
25
|
user_groups: ["manager", "employee"]
|
|
25
|
-
- id:
|
|
26
|
+
- id: cat
|
|
26
27
|
label: Category
|
|
27
28
|
columns: [category]
|
|
28
29
|
user_groups: ["manager", "employee"]
|
|
29
|
-
- id:
|
|
30
|
+
- id: subcat
|
|
30
31
|
label: Subcategory
|
|
31
32
|
columns: [category, subcategory]
|
|
32
33
|
user_groups: ["manager", "employee"]
|
|
@@ -44,7 +45,7 @@ parameters:
|
|
|
44
45
|
max_value: 1000
|
|
45
46
|
increment: 10
|
|
46
47
|
default_value: 1000
|
|
47
|
-
parent_option_ids:
|
|
48
|
+
parent_option_ids: trans
|
|
48
49
|
|
|
49
50
|
- type: DateParameter
|
|
50
51
|
factory: CreateFromSource
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from squirrels import arguments as args, connections as cn
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def main(connections: dict[str, cn.ConnectionProperties | Any], sqrl: args.ConnectionsArgs) -> None:
|
|
6
|
+
"""
|
|
7
|
+
Define sqlalchemy engines by adding them to the "connections" dictionary
|
|
8
|
+
"""
|
|
9
|
+
## SQLAlchemy URL for a connection engine
|
|
10
|
+
conn_str: str = sqrl.env_vars["SQLITE_URI"].format(project_path=sqrl.project_path)
|
|
11
|
+
|
|
12
|
+
## Assigning names to connection engines
|
|
13
|
+
connections["default"] = cn.ConnectionProperties(label="SQLite Expenses Database", type=cn.ConnectionTypeEnum.SQLALCHEMY, uri=conn_str)
|
|
14
|
+
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
from typing import Any
|
|
2
|
-
from squirrels import
|
|
2
|
+
from squirrels import arguments as args, parameters as p
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
def main(ctx: dict[str, Any], sqrl: ContextArgs) -> None:
|
|
5
|
+
def main(ctx: dict[str, Any], sqrl: args.ContextArgs) -> None:
|
|
6
6
|
"""
|
|
7
7
|
Define context variables AFTER parameter selections are made by adding entries to the dictionary "ctx".
|
|
8
8
|
These context variables can then be used in the models.
|
|
@@ -18,52 +18,50 @@ def main(ctx: dict[str, Any], sqrl: ContextArgs) -> None:
|
|
|
18
18
|
aliases = group_by_param.get_selected("aliases", default_field="columns")
|
|
19
19
|
assert isinstance(columns, list) and isinstance(aliases, list) and len(columns) == len(aliases)
|
|
20
20
|
|
|
21
|
-
ctx["
|
|
22
|
-
ctx["
|
|
23
|
-
ctx["
|
|
24
|
-
ctx["
|
|
25
|
-
ctx["
|
|
21
|
+
ctx["group_by_cols"] = columns
|
|
22
|
+
ctx["rename_dict"] = {x: y for x, y in zip(columns, aliases) if not y.startswith("_")}
|
|
23
|
+
ctx["select_dim_cols"] = list(x+" as "+y for x, y in ctx["rename_dict"].items())
|
|
24
|
+
ctx["order_by_cols"] = list(ctx["rename_dict"].values())
|
|
25
|
+
ctx["order_by_cols_desc"] = list(x+" DESC" for x in ctx["order_by_cols"])
|
|
26
26
|
|
|
27
27
|
if sqrl.param_exists("limit"):
|
|
28
28
|
limit_param = sqrl.prms["limit"]
|
|
29
29
|
assert isinstance(limit_param, p.NumberParameter)
|
|
30
30
|
|
|
31
|
-
ctx["
|
|
32
|
-
else:
|
|
33
|
-
ctx["limit_clause"] = ""
|
|
31
|
+
ctx["limit"] = int(limit_param.get_selected_value())
|
|
34
32
|
|
|
35
33
|
if sqrl.param_exists("start_date"):
|
|
36
34
|
start_date_param = sqrl.prms["start_date"]
|
|
37
35
|
assert isinstance(start_date_param, p.DateParameter)
|
|
38
36
|
|
|
39
|
-
ctx["start_date"] = start_date_param.
|
|
37
|
+
ctx["start_date"] = start_date_param.get_selected_date()
|
|
40
38
|
|
|
41
39
|
if sqrl.param_exists("end_date"):
|
|
42
40
|
end_date_param = sqrl.prms["end_date"]
|
|
43
41
|
assert isinstance(end_date_param, p.DateParameter)
|
|
44
42
|
|
|
45
|
-
ctx["end_date"] = end_date_param.
|
|
43
|
+
ctx["end_date"] = end_date_param.get_selected_date()
|
|
46
44
|
|
|
47
45
|
if sqrl.param_exists("date_range"):
|
|
48
46
|
date_range_param = sqrl.prms["date_range"]
|
|
49
47
|
assert isinstance(date_range_param, p.DateRangeParameter)
|
|
50
48
|
|
|
51
|
-
ctx["start_date_from_range"] = date_range_param.
|
|
52
|
-
ctx["end_date_from_range"] = date_range_param.
|
|
49
|
+
ctx["start_date_from_range"] = date_range_param.get_selected_start_date()
|
|
50
|
+
ctx["end_date_from_range"] = date_range_param.get_selected_end_date()
|
|
53
51
|
|
|
54
52
|
if sqrl.param_exists("category"):
|
|
55
53
|
category_param = sqrl.prms["category"]
|
|
56
54
|
assert isinstance(category_param, p.MultiSelectParameter)
|
|
57
55
|
|
|
58
56
|
ctx["has_categories"] = category_param.has_non_empty_selection()
|
|
59
|
-
ctx["categories"] = category_param.
|
|
57
|
+
ctx["categories"] = category_param.get_selected_ids_as_list()
|
|
60
58
|
|
|
61
59
|
if sqrl.param_exists("subcategory"):
|
|
62
60
|
subcategory_param = sqrl.prms["subcategory"]
|
|
63
61
|
assert isinstance(subcategory_param, p.MultiSelectParameter)
|
|
64
62
|
|
|
65
63
|
ctx["has_subcategories"] = subcategory_param.has_non_empty_selection()
|
|
66
|
-
ctx["subcategories"] = subcategory_param.
|
|
64
|
+
ctx["subcategories"] = subcategory_param.get_selected_ids_as_list()
|
|
67
65
|
|
|
68
66
|
if sqrl.param_exists("min_filter"):
|
|
69
67
|
min_amount_filter = sqrl.prms["min_filter"]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from squirrels import arguments as args, parameters as p, parameter_options as po, data_sources as ds
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## Example of creating SingleSelectParameter and specifying each option by code
|
|
5
|
+
@p.SingleSelectParameter.create_with_options(
|
|
6
|
+
"group_by", "Group By", description="Dimension(s) to aggregate by", user_attribute="role"
|
|
7
|
+
)
|
|
8
|
+
def group_by_options():
|
|
9
|
+
return [
|
|
10
|
+
po.SelectParameterOption(
|
|
11
|
+
"trans", "Transaction",
|
|
12
|
+
columns=["id","date","category","subcategory","description"],
|
|
13
|
+
aliases=["_id","date","category","subcategory","description"], # any alias starting with "_" will not be selected - see context.py for implementation
|
|
14
|
+
user_groups=["manager"]
|
|
15
|
+
),
|
|
16
|
+
po.SelectParameterOption("day" , "Day" , columns=["date"], aliases=["day"] , user_groups=["manager","employee"]),
|
|
17
|
+
po.SelectParameterOption("month" , "Month" , columns=["month"] , user_groups=["manager","employee"]),
|
|
18
|
+
po.SelectParameterOption("cat" , "Category" , columns=["category"] , user_groups=["manager","employee"]),
|
|
19
|
+
po.SelectParameterOption("subcat" , "Subcategory" , columns=["category","subcategory"] , user_groups=["manager","employee"]),
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## Example of creating NumberParameter with options
|
|
24
|
+
@p.NumberParameter.create_with_options(
|
|
25
|
+
"limit", "Max Number of Rows", description="Maximum number of rows to return", parent_name="group_by"
|
|
26
|
+
)
|
|
27
|
+
def limit_options():
|
|
28
|
+
return [po.NumberParameterOption(0, 1000, increment=10, default_value=1000, parent_option_ids="trans")]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
## Example of creating DateParameter
|
|
32
|
+
@p.DateParameter.create_from_source(
|
|
33
|
+
"start_date", "Start Date", description="Start date to filter transactions by"
|
|
34
|
+
)
|
|
35
|
+
def start_date_source():
|
|
36
|
+
return ds.DateDataSource(
|
|
37
|
+
"SELECT min(date) AS min_date, max(date) AS max_date FROM expenses",
|
|
38
|
+
default_date_col="min_date", min_date_col="min_date", max_date_col="max_date"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
## Example of creating DateParameter from list of DateParameterOption's
|
|
43
|
+
@p.DateParameter.create_with_options(
|
|
44
|
+
"end_date", "End Date", description="End date to filter transactions by"
|
|
45
|
+
)
|
|
46
|
+
def end_date_options():
|
|
47
|
+
return [po.DateParameterOption("2024-12-31", min_date="2024-01-01", max_date="2024-12-31")]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## Example of creating DateRangeParameter
|
|
51
|
+
@p.DateRangeParameter.create_simple(
|
|
52
|
+
"date_range", "Date Range", "2024-01-01", "2024-12-31", min_date="2024-01-01", max_date="2024-12-31",
|
|
53
|
+
description="Date range to filter transactions by"
|
|
54
|
+
)
|
|
55
|
+
def date_range_options():
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
## Example of creating MultiSelectParameter from lookup query/table
|
|
60
|
+
@p.MultiSelectParameter.create_from_source(
|
|
61
|
+
"category", "Category Filter",
|
|
62
|
+
description="The expense categories to filter transactions by"
|
|
63
|
+
)
|
|
64
|
+
def category_source():
|
|
65
|
+
return ds.SelectDataSource("seed_categories", "category_id", "category", from_seeds=True)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## Example of creating MultiSelectParameter with parent from lookup query/table
|
|
69
|
+
@p.MultiSelectParameter.create_from_source(
|
|
70
|
+
"subcategory", "Subcategory Filter", parent_name="category",
|
|
71
|
+
description="The expense subcategories to filter transactions by (available options are based on selected value(s) of 'Category Filter')"
|
|
72
|
+
)
|
|
73
|
+
def subcategory_source():
|
|
74
|
+
return ds.SelectDataSource(
|
|
75
|
+
"seed_subcategories", "subcategory_id", "subcategory", from_seeds=True, parent_id_col="category_id"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
## Example of creating NumberParameter
|
|
80
|
+
@p.NumberParameter.create_simple(
|
|
81
|
+
"min_filter", "Amounts Greater Than", min_value=0, max_value=300, increment=10,
|
|
82
|
+
description="Number to filter on transactions with an amount greater than this value"
|
|
83
|
+
)
|
|
84
|
+
def min_filter_options():
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
## Example of creating NumberParameter from lookup query/table
|
|
89
|
+
@p.NumberParameter.create_from_source(
|
|
90
|
+
"max_filter", "Amounts Less Than",
|
|
91
|
+
description="Number to filter on transactions with an amount less than this value"
|
|
92
|
+
)
|
|
93
|
+
def max_filter_source():
|
|
94
|
+
query = "SELECT 0 as min_value, 300 as max_value, 10 as increment"
|
|
95
|
+
return ds.NumberDataSource(
|
|
96
|
+
query, "min_value", "max_value", increment_col="increment", default_value_col="max_value"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
## Example of creating NumberRangeParameter
|
|
101
|
+
@p.NumberRangeParameter.create_simple(
|
|
102
|
+
"between_filter", "Amounts Between", min_value=0, max_value=300, default_lower_value=0, default_upper_value=300,
|
|
103
|
+
description="Number range to filter on transactions with an amount within this range"
|
|
104
|
+
)
|
|
105
|
+
def between_filter_options():
|
|
106
|
+
pass
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
from squirrels import auth, arguments as args
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class User(auth.BaseUser):
|
|
6
|
+
"""
|
|
7
|
+
Extend the BaseUser class with custom attributes. The attributes defined here will be added as columns to the users table.
|
|
8
|
+
- Only the following types are supported: [str, int, float, bool, typing.Literal]
|
|
9
|
+
- For str, int, and float types, add "| None" after the type to make it nullable.
|
|
10
|
+
- Always set a default value for the column (use None if default is null).
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
organization: str | None = None
|
|
14
|
+
"""
|
|
15
|
+
role: Literal["manager", "employee"] = "employee"
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def dropped_columns(cls) -> list[str]:
|
|
19
|
+
"""
|
|
20
|
+
The fields defined above cannot be modified once added to the database.
|
|
21
|
+
However, you can choose to drop columns by adding them to this list.
|
|
22
|
+
"""
|
|
23
|
+
return []
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# @auth.provider(name="google", label="Google", icon="https://www.google.com/favicon.ico")
|
|
27
|
+
def google_auth_provider(sqrl: args.AuthProviderArgs) -> auth.ProviderConfigs:
|
|
28
|
+
"""
|
|
29
|
+
Provider configs for authenticating a user using Google credentials.
|
|
30
|
+
|
|
31
|
+
See the following page for setting up the CLIENT_ID and CLIENT_SECRET for Google specifically:
|
|
32
|
+
https://support.google.com/googleapi/answer/6158849?hl=en
|
|
33
|
+
"""
|
|
34
|
+
def get_sqrl_user(claims: dict) -> User:
|
|
35
|
+
return User(
|
|
36
|
+
username=claims["email"],
|
|
37
|
+
is_admin=False,
|
|
38
|
+
role="employee"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# TODO: Add GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET to the .env file
|
|
42
|
+
# Then, uncomment the @auth.provider decorator above and set the client_id and client_secret below
|
|
43
|
+
provider_configs = auth.ProviderConfigs(
|
|
44
|
+
client_id="", # sqrl.env_vars["GOOGLE_CLIENT_ID"],
|
|
45
|
+
client_secret="", # sqrl.env_vars["GOOGLE_CLIENT_SECRET"],
|
|
46
|
+
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
|
|
47
|
+
client_kwargs={"scope": "openid email profile"},
|
|
48
|
+
get_user=get_sqrl_user
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return provider_configs
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.dataset-results {
|
|
3
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
4
|
+
background: white;
|
|
5
|
+
border-radius: 8px;
|
|
6
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
max-width: 100%;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.dataset-results .table-container {
|
|
12
|
+
overflow: auto;
|
|
13
|
+
max-height: 400px;
|
|
14
|
+
background: white;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.dataset-results .data-table {
|
|
18
|
+
width: 100%;
|
|
19
|
+
border-collapse: collapse;
|
|
20
|
+
font-size: 13px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.dataset-results .data-table thead {
|
|
24
|
+
background: #f8f9fa;
|
|
25
|
+
position: sticky;
|
|
26
|
+
top: 0;
|
|
27
|
+
z-index: 10;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.dataset-results .data-table th {
|
|
31
|
+
padding: 8px 12px;
|
|
32
|
+
text-align: left;
|
|
33
|
+
font-weight: 600;
|
|
34
|
+
color: #374151;
|
|
35
|
+
border-bottom: 2px solid #e1e5e9;
|
|
36
|
+
white-space: nowrap;
|
|
37
|
+
font-size: 12px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.dataset-results .data-table th .column-type {
|
|
41
|
+
display: block;
|
|
42
|
+
font-size: 10px;
|
|
43
|
+
color: #6b7280;
|
|
44
|
+
font-weight: 400;
|
|
45
|
+
margin-top: 1px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.dataset-results .data-table td {
|
|
49
|
+
padding: 8px 12px;
|
|
50
|
+
border-bottom: 1px solid #f3f4f6;
|
|
51
|
+
color: #374151;
|
|
52
|
+
vertical-align: top;
|
|
53
|
+
font-size: 12px;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.dataset-results .data-table tbody tr:hover {
|
|
57
|
+
background: #f8f9fa;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.dataset-results .data-table tbody tr:nth-child(even) {
|
|
61
|
+
background: #fafbfc;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.dataset-results .data-table tbody tr:nth-child(even):hover {
|
|
65
|
+
background: #f1f3f4;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.dataset-results .empty-state {
|
|
69
|
+
text-align: center;
|
|
70
|
+
padding: 40px 20px;
|
|
71
|
+
color: #6b7280;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.dataset-results .empty-state h3 {
|
|
75
|
+
font-size: 16px;
|
|
76
|
+
margin-bottom: 6px;
|
|
77
|
+
color: #374151;
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
80
|
+
|
|
81
|
+
<div class="dataset-results">
|
|
82
|
+
{% if schema.fields and data %}
|
|
83
|
+
<div class="table-container">
|
|
84
|
+
<table class="data-table">
|
|
85
|
+
<thead>
|
|
86
|
+
<tr>
|
|
87
|
+
{% for field in schema.fields %}
|
|
88
|
+
<th>
|
|
89
|
+
{{ field.name }}
|
|
90
|
+
<span class="column-type">{{ field.type | upper }}</span>
|
|
91
|
+
</th>
|
|
92
|
+
{% endfor %}
|
|
93
|
+
</tr>
|
|
94
|
+
</thead>
|
|
95
|
+
<tbody>
|
|
96
|
+
{% for row in data %}
|
|
97
|
+
<tr>
|
|
98
|
+
{% for cell in row %}
|
|
99
|
+
<td>{{ cell if cell is not none else '' }}</td>
|
|
100
|
+
{% endfor %}
|
|
101
|
+
</tr>
|
|
102
|
+
{% endfor %}
|
|
103
|
+
</tbody>
|
|
104
|
+
</table>
|
|
105
|
+
</div>
|
|
106
|
+
{% else %}
|
|
107
|
+
<div class="empty-state">
|
|
108
|
+
<h3>No Data Available</h3>
|
|
109
|
+
<p>This dataset returned no results.</p>
|
|
110
|
+
</div>
|
|
111
|
+
{% endif %}
|
|
112
|
+
</div>
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Login - OAuth Authorization</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
box-sizing: border-box;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
16
|
+
background: #f5f7fa;
|
|
17
|
+
min-height: 100vh;
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
justify-content: center;
|
|
21
|
+
padding: 20px;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.login-container {
|
|
25
|
+
background: white;
|
|
26
|
+
border-radius: 12px;
|
|
27
|
+
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
|
28
|
+
padding: 40px;
|
|
29
|
+
width: 100%;
|
|
30
|
+
max-width: 450px;
|
|
31
|
+
position: relative;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.logo {
|
|
35
|
+
text-align: center;
|
|
36
|
+
margin-bottom: 30px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.logo h1 {
|
|
40
|
+
color: #1f2937;
|
|
41
|
+
font-size: 24px;
|
|
42
|
+
font-weight: 700;
|
|
43
|
+
margin-bottom: 8px;
|
|
44
|
+
line-height: 1.3;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.logo .brand {
|
|
48
|
+
color: #1f2937;
|
|
49
|
+
font-weight: 700;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.logo .project {
|
|
53
|
+
color: #6b7280;
|
|
54
|
+
font-weight: 600;
|
|
55
|
+
font-size: 20px;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.logo p {
|
|
59
|
+
color: #6b7280;
|
|
60
|
+
font-size: 14px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.client-info {
|
|
64
|
+
background: #f8f9fa;
|
|
65
|
+
border-radius: 8px;
|
|
66
|
+
padding: 16px;
|
|
67
|
+
margin-bottom: 24px;
|
|
68
|
+
border-left: 4px solid #2563eb;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.client-info h3 {
|
|
72
|
+
color: #333;
|
|
73
|
+
font-size: 16px;
|
|
74
|
+
margin-bottom: 4px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.client-info p {
|
|
78
|
+
color: #666;
|
|
79
|
+
font-size: 14px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.form-group {
|
|
83
|
+
margin-bottom: 20px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.form-group label {
|
|
87
|
+
display: block;
|
|
88
|
+
margin-bottom: 6px;
|
|
89
|
+
color: #333;
|
|
90
|
+
font-weight: 500;
|
|
91
|
+
font-size: 14px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.form-group input {
|
|
95
|
+
width: 100%;
|
|
96
|
+
padding: 12px 16px;
|
|
97
|
+
border: 2px solid #e1e5e9;
|
|
98
|
+
border-radius: 8px;
|
|
99
|
+
font-size: 16px;
|
|
100
|
+
transition: border-color 0.3s ease;
|
|
101
|
+
background-color: #fff;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.form-group input:focus {
|
|
105
|
+
outline: none;
|
|
106
|
+
border-color: #2563eb;
|
|
107
|
+
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.btn {
|
|
111
|
+
width: 100%;
|
|
112
|
+
padding: 12px 16px;
|
|
113
|
+
border: none;
|
|
114
|
+
border-radius: 8px;
|
|
115
|
+
font-size: 16px;
|
|
116
|
+
font-weight: 600;
|
|
117
|
+
cursor: pointer;
|
|
118
|
+
transition: all 0.3s ease;
|
|
119
|
+
text-decoration: none;
|
|
120
|
+
display: inline-block;
|
|
121
|
+
text-align: center;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.btn-primary {
|
|
125
|
+
background: #2563eb;
|
|
126
|
+
color: white;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.btn-primary:hover {
|
|
130
|
+
background: #1d4ed8;
|
|
131
|
+
transform: translateY(-1px);
|
|
132
|
+
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.btn-provider {
|
|
136
|
+
background: #fff;
|
|
137
|
+
color: #333;
|
|
138
|
+
border: 2px solid #e1e5e9;
|
|
139
|
+
margin-bottom: 12px;
|
|
140
|
+
display: flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
justify-content: center;
|
|
143
|
+
gap: 12px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.btn-provider:hover {
|
|
147
|
+
border-color: #2563eb;
|
|
148
|
+
background: #f8f9fa;
|
|
149
|
+
transform: translateY(-1px);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.provider-icon {
|
|
153
|
+
width: 20px;
|
|
154
|
+
height: 20px;
|
|
155
|
+
border-radius: 50%;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.divider {
|
|
159
|
+
text-align: center;
|
|
160
|
+
margin: 24px 0;
|
|
161
|
+
position: relative;
|
|
162
|
+
color: #666;
|
|
163
|
+
font-size: 14px;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.divider::before {
|
|
167
|
+
content: '';
|
|
168
|
+
position: absolute;
|
|
169
|
+
top: 50%;
|
|
170
|
+
left: 0;
|
|
171
|
+
right: 0;
|
|
172
|
+
height: 1px;
|
|
173
|
+
background: #e1e5e9;
|
|
174
|
+
z-index: 1;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.divider span {
|
|
178
|
+
background: white;
|
|
179
|
+
padding: 0 16px;
|
|
180
|
+
position: relative;
|
|
181
|
+
z-index: 2;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.error-message {
|
|
185
|
+
background: #fee;
|
|
186
|
+
border: 1px solid #fcc;
|
|
187
|
+
color: #c33;
|
|
188
|
+
padding: 12px;
|
|
189
|
+
border-radius: 8px;
|
|
190
|
+
margin-bottom: 20px;
|
|
191
|
+
font-size: 14px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.info-message {
|
|
195
|
+
background: #e3f2fd;
|
|
196
|
+
border: 1px solid #bbdefb;
|
|
197
|
+
color: #1976d2;
|
|
198
|
+
padding: 12px;
|
|
199
|
+
border-radius: 8px;
|
|
200
|
+
margin-bottom: 20px;
|
|
201
|
+
font-size: 14px;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@media (max-width: 480px) {
|
|
205
|
+
.login-container {
|
|
206
|
+
padding: 24px;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.logo h1 {
|
|
210
|
+
font-size: 24px;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
</style>
|
|
214
|
+
</head>
|
|
215
|
+
<body>
|
|
216
|
+
<div class="login-container">
|
|
217
|
+
<div class="logo">
|
|
218
|
+
<h1>
|
|
219
|
+
<span class="brand">Squirrels Project</span>
|
|
220
|
+
{% if project_name %}
|
|
221
|
+
<br>
|
|
222
|
+
<span class="project">{{ project_name }}</span>
|
|
223
|
+
{% endif %}
|
|
224
|
+
</h1>
|
|
225
|
+
<p>Please sign in to continue</p>
|
|
226
|
+
</div>
|
|
227
|
+
|
|
228
|
+
{% if client_name %}
|
|
229
|
+
<div class="client-info">
|
|
230
|
+
<h3>{{ client_name }}</h3>
|
|
231
|
+
<p>wants to access your account</p>
|
|
232
|
+
</div>
|
|
233
|
+
{% endif %}
|
|
234
|
+
|
|
235
|
+
<!-- Username/Password Login Form -->
|
|
236
|
+
<form method="post" action="{{ login_url }}?redirect_url={{ return_url | urlencode }}">
|
|
237
|
+
<div class="form-group">
|
|
238
|
+
<label for="username">Username</label>
|
|
239
|
+
<input type="text" id="username" name="username" required autocomplete="username">
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<div class="form-group">
|
|
243
|
+
<label for="password">Password</label>
|
|
244
|
+
<input type="password" id="password" name="password" required autocomplete="current-password">
|
|
245
|
+
</div>
|
|
246
|
+
|
|
247
|
+
<input type="hidden" name="redirect_url" value="{{ return_url }}">
|
|
248
|
+
|
|
249
|
+
<button type="submit" class="btn btn-primary">
|
|
250
|
+
Sign In
|
|
251
|
+
</button>
|
|
252
|
+
</form>
|
|
253
|
+
|
|
254
|
+
{% if providers and providers|length > 0 %}
|
|
255
|
+
<div class="divider">
|
|
256
|
+
<span>or</span>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<!-- OAuth Provider Login Options -->
|
|
260
|
+
<div class="providers-section">
|
|
261
|
+
{% for provider in providers %}
|
|
262
|
+
<a href="{{ provider.login_url }}?redirect_url={{ return_url | urlencode }}" class="btn btn-provider">
|
|
263
|
+
<img src="{{ provider.icon }}" alt="{{ provider.label }}" class="provider-icon" onerror="this.style.display='none'">
|
|
264
|
+
<span>Continue with {{ provider.label }}</span>
|
|
265
|
+
</a>
|
|
266
|
+
{% endfor %}
|
|
267
|
+
</div>
|
|
268
|
+
{% endif %}
|
|
269
|
+
</div>
|
|
270
|
+
</body>
|
|
271
|
+
</html>
|