squirrels 0.3.2__py3-none-any.whl → 0.4.0__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 (56) hide show
  1. squirrels/__init__.py +7 -3
  2. squirrels/_api_response_models.py +96 -72
  3. squirrels/_api_server.py +375 -201
  4. squirrels/_authenticator.py +23 -22
  5. squirrels/_command_line.py +70 -46
  6. squirrels/_connection_set.py +23 -25
  7. squirrels/_constants.py +29 -78
  8. squirrels/_dashboards_io.py +61 -0
  9. squirrels/_environcfg.py +53 -50
  10. squirrels/_initializer.py +184 -141
  11. squirrels/_manifest.py +168 -195
  12. squirrels/_models.py +159 -292
  13. squirrels/_package_loader.py +7 -8
  14. squirrels/_parameter_configs.py +173 -141
  15. squirrels/_parameter_sets.py +49 -38
  16. squirrels/_py_module.py +7 -7
  17. squirrels/_seeds.py +13 -12
  18. squirrels/_utils.py +114 -54
  19. squirrels/_version.py +1 -1
  20. squirrels/arguments/init_time_args.py +16 -10
  21. squirrels/arguments/run_time_args.py +89 -24
  22. squirrels/dashboards.py +82 -0
  23. squirrels/data_sources.py +212 -232
  24. squirrels/dateutils.py +29 -26
  25. squirrels/package_data/assets/index.css +1 -1
  26. squirrels/package_data/assets/index.js +27 -18
  27. squirrels/package_data/base_project/.gitignore +2 -2
  28. squirrels/package_data/base_project/connections.yml +1 -1
  29. squirrels/package_data/base_project/dashboards/dashboard_example.py +32 -0
  30. squirrels/package_data/base_project/dashboards.yml +10 -0
  31. squirrels/package_data/base_project/docker/.dockerignore +9 -4
  32. squirrels/package_data/base_project/docker/Dockerfile +7 -6
  33. squirrels/package_data/base_project/docker/compose.yml +1 -1
  34. squirrels/package_data/base_project/env.yml +2 -2
  35. squirrels/package_data/base_project/models/dbviews/{database_view1.py → dbview_example.py} +2 -1
  36. squirrels/package_data/base_project/models/dbviews/{database_view1.sql → dbview_example.sql} +3 -2
  37. squirrels/package_data/base_project/models/federates/{dataset_example.py → federate_example.py} +6 -6
  38. squirrels/package_data/base_project/models/federates/{dataset_example.sql → federate_example.sql} +1 -1
  39. squirrels/package_data/base_project/parameters.yml +6 -4
  40. squirrels/package_data/base_project/pyconfigs/auth.py +1 -1
  41. squirrels/package_data/base_project/pyconfigs/connections.py +1 -1
  42. squirrels/package_data/base_project/pyconfigs/context.py +38 -10
  43. squirrels/package_data/base_project/pyconfigs/parameters.py +15 -7
  44. squirrels/package_data/base_project/squirrels.yml.j2 +14 -7
  45. squirrels/package_data/templates/index.html +3 -3
  46. squirrels/parameter_options.py +103 -106
  47. squirrels/parameters.py +347 -195
  48. squirrels/project.py +378 -0
  49. squirrels/user_base.py +14 -6
  50. {squirrels-0.3.2.dist-info → squirrels-0.4.0.dist-info}/METADATA +12 -23
  51. squirrels-0.4.0.dist-info/RECORD +60 -0
  52. squirrels/_timer.py +0 -23
  53. squirrels-0.3.2.dist-info/RECORD +0 -56
  54. {squirrels-0.3.2.dist-info → squirrels-0.4.0.dist-info}/LICENSE +0 -0
  55. {squirrels-0.3.2.dist-info → squirrels-0.4.0.dist-info}/WHEEL +0 -0
  56. {squirrels-0.3.2.dist-info → squirrels-0.4.0.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,4 @@
1
- __pycache__
1
+ **/__pycache__/
2
2
 
3
3
  # common virtual environment names
4
4
  .env/
@@ -6,7 +6,7 @@ __pycache__
6
6
  venv/
7
7
 
8
8
  # squirrels files to ignore
9
- environcfg.yml
10
9
  env.yml
10
+ logs/
11
11
  target/
12
12
  sqrl_packages/
@@ -2,6 +2,6 @@
2
2
  connections:
3
3
  - name: default
4
4
  credential: null
5
- url: {{ env_vars.sqlite_conn_str }} ## using Jinja to substitute environment variable from environcfg.yml
5
+ url: {{ env_vars.sqlite_conn_str }} ## using Jinja to substitute environment variable from env.yml
6
6
 
7
7
 
@@ -0,0 +1,32 @@
1
+ from squirrels import DashboardArgs, dashboards as d
2
+ from matplotlib import pyplot as plt, figure as f, axes as a
3
+
4
+
5
+ async def main(sqrl: DashboardArgs) -> d.PngDashboard:
6
+ """
7
+ Create a dashboard by retrieving datasets using "sqrl.dataset" method and transform the datasets to return as a PngDashboard or a HtmlDashboard.
8
+ - The PngDashboard constructor takes a single argument for either a matplotlib.figure.Figure or io.BytesIO/bytes of PNG data
9
+ - The HtmlDashboard constructor takes a single argument for a io.StringIO/string of HTML data
10
+
11
+ It is imperative to set the correct return type in the function signature for "main" above! It allows Squirrels to provide the correct format to
12
+ the data catalog without having to run this function.
13
+ """
14
+ spending_by_month_df = await sqrl.dataset("dataset_example", fixed_parameters={"group_by": "g4"})
15
+ spending_by_subcategory_df = await sqrl.dataset("dataset_example", fixed_parameters={"group_by": "g3"})
16
+
17
+ # Create a figure with two subplots
18
+ fig, (ax0, ax1) = plt.subplots(2, 1, figsize=(8, 8), height_ratios=(1, 2))
19
+ fig: f.Figure; ax0: a.Axes; ax1: a.Axes
20
+ fig.tight_layout(pad=4, h_pad=6)
21
+
22
+ # Create a bar chart of spending by month
23
+ spending_by_month_df.sort_values("month").plot(x="month", y="total_amount", ax=ax0)
24
+ ax0.set_title("Spending by Month")
25
+
26
+ # Create a pie chart of spending by subcategory
27
+ df_by_subcategory = spending_by_subcategory_df.set_index("subcategory").sort_values("total_amount", ascending=False)
28
+ autopct = lambda pct: ('%.1f%%' % pct) if pct > 6 else ''
29
+ df_by_subcategory.plot(y="total_amount", kind='pie', ax=ax1, autopct=autopct, legend=False, ylabel="")
30
+ ax1.set_title("Spending by Subcategory")
31
+
32
+ return d.PngDashboard(fig)
@@ -0,0 +1,10 @@
1
+ dashboards:
2
+ - name: dashboard_example
3
+ label: Dashboard Example
4
+ scope: public
5
+ parameters:
6
+ - start_date
7
+ - end_date
8
+ - category
9
+
10
+
@@ -1,8 +1,13 @@
1
- __pycache__
2
- venv/
3
- .git
1
+ **/__pycache__/
2
+
3
+ # common virtual environment names
4
+ .venv/
4
5
 
5
6
  # squirrels files to ignore
6
- environcfg.yml
7
+ env.yml
7
8
  target/
8
9
  sqrl_packages/
10
+
11
+ # additional files for docker to ignore
12
+ Dockerfile
13
+ .git/
@@ -1,15 +1,16 @@
1
- # Change here to use different python version (ex. 3.12-slim for version 3.12)
2
- FROM python:3.11-slim
1
+ # Change here to use different python version (ex. 3.11-slim for version 3.11)
2
+ FROM python:3.12-slim
3
3
  WORKDIR /app
4
4
 
5
- # Needed if any python dependencies are installed from git, or for
6
- # "squirrels deps" command if there are packages defined in "squirrels.yml"
5
+ COPY . .
6
+
7
+ # Only needed if there are python dependencies installed using git, or for the
8
+ # "sqrl deps" command if there are packages defined in "squirrels.yml"
7
9
  RUN apt-get update && apt-get install -y git
8
10
 
9
- COPY requirements-lock.txt .
10
11
  RUN pip install --no-cache-dir -r requirements-lock.txt
11
12
 
12
- COPY . .
13
13
  RUN squirrels deps
14
14
 
15
+ EXPOSE 4465
15
16
  CMD ["squirrels", "run", "--host", "0.0.0.0", "--port", "4465"]
@@ -4,4 +4,4 @@ services:
4
4
  ports:
5
5
  - "4465:4465"
6
6
  volumes:
7
- - ./environcfg.yml:/app/environcfg.yml
7
+ - ./env.yml:/app/env.yml
@@ -7,7 +7,7 @@ users:
7
7
  is_internal: True
8
8
  password: I<3Squirrels
9
9
  full_name: Alice Doe
10
- role: employee
10
+ role: manager
11
11
  bob:
12
12
  is_internal: False
13
13
  password: abcd5678
@@ -16,7 +16,7 @@ users:
16
16
 
17
17
  ## Custom environment variables / secrets
18
18
  env_vars:
19
- sqlite_conn_str: sqlite://{username}:{password}@/./assets/expenses.db
19
+ sqlite_conn_str: sqlite://{username}:{password}@/{project_path}/assets/expenses.db
20
20
 
21
21
  ## Database credentials
22
22
  credentials:
@@ -24,7 +24,8 @@ def main(sqrl: ModelArgs) -> pd.DataFrame:
24
24
  WITH
25
25
  transactions_with_masked_id AS (
26
26
  SELECT *,
27
- {masked_id} as masked_id
27
+ {masked_id} as masked_id,
28
+ STRFTIME('%Y-%m', date) AS month
28
29
  FROM transactions
29
30
  )
30
31
  SELECT {sqrl.ctx["select_dim_cols"]}
@@ -5,11 +5,12 @@ transactions_with_masked_id AS (
5
5
  id as masked_id
6
6
  {%- else %}
7
7
  '***' as masked_id
8
- {%- endif %}
8
+ {%- endif %},
9
+ STRFTIME('%Y-%m', date) AS month
9
10
  FROM transactions
10
11
  )
11
12
  SELECT {{ ctx.select_dim_cols }}
12
- , sum(-amount) as total_amount
13
+ , SUM(-amount) as total_amount
13
14
  FROM transactions_with_masked_id
14
15
  WHERE date >= :start_date
15
16
  AND date <= :end_date
@@ -1,14 +1,14 @@
1
- from typing import Iterable
1
+ from typing import Sequence
2
2
  from squirrels import ModelDepsArgs, ModelArgs
3
3
  import pandas as pd
4
4
 
5
5
 
6
- def dependencies(sqrl: ModelDepsArgs) -> Iterable[str]:
6
+ def dependencies(sqrl: ModelDepsArgs) -> Sequence[str]:
7
7
  """
8
8
  Define list of dependent models here. This will determine the dependencies first, at compile-time,
9
9
  before running the model.
10
10
  """
11
- return ["database_view1"]
11
+ return ["dbview_example"]
12
12
 
13
13
 
14
14
  def main(sqrl: ModelArgs) -> pd.DataFrame:
@@ -16,6 +16,6 @@ def main(sqrl: ModelArgs) -> pd.DataFrame:
16
16
  Create federated models by joining/processing dependent database views and/or other federated models to
17
17
  form and return the result as a new pandas DataFrame.
18
18
  """
19
- df = sqrl.ref("database_view1")
20
- order_by_cols: str = sqrl.ctx["order_by_cols_list"]
21
- return df.sort_values(order_by_cols, ascending=False)
19
+ (DBVIEW_EXAMPLE,) = dependencies(sqrl)
20
+ df = sqrl.ref(DBVIEW_EXAMPLE)
21
+ return df.sort_values(sqrl.ctx["order_by_cols_list"], ascending=False)
@@ -1,3 +1,3 @@
1
1
  SELECT *
2
- FROM {{ ref("database_view1") }}
2
+ FROM {{ ref("dbview_example") }}
3
3
  ORDER BY {{ ctx.order_by_cols }}
@@ -36,14 +36,16 @@ parameters:
36
36
  - parent_option_ids: g0
37
37
 
38
38
  - type: DateParameter
39
- factory: CreateWithOptions
39
+ factory: CreateFromSource
40
40
  arguments:
41
41
  name: start_date
42
42
  label: Start Date
43
43
  description: Start date to filter transactions by
44
- all_options:
45
- - default_date: 2023-01-01
46
- date_format: '%Y-%m-%d' ## optional, default, format comes from python datetime, exists for Date and DateRange parameter options
44
+ data_source:
45
+ table_or_query: SELECT min(date) AS min_date, max(date) AS max_date FROM transactions
46
+ default_date_col: min_date
47
+ min_date_col: min_date
48
+ max_date_col: max_date
47
49
 
48
50
  - type: DateParameter
49
51
  factory: CreateWithOptions
@@ -18,7 +18,7 @@ def get_user_if_valid(sqrl: AuthArgs) -> Union[User, WrongPassword, None]:
18
18
  Return:
19
19
  - User instance - if username and password are correct
20
20
  - WrongPassword() - if username exists but password is incorrect
21
- - None - if the username doesn't exist (and search for username will continue for "fake users" configured in environcfg.yml)
21
+ - None - if the username doesn't exist (and search for username will continue for "fake users" configured in env.yml)
22
22
  """
23
23
  mock_users_db = {
24
24
  "johndoe": {
@@ -10,7 +10,7 @@ def main(connections: dict[str, Engine], sqrl: ConnectionsArgs) -> None:
10
10
  ## SQLAlchemy URL for a connection engine
11
11
  conn_str = 'sqlite:///./assets/expenses.db'
12
12
 
13
- ## Can also leverage environment variables and credentials in the environcfg.yml file for connection details
13
+ ## Can also leverage environment variables and credentials in the env.yml file for connection details
14
14
  # conn_str_raw: str = sqrl.env_vars["sqlite_conn_str"]
15
15
  # username, password = sqrl.get_credential('my_key')
16
16
  # conn_str = conn_str_raw.format(username=username, password=password)
@@ -11,9 +11,12 @@ def main(ctx: dict[str, Any], sqrl: ContextArgs) -> None:
11
11
  sqrl.prms and/or sqrl.traits to determine the conditions to execute certain blocks of code.
12
12
  """
13
13
  if sqrl.param_exists("group_by"):
14
- group_by_param: p.SingleSelectParameter = sqrl.prms["group_by"]
14
+ group_by_param = sqrl.prms["group_by"]
15
+ assert isinstance(group_by_param, p.SingleSelectParameter)
16
+
15
17
  columns = group_by_param.get_selected("columns")
16
18
  aliases = group_by_param.get_selected("aliases", default_field="columns")
19
+ assert isinstance(columns, list) and isinstance(aliases, list)
17
20
 
18
21
  ctx["select_dim_cols"] = ", ".join(x+" as "+y for x, y in zip(columns, aliases))
19
22
  ctx["group_by_cols"] = ", ".join(columns)
@@ -21,51 +24,76 @@ def main(ctx: dict[str, Any], sqrl: ContextArgs) -> None:
21
24
  ctx["order_by_cols_list"] = aliases
22
25
 
23
26
  if sqrl.param_exists("description_filter"):
24
- descript_param: p.TextParameter = sqrl.prms["description_filter"]
27
+ descript_param = sqrl.prms["description_filter"]
28
+ assert isinstance(descript_param, p.TextParameter)
29
+
25
30
  desc_pattern = descript_param.get_entered_text().apply_percent_wrap()
31
+
26
32
  sqrl.set_placeholder("desc_pattern", desc_pattern)
27
33
 
28
34
  if sqrl.param_exists("start_date"):
29
- start_date_param: p.DateParameter = sqrl.prms["start_date"]
35
+ start_date_param = sqrl.prms["start_date"]
36
+ assert isinstance(start_date_param, p.DateParameter)
37
+
30
38
  start_date = start_date_param.get_selected_date()
39
+
31
40
  sqrl.set_placeholder("start_date", start_date)
32
41
 
33
42
  if sqrl.param_exists("end_date"):
34
- end_date_param: p.DateParameter = sqrl.prms["end_date"]
43
+ end_date_param = sqrl.prms["end_date"]
44
+ assert isinstance(end_date_param, p.DateParameter)
45
+
35
46
  end_date = end_date_param.get_selected_date()
47
+
36
48
  sqrl.set_placeholder("end_date", end_date)
37
49
 
38
50
  if sqrl.param_exists("date_range"):
39
- date_range_param: p.DateRangeParameter = sqrl.prms["date_range"]
51
+ date_range_param = sqrl.prms["date_range"]
52
+ assert isinstance(date_range_param, p.DateRangeParameter)
53
+
40
54
  start_date = date_range_param.get_selected_start_date()
41
55
  end_date = date_range_param.get_selected_end_date()
56
+
42
57
  sqrl.set_placeholder("start_date", start_date)
43
58
  sqrl.set_placeholder("end_date", end_date)
44
59
 
45
60
  if sqrl.param_exists("category"):
46
- category_param: p.MultiSelectParameter = sqrl.prms["category"]
61
+ category_param = sqrl.prms["category"]
62
+ assert isinstance(category_param, p.MultiSelectParameter)
63
+
47
64
  ctx["has_categories"] = category_param.has_non_empty_selection()
48
65
  ctx["categories"] = category_param.get_selected_labels_quoted_joined()
49
66
 
50
67
  if sqrl.param_exists("subcategory"):
51
- subcategory_param: p.MultiSelectParameter = sqrl.prms["subcategory"]
68
+ subcategory_param = sqrl.prms["subcategory"]
69
+ assert isinstance(subcategory_param, p.MultiSelectParameter)
70
+
52
71
  ctx["has_subcategories"] = subcategory_param.has_non_empty_selection()
53
72
  ctx["subcategories"] = subcategory_param.get_selected_labels_quoted_joined()
54
73
 
55
74
  if sqrl.param_exists("min_filter"):
56
- min_amount_filter: p.NumberParameter = sqrl.prms["min_filter"]
75
+ min_amount_filter = sqrl.prms["min_filter"]
76
+ assert isinstance(min_amount_filter, p.NumberParameter)
77
+
57
78
  min_amount = min_amount_filter.get_selected_value()
79
+
58
80
  sqrl.set_placeholder("min_amount", min_amount)
59
81
 
60
82
  if sqrl.param_exists("max_filter"):
61
- max_amount_filter: p.NumberParameter = sqrl.prms["max_filter"]
83
+ max_amount_filter = sqrl.prms["max_filter"]
84
+ assert isinstance(max_amount_filter, p.NumberParameter)
85
+
62
86
  max_amount = max_amount_filter.get_selected_value()
87
+
63
88
  sqrl.set_placeholder("max_amount", max_amount)
64
89
 
65
90
  if sqrl.param_exists("between_filter"):
66
- between_filter: p.NumberRangeParameter = sqrl.prms["between_filter"]
91
+ between_filter = sqrl.prms["between_filter"]
92
+ assert isinstance(between_filter, p.NumberRangeParameter)
93
+
67
94
  min_amount = between_filter.get_selected_lower_value()
68
95
  max_amount = between_filter.get_selected_upper_value()
96
+
69
97
  sqrl.set_placeholder("min_amount", min_amount)
70
98
  sqrl.set_placeholder("max_amount", max_amount)
71
99
 
@@ -20,6 +20,7 @@ def main(sqrl: ParametersArgs) -> None:
20
20
  group_by_options = [
21
21
  po.SelectParameterOption("g0", "Transaction", columns=["masked_id", "date", "description"], aliases=["id", "date", "description"]),
22
22
  po.SelectParameterOption("g1", "Date", columns=["date"]),
23
+ po.SelectParameterOption("g4", "Month", columns=["month"]),
23
24
  po.SelectParameterOption("g2", "Category", columns=["category"]),
24
25
  po.SelectParameterOption("g3", "Subcategory", columns=["category", "subcategory"]),
25
26
  ]
@@ -29,26 +30,33 @@ def main(sqrl: ParametersArgs) -> None:
29
30
 
30
31
  ## Example of creating a TextParameter
31
32
  parent_name = "group_by"
32
- text_options = [po.TextParameterOption(parent_option_ids="g0")]
33
+ description_text_options = [
34
+ po.TextParameterOption(parent_option_ids="g0")
35
+ ]
33
36
  p.TextParameter.CreateWithOptions(
34
- "description_filter", "Description Contains", text_options, parent_name=parent_name,
37
+ "description_filter", "Description Contains", description_text_options, parent_name=parent_name,
35
38
  description="Substring of description to filter transactions by"
36
39
  )
37
40
 
38
- ## Example of creating DateParameter
39
- p.DateParameter.CreateSimple(
40
- "start_date", "Start Date", "2023-01-01", description="Start date to filter transactions by"
41
+ ## Example of creating DateParameter from lookup query/table
42
+ start_date_source = ds.DateDataSource(
43
+ "SELECT min(date) AS min_date, max(date) AS max_date FROM transactions",
44
+ default_date_col="min_date", min_date_col="min_date", max_date_col="max_date"
45
+ )
46
+ p.DateParameter.CreateFromSource(
47
+ "start_date", "Start Date", start_date_source, description="Start date to filter transactions by"
41
48
  )
42
49
 
43
50
  ## Example of creating DateParameter from list of DateParameterOption's
44
- end_date_option = [po.DateParameterOption("2023-12-31")]
51
+ end_date_option = [po.DateParameterOption("2023-12-31", min_date="2023-01-01", max_date="2023-12-31")]
45
52
  p.DateParameter.CreateWithOptions(
46
53
  "end_date", "End Date", end_date_option, description="End date to filter transactions by"
47
54
  )
48
55
 
49
56
  ## Example of creating DateRangeParameter
50
57
  p.DateRangeParameter.CreateSimple(
51
- "date_range", "Date Range", "2023-01-01", "2023-12-31", description="Date range to filter transactions by"
58
+ "date_range", "Date Range", "2023-01-01", "2023-12-31", min_date="2023-01-01", max_date="2023-12-31",
59
+ description="Date range to filter transactions by"
52
60
  )
53
61
 
54
62
  ## Example of creating MultiSelectParameter from lookup query/table
@@ -6,10 +6,11 @@ project_variables:
6
6
 
7
7
  packages: []
8
8
 
9
- ## Example value for packages:
10
- # - git: https://.../myrepo.git
11
- # revision: v0.1.0
12
- # directory: custom_name ## optional
9
+ ## Example for packages section:
10
+ # packages:
11
+ # - git: https://.../myrepo.git
12
+ # revision: v0.1.0
13
+ # directory: custom_name ## optional
13
14
 
14
15
 
15
16
  {{ connections -}}
@@ -21,6 +22,7 @@ packages: []
21
22
  datasets:
22
23
  - name: dataset_example ## model name uses same name unless "model" field is specified
23
24
  label: Dataset Example
25
+ model: federate_example ## optional - if not specified, then the "name" field is used
24
26
  scope: public ## optional - one of 'public' (default), 'protected', or 'private'
25
27
  parameters: ## optional - if not specified, then all parameters are used
26
28
  - group_by
@@ -35,7 +37,7 @@ datasets:
35
37
 
36
38
  - name: protected_dataset_example ## requires auth.py file to work
37
39
  label: Dataset Example 2
38
- model: dataset_example
40
+ model: federate_example
39
41
  scope: protected
40
42
  parameters:
41
43
  - group_by
@@ -47,6 +49,9 @@ datasets:
47
49
  default_test_set: auth_test1 ## optional - if not specified, uses setting 'selection_test_sets.default_name_used'
48
50
 
49
51
 
52
+ {{ dashboards -}}
53
+
54
+
50
55
  selection_test_sets:
51
56
  - name: no_auth_test1
52
57
  datasets: ## optional section - if not provided, then test set is applicable for any dataset
@@ -77,8 +82,10 @@ settings: {}
77
82
  # auth.token.expire_minutes: 30
78
83
  # parameters.cache.size: 1024
79
84
  # parameters.cache.ttl_minutes: 60
80
- # results.cache.size: 128
81
- # results.cache.ttl_minutes: 60
85
+ # datasets.cache.size: 128
86
+ # datasets.cache.ttl_minutes: 60
87
+ # dashboards.cache.size: 128
88
+ # dashboards.cache.ttl_minutes: 60
82
89
  # selection_test_sets.default_name_used: default
83
90
  # connections.default_name_used: default
84
91
  # defaults.federates.materialized: table
@@ -6,11 +6,11 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <script>
8
8
  const hostname = '';
9
- const catalogURL = '{{ catalog_path }}';
9
+ const projectMetadataURL = '{{ project_metadata_path }}';
10
10
  </script>
11
11
  <title>Squirrels Testing UI</title>
12
- <script type="module" crossorigin src="/assets/index.js"></script>
13
- <link rel="stylesheet" crossorigin href="/assets/index.css">
12
+ <script type="module" crossorigin src="/assets/index.js?version=0.4.0"></script>
13
+ <link rel="stylesheet" crossorigin href="/assets/index.css?version=0.4.0">
14
14
  </head>
15
15
  <body>
16
16
  <div id="root"></div>