squirrels 0.5.0rc0__py3-none-any.whl → 0.5.1__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 (108) hide show
  1. dateutils/__init__.py +6 -0
  2. dateutils/_enums.py +25 -0
  3. squirrels/dateutils.py → dateutils/_implementation.py +58 -111
  4. dateutils/types.py +6 -0
  5. squirrels/__init__.py +10 -12
  6. squirrels/_api_routes/__init__.py +5 -0
  7. squirrels/_api_routes/auth.py +271 -0
  8. squirrels/_api_routes/base.py +171 -0
  9. squirrels/_api_routes/dashboards.py +158 -0
  10. squirrels/_api_routes/data_management.py +148 -0
  11. squirrels/_api_routes/datasets.py +265 -0
  12. squirrels/_api_routes/oauth2.py +298 -0
  13. squirrels/_api_routes/project.py +252 -0
  14. squirrels/_api_server.py +245 -781
  15. squirrels/_arguments/__init__.py +0 -0
  16. squirrels/{arguments → _arguments}/init_time_args.py +7 -2
  17. squirrels/{arguments → _arguments}/run_time_args.py +13 -35
  18. squirrels/_auth.py +720 -212
  19. squirrels/_command_line.py +81 -41
  20. squirrels/_compile_prompts.py +147 -0
  21. squirrels/_connection_set.py +16 -7
  22. squirrels/_constants.py +29 -9
  23. squirrels/{_dashboards_io.py → _dashboards.py} +87 -6
  24. squirrels/_data_sources.py +570 -0
  25. squirrels/{dataset_result.py → _dataset_types.py} +2 -4
  26. squirrels/_exceptions.py +9 -37
  27. squirrels/_initializer.py +83 -59
  28. squirrels/_logging.py +117 -0
  29. squirrels/_manifest.py +129 -62
  30. squirrels/_model_builder.py +10 -52
  31. squirrels/_model_configs.py +3 -3
  32. squirrels/_model_queries.py +1 -1
  33. squirrels/_models.py +249 -118
  34. squirrels/{package_data → _package_data}/base_project/.env +16 -4
  35. squirrels/{package_data → _package_data}/base_project/.env.example +15 -3
  36. squirrels/{package_data → _package_data}/base_project/connections.yml +4 -3
  37. squirrels/{package_data → _package_data}/base_project/dashboards/dashboard_example.py +4 -4
  38. squirrels/_package_data/base_project/dashboards/dashboard_example.yml +22 -0
  39. squirrels/{package_data → _package_data}/base_project/duckdb_init.sql +1 -0
  40. squirrels/_package_data/base_project/macros/macros_example.sql +17 -0
  41. squirrels/{package_data → _package_data}/base_project/models/builds/build_example.py +2 -2
  42. squirrels/{package_data → _package_data}/base_project/models/builds/build_example.sql +1 -1
  43. squirrels/{package_data → _package_data}/base_project/models/builds/build_example.yml +2 -0
  44. squirrels/_package_data/base_project/models/dbviews/dbview_example.sql +17 -0
  45. squirrels/_package_data/base_project/models/dbviews/dbview_example.yml +32 -0
  46. squirrels/_package_data/base_project/models/federates/federate_example.py +48 -0
  47. squirrels/_package_data/base_project/models/federates/federate_example.sql +21 -0
  48. squirrels/{package_data → _package_data}/base_project/models/federates/federate_example.yml +7 -7
  49. squirrels/{package_data → _package_data}/base_project/models/sources.yml +5 -6
  50. squirrels/{package_data → _package_data}/base_project/parameters.yml +32 -45
  51. squirrels/_package_data/base_project/pyconfigs/connections.py +18 -0
  52. squirrels/{package_data → _package_data}/base_project/pyconfigs/context.py +31 -22
  53. squirrels/_package_data/base_project/pyconfigs/parameters.py +141 -0
  54. squirrels/_package_data/base_project/pyconfigs/user.py +44 -0
  55. squirrels/{package_data → _package_data}/base_project/seeds/seed_categories.yml +1 -1
  56. squirrels/{package_data → _package_data}/base_project/seeds/seed_subcategories.yml +1 -1
  57. squirrels/_package_data/base_project/squirrels.yml.j2 +61 -0
  58. squirrels/_package_data/templates/dataset_results.html +112 -0
  59. squirrels/_package_data/templates/oauth_login.html +271 -0
  60. squirrels/_package_data/templates/squirrels_studio.html +20 -0
  61. squirrels/_parameter_configs.py +76 -55
  62. squirrels/_parameter_options.py +348 -0
  63. squirrels/_parameter_sets.py +53 -45
  64. squirrels/_parameters.py +1664 -0
  65. squirrels/_project.py +403 -242
  66. squirrels/_py_module.py +3 -2
  67. squirrels/_request_context.py +33 -0
  68. squirrels/_schemas/__init__.py +0 -0
  69. squirrels/_schemas/auth_models.py +167 -0
  70. squirrels/_schemas/query_param_models.py +75 -0
  71. squirrels/{_api_response_models.py → _schemas/response_models.py} +48 -18
  72. squirrels/_seeds.py +1 -1
  73. squirrels/_sources.py +23 -19
  74. squirrels/_utils.py +121 -39
  75. squirrels/_version.py +1 -1
  76. squirrels/arguments.py +7 -0
  77. squirrels/auth.py +4 -0
  78. squirrels/connections.py +3 -0
  79. squirrels/dashboards.py +2 -81
  80. squirrels/data_sources.py +14 -563
  81. squirrels/parameter_options.py +13 -348
  82. squirrels/parameters.py +14 -1266
  83. squirrels/types.py +16 -0
  84. {squirrels-0.5.0rc0.dist-info → squirrels-0.5.1.dist-info}/METADATA +42 -30
  85. squirrels-0.5.1.dist-info/RECORD +98 -0
  86. squirrels/package_data/base_project/dashboards/dashboard_example.yml +0 -22
  87. squirrels/package_data/base_project/macros/macros_example.sql +0 -15
  88. squirrels/package_data/base_project/models/dbviews/dbview_example.sql +0 -12
  89. squirrels/package_data/base_project/models/dbviews/dbview_example.yml +0 -26
  90. squirrels/package_data/base_project/models/federates/federate_example.py +0 -44
  91. squirrels/package_data/base_project/models/federates/federate_example.sql +0 -17
  92. squirrels/package_data/base_project/pyconfigs/connections.py +0 -14
  93. squirrels/package_data/base_project/pyconfigs/parameters.py +0 -93
  94. squirrels/package_data/base_project/pyconfigs/user.py +0 -23
  95. squirrels/package_data/base_project/squirrels.yml.j2 +0 -71
  96. squirrels-0.5.0rc0.dist-info/RECORD +0 -70
  97. /squirrels/{package_data → _package_data}/base_project/assets/expenses.db +0 -0
  98. /squirrels/{package_data → _package_data}/base_project/assets/weather.db +0 -0
  99. /squirrels/{package_data → _package_data}/base_project/docker/.dockerignore +0 -0
  100. /squirrels/{package_data → _package_data}/base_project/docker/Dockerfile +0 -0
  101. /squirrels/{package_data → _package_data}/base_project/docker/compose.yml +0 -0
  102. /squirrels/{package_data/base_project/.gitignore → _package_data/base_project/gitignore} +0 -0
  103. /squirrels/{package_data → _package_data}/base_project/seeds/seed_categories.csv +0 -0
  104. /squirrels/{package_data → _package_data}/base_project/seeds/seed_subcategories.csv +0 -0
  105. /squirrels/{package_data → _package_data}/base_project/tmp/.gitignore +0 -0
  106. {squirrels-0.5.0rc0.dist-info → squirrels-0.5.1.dist-info}/WHEEL +0 -0
  107. {squirrels-0.5.0rc0.dist-info → squirrels-0.5.1.dist-info}/entry_points.txt +0 -0
  108. {squirrels-0.5.0rc0.dist-info → squirrels-0.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,141 @@
1
+ from squirrels import 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
+ name="group_by", label="Group By",
7
+ description="Dimension(s) to aggregate by",
8
+ user_attribute="access_level"
9
+ )
10
+ def group_by_options():
11
+ return [
12
+ po.SelectParameterOption(
13
+ id="trans", label="Transaction",
14
+ columns=["id","date","category","subcategory","description"],
15
+ aliases=["_id","date","category","subcategory","description"], # in context.py, any alias starting with "_" will not be selected
16
+ user_groups=["admin"]
17
+ ),
18
+ po.SelectParameterOption(
19
+ id="day", label="Day",
20
+ columns=["date"],
21
+ aliases=["day"],
22
+ user_groups=["admin","member"]
23
+ ),
24
+ po.SelectParameterOption(
25
+ id="month", label="Month",
26
+ columns=["month"],
27
+ user_groups=["admin","member","guest"]
28
+ ),
29
+ po.SelectParameterOption(
30
+ id="cat", label="Category",
31
+ columns=["category"],
32
+ user_groups=["admin","member","guest"]
33
+ ),
34
+ po.SelectParameterOption(
35
+ id="subcat", label="Subcategory",
36
+ columns=["category","subcategory"],
37
+ user_groups=["admin","member","guest"]
38
+ ),
39
+ ]
40
+
41
+
42
+ ## Example of creating DateParameter
43
+ @p.DateParameter.create_from_source(
44
+ name="start_date", label="Start Date",
45
+ description="Start date to filter transactions by"
46
+ )
47
+ def start_date_source():
48
+ return ds.DateDataSource(
49
+ table_or_query="SELECT min(date) AS min_date, max(date) AS max_date FROM expenses",
50
+ default_date_col="min_date",
51
+ min_date_col="min_date", max_date_col="max_date",
52
+ )
53
+
54
+
55
+ ## Example of creating DateParameter from list of DateParameterOption's
56
+ @p.DateParameter.create_with_options(
57
+ name="end_date", label="End Date",
58
+ description="End date to filter transactions by"
59
+ )
60
+ def end_date_options():
61
+ return [
62
+ po.DateParameterOption(
63
+ default_date="2024-12-31", min_date="2024-01-01", max_date="2024-12-31"
64
+ )
65
+ ]
66
+
67
+
68
+ ## Example of creating DateRangeParameter
69
+ @p.DateRangeParameter.create_simple(
70
+ name="date_range", label="Date Range",
71
+ default_start_date="2024-01-01", default_end_date="2024-12-31",
72
+ min_date="2024-01-01", max_date="2024-12-31",
73
+ description="Date range to filter transactions by"
74
+ )
75
+ def date_range_options():
76
+ pass
77
+
78
+
79
+ ## Example of creating MultiSelectParameter from lookup query/table
80
+ @p.MultiSelectParameter.create_from_source(
81
+ name="category", label="Category Filter",
82
+ description="The expense categories to filter transactions by"
83
+ )
84
+ def category_source():
85
+ return ds.SelectDataSource(
86
+ table_or_query="seed_categories",
87
+ id_col="category_id",
88
+ options_col="category",
89
+ source=ds.SourceEnum.SEEDS
90
+ )
91
+
92
+
93
+ ## Example of creating MultiSelectParameter with parent from lookup query/table
94
+ @p.MultiSelectParameter.create_from_source(
95
+ name="subcategory", label="Subcategory Filter",
96
+ description="The expense subcategories to filter transactions by (available options are based on selected value(s) of 'Category Filter')",
97
+ parent_name="category"
98
+ )
99
+ def subcategory_source():
100
+ return ds.SelectDataSource(
101
+ table_or_query="seed_subcategories",
102
+ id_col="subcategory_id",
103
+ options_col="subcategory",
104
+ source=ds.SourceEnum.SEEDS,
105
+ parent_id_col="category_id"
106
+ )
107
+
108
+
109
+ ## Example of creating NumberParameter
110
+ @p.NumberParameter.create_simple(
111
+ name="min_filter", label="Amounts Greater Than",
112
+ min_value=0, max_value=300, increment=10,
113
+ description="Number to filter on transactions with an amount greater than this value"
114
+ )
115
+ def min_filter_options():
116
+ pass
117
+
118
+
119
+ ## Example of creating NumberParameter from lookup query/table
120
+ @p.NumberParameter.create_from_source(
121
+ name="max_filter", label="Amounts Less Than",
122
+ description="Number to filter on transactions with an amount less than this value"
123
+ )
124
+ def max_filter_source():
125
+ return ds.NumberDataSource(
126
+ table_or_query="SELECT 0 as min_value, 300 as max_value, 10 as increment",
127
+ min_value_col="min_value", max_value_col="max_value",
128
+ increment_col="increment",
129
+ default_value_col="max_value"
130
+ )
131
+
132
+
133
+ ## Example of creating NumberRangeParameter
134
+ @p.NumberRangeParameter.create_simple(
135
+ name="between_filter", label="Amounts Between",
136
+ min_value=0, max_value=300,
137
+ default_lower_value=0, default_upper_value=300,
138
+ description="Number range to filter on transactions with an amount within this range"
139
+ )
140
+ def between_filter_options():
141
+ pass
@@ -0,0 +1,44 @@
1
+ from typing import Literal
2
+ from squirrels import auth, arguments as args
3
+
4
+
5
+ class CustomUserFields(auth.CustomUserFields):
6
+ """
7
+ Extend the CustomUserFields class to add custom user attributes.
8
+ - Only the following types are supported: [str, int, float, bool, typing.Literal]
9
+ - Add "| None" after the type to make it nullable.
10
+ - Always set a default value for the field (use None if default is null).
11
+
12
+ Example:
13
+ organization: str | None = None
14
+ """
15
+ role: Literal["manager", "staff", "customer"] = "staff"
16
+
17
+
18
+ # @auth.provider(name="google", label="Google", icon="https://www.google.com/favicon.ico")
19
+ def google_auth_provider(sqrl: args.AuthProviderArgs) -> auth.ProviderConfigs:
20
+ """
21
+ Provider configs for authenticating a user using Google credentials.
22
+
23
+ See the following page for setting up the CLIENT_ID and CLIENT_SECRET for Google specifically:
24
+ https://support.google.com/googleapi/answer/6158849?hl=en
25
+ """
26
+ def get_sqrl_user(claims: dict) -> auth.RegisteredUser:
27
+ custom_fields = CustomUserFields(role="customer")
28
+ return auth.RegisteredUser(
29
+ username=claims["email"],
30
+ access_level="member",
31
+ custom_fields=custom_fields
32
+ )
33
+
34
+ # TODO: Add GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET to the .env file
35
+ # Then, uncomment the @auth.provider decorator above and set the client_id and client_secret below
36
+ provider_configs = auth.ProviderConfigs(
37
+ client_id="", # sqrl.env_vars["GOOGLE_CLIENT_ID"],
38
+ client_secret="", # sqrl.env_vars["GOOGLE_CLIENT_SECRET"],
39
+ server_url="https://accounts.google.com",
40
+ client_kwargs={"scope": "openid email profile"},
41
+ get_user=get_sqrl_user
42
+ )
43
+
44
+ return provider_configs
@@ -1,7 +1,7 @@
1
1
  description: |
2
2
  Lookup table for the category IDs and names of transactions.
3
3
 
4
- cast_column_types: true # optional, default is false - setting SQRL_SEEDS__INFER_SCHEMA is ignored for this seed if this is set to true
4
+ cast_column_types: true # optional, default is false - if set to true, then SQRL_SEEDS__INFER_SCHEMA is ignored for this seed
5
5
 
6
6
  columns:
7
7
  - name: category_id
@@ -1,7 +1,7 @@
1
1
  description: |
2
2
  Lookup table for the subcategory IDs and names of transactions.
3
3
 
4
- cast_column_types: true # optional, default is false - setting SQRL_SEEDS__INFER_SCHEMA is ignored for this seed if this is set to true
4
+ cast_column_types: true # optional, default is false - if set to true, then SQRL_SEEDS__INFER_SCHEMA is ignored for this seed
5
5
 
6
6
  columns:
7
7
  - name: subcategory_id
@@ -0,0 +1,61 @@
1
+ project_variables:
2
+ name: sample_expenses
3
+ label: "Sample Expenses"
4
+ description: This is a sample squirrels project for analyzing expense transactions
5
+ major_version: 1
6
+
7
+
8
+ packages: []
9
+
10
+ ## Example for packages section:
11
+ # packages:
12
+ # - git: https://.../myrepo.git
13
+ # revision: v0.1.0
14
+ # directory: custom_name ## optional
15
+
16
+
17
+ {{ connections -}}
18
+
19
+
20
+ {{ parameters -}}
21
+
22
+
23
+ datasets:
24
+ - name: expense_transactions ## model name uses same name unless "model" field is specified
25
+ label: Expense Transactions - DBView Example
26
+ description: All expense transactions
27
+ model: dbview_example
28
+ scope: private ## optional - one of 'public' (default), 'protected', or 'private'
29
+ parameters: ## optional - if not specified, then all parameters are used
30
+ - start_date
31
+ - end_date
32
+ - min_filter
33
+ - max_filter
34
+
35
+ - name: grouped_expenses
36
+ label: Grouped Expenses - Federate Example
37
+ description: Aggregated expense transactions by custom dimension using federate_example model
38
+ model: federate_example
39
+ scope: public ## using an auth.py file is suggested for protected or private datasets
40
+ parameters:
41
+ - group_by
42
+ - date_range
43
+ - category
44
+ - subcategory
45
+ - between_filter
46
+
47
+
48
+ selection_test_sets:
49
+ - name: set_start_date
50
+ parameters: ## optional section - if not provided, then default value is used for all parameters
51
+ start_date: 2024-07-01 ## this parameter is only used by model "dbview_example"
52
+
53
+ - name: set_date_range
54
+ parameters:
55
+ date_range: [2024-02-01,2024-11-30] ## this parameter is only used by model "federate_example"
56
+
57
+ - name: use_admin_privileged_group_by
58
+ user:
59
+ access_level: admin
60
+ parameters:
61
+ group_by: trans
@@ -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>
@@ -0,0 +1,20 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link id="favicon" rel="icon" type="image/x-icon" href="{{ sqrl_studio_base_url }}/favicon.ico" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>Squirrels Studio</title>
8
+ <script>
9
+ // Optional: Set global defaults for project parameters
10
+ window.DEFAULT_HOSTNAME = '';
11
+ window.DEFAULT_PROJECT_NAME = '{{ project_name }}';
12
+ window.DEFAULT_PROJECT_VERSION = '{{ project_version }}';
13
+ </script>
14
+ <script type="module" crossorigin src="{{ sqrl_studio_base_url }}/assets/index.js"></script>
15
+ <link rel="stylesheet" crossorigin href="{{ sqrl_studio_base_url }}/assets/index.css">
16
+ </head>
17
+ <body>
18
+ <div id="root"></div>
19
+ </body>
20
+ </html>