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
squirrels/_utils.py CHANGED
@@ -1,11 +1,9 @@
1
- from typing import Sequence, Optional, Union, TypeVar, Callable, Any, Iterable
1
+ from typing import Sequence, Optional, Union, TypeVar, Callable, Iterable, Literal, Any
2
2
  from datetime import datetime
3
3
  from pathlib import Path
4
- from functools import lru_cache
5
- from pydantic import BaseModel
6
4
  import os, time, logging, json, duckdb, polars as pl, yaml
7
5
  import jinja2 as j2, jinja2.nodes as j2_nodes
8
- import sqlglot, sqlglot.expressions, asyncio
6
+ import sqlglot, sqlglot.expressions, asyncio, hashlib, inspect, base64
9
7
 
10
8
  from . import _constants as c
11
9
  from ._exceptions import ConfigurationError
@@ -20,7 +18,7 @@ polars_dtypes_to_sqrl_dtypes: dict[type[pl.DataType], list[str]] = {
20
18
  pl.Int32: ["integer", "int", "int4"],
21
19
  pl.Int64: ["bigint", "long", "int8"],
22
20
  pl.Float32: ["float", "float4", "real"],
23
- pl.Float64: ["double", "float8"],
21
+ pl.Float64: ["double", "float8", "decimal"], # Note: Polars Decimal type is considered unstable, so we use Float64 for "decimal"
24
22
  pl.Boolean: ["boolean", "bool", "logical"],
25
23
  pl.Date: ["date"],
26
24
  pl.Time: ["time"],
@@ -35,12 +33,20 @@ sqrl_dtypes_to_polars_dtypes: dict[str, type[pl.DataType]] = {sqrl_type: k for k
35
33
  ## Other utility classes
36
34
 
37
35
  class Logger(logging.Logger):
38
- def log_activity_time(self, activity: str, start_timestamp: float, *, request_id: str | None = None) -> None:
36
+ def info(self, msg: str, *, data: dict[str, Any] = {}, **kwargs) -> None:
37
+ super().info(msg, extra={"data": data}, **kwargs)
38
+
39
+ def log_activity_time(self, activity: str, start_timestamp: float, *, additional_data: dict[str, Any] = {}) -> None:
39
40
  end_timestamp = time.time()
40
41
  time_taken = round((end_timestamp-start_timestamp) * 10**3, 3)
41
- data = { "activity": activity, "start_timestamp": start_timestamp, "end_timestamp": end_timestamp, "time_taken_ms": time_taken }
42
- info = { "request_id": request_id } if request_id else {}
43
- self.info(f'Time taken for "{activity}": {time_taken}ms', extra={"data": data, "info": info})
42
+ data = {
43
+ "activity": activity,
44
+ "start_timestamp": start_timestamp,
45
+ "end_timestamp": end_timestamp,
46
+ "time_taken_ms": time_taken,
47
+ **additional_data
48
+ }
49
+ self.info(f'Time taken for "{activity}": {time_taken}ms', data=data)
44
50
 
45
51
 
46
52
  class EnvironmentWithMacros(j2.Environment):
@@ -85,14 +91,6 @@ class EnvironmentWithMacros(j2.Environment):
85
91
 
86
92
  ## Utility functions/variables
87
93
 
88
- def log_activity_time(logger: logging.Logger, activity: str, start_timestamp: float, *, request_id: str | None = None) -> None:
89
- end_timestamp = time.time()
90
- time_taken = round((end_timestamp-start_timestamp) * 10**3, 3)
91
- data = { "activity": activity, "start_timestamp": start_timestamp, "end_timestamp": end_timestamp, "time_taken_ms": time_taken }
92
- info = { "request_id": request_id } if request_id else {}
93
- logger.debug(f'Time taken for "{activity}": {time_taken}ms', extra={"data": data, "info": info})
94
-
95
-
96
94
  def render_string(raw_str: str, *, base_path: str = ".", **kwargs) -> str:
97
95
  """
98
96
  Given a template string, render it with the given keyword arguments
@@ -128,7 +126,7 @@ def read_file(filepath: FilePath) -> str:
128
126
 
129
127
  def normalize_name(name: str) -> str:
130
128
  """
131
- Normalizes names to the convention of the squirrels manifest file.
129
+ Normalizes names to the convention of the squirrels manifest file (with underscores instead of dashes).
132
130
 
133
131
  Arguments:
134
132
  name: The name to normalize.
@@ -141,7 +139,7 @@ def normalize_name(name: str) -> str:
141
139
 
142
140
  def normalize_name_for_api(name: str) -> str:
143
141
  """
144
- Normalizes names to the REST API convention.
142
+ Normalizes names to the REST API convention (with dashes instead of underscores).
145
143
 
146
144
  Arguments:
147
145
  name: The name to normalize.
@@ -196,8 +194,10 @@ def process_if_not_none(input_val: Optional[X], processor: Callable[[X], Y]) ->
196
194
  return processor(input_val)
197
195
 
198
196
 
199
- @lru_cache(maxsize=1)
200
- def _read_duckdb_init_sql() -> tuple[str, Path | None]:
197
+ def _read_duckdb_init_sql(
198
+ *,
199
+ datalake_db_path: str | None = None,
200
+ ) -> str:
201
201
  """
202
202
  Reads and caches the duckdb init file content.
203
203
  Returns None if file doesn't exist or is empty.
@@ -212,35 +212,38 @@ def _read_duckdb_init_sql() -> tuple[str, Path | None]:
212
212
  if Path(c.DUCKDB_INIT_FILE).exists():
213
213
  with open(c.DUCKDB_INIT_FILE, 'r') as f:
214
214
  init_contents.append(f.read())
215
-
216
- init_sql = "\n".join(init_contents).strip()
217
- target_init_path = None
218
- if init_sql:
219
- target_init_path = Path(c.TARGET_FOLDER, c.DUCKDB_INIT_FILE)
220
- target_init_path.parent.mkdir(parents=True, exist_ok=True)
221
- target_init_path.write_text(init_sql)
222
-
223
- return init_sql, target_init_path
215
+
216
+ if datalake_db_path:
217
+ attach_stmt = f"ATTACH '{datalake_db_path}' AS vdl (READ_ONLY);"
218
+ init_contents.append(attach_stmt)
219
+ use_stmt = f"USE vdl;"
220
+ init_contents.append(use_stmt)
221
+
222
+ init_sql = "\n\n".join(init_contents).strip()
223
+ return init_sql
224
224
  except Exception as e:
225
225
  raise ConfigurationError(f"Failed to read {c.DUCKDB_INIT_FILE}: {str(e)}") from e
226
226
 
227
- def create_duckdb_connection(filepath: str | Path = ":memory:", *, read_only: bool = False) -> duckdb.DuckDBPyConnection:
227
+ def create_duckdb_connection(
228
+ db_path: str | Path = ":memory:",
229
+ *,
230
+ datalake_db_path: str | None = None
231
+ ) -> duckdb.DuckDBPyConnection:
228
232
  """
229
233
  Creates a DuckDB connection and initializes it with statements from duckdb init file
230
234
 
231
235
  Arguments:
232
236
  filepath: Path to the DuckDB database file. Defaults to in-memory database.
233
- read_only: Whether to open the database in read-only mode. Defaults to False.
237
+ datalake_db_path: The path to the VDL catalog database if applicable. If exists, this is attached as 'vdl' (READ_ONLY). Default is None.
234
238
 
235
239
  Returns:
236
240
  A DuckDB connection (which must be closed after use)
237
241
  """
238
- conn = duckdb.connect(filepath, read_only=read_only)
242
+ conn = duckdb.connect(db_path)
239
243
 
240
244
  try:
241
- init_sql, _ = _read_duckdb_init_sql()
242
- if init_sql:
243
- conn.execute(init_sql)
245
+ init_sql = _read_duckdb_init_sql(datalake_db_path=datalake_db_path)
246
+ conn.execute(init_sql)
244
247
  except Exception as e:
245
248
  conn.close()
246
249
  raise ConfigurationError(f"Failed to execute {c.DUCKDB_INIT_FILE}: {str(e)}") from e
@@ -284,13 +287,20 @@ def load_yaml_config(filepath: FilePath) -> dict:
284
287
  """
285
288
  try:
286
289
  with open(filepath, 'r') as f:
287
- return yaml.safe_load(f)
290
+ content = yaml.safe_load(f)
291
+ content = content if content else {}
292
+
293
+ if not isinstance(content, dict):
294
+ raise yaml.YAMLError(f"Parsed content from YAML file must be a dictionary. Got: {content}")
295
+
296
+ return content
288
297
  except yaml.YAMLError as e:
289
298
  raise ConfigurationError(f"Failed to parse yaml file: {filepath}") from e
290
299
 
291
300
 
292
301
  def run_duckdb_stmt(
293
- logger: Logger, duckdb_conn: duckdb.DuckDBPyConnection, stmt: str, *, params: dict[str, Any] | None = None, redacted_values: list[str] = []
302
+ logger: Logger, duckdb_conn: duckdb.DuckDBPyConnection, stmt: str, *, params: dict[str, Any] | None = None,
303
+ model_name: str | None = None, redacted_values: list[str] = []
294
304
  ) -> duckdb.DuckDBPyConnection:
295
305
  """
296
306
  Runs a statement on a DuckDB connection
@@ -306,7 +316,8 @@ def run_duckdb_stmt(
306
316
  for value in redacted_values:
307
317
  redacted_stmt = redacted_stmt.replace(value, "[REDACTED]")
308
318
 
309
- logger.info(f"Running statement: {redacted_stmt}", extra={"data": {"params": params}})
319
+ for_model_name = f" for model '{model_name}'" if model_name is not None else ""
320
+ logger.debug(f"Running SQL statement{for_model_name}:\n{redacted_stmt}")
310
321
  try:
311
322
  return duckdb_conn.execute(stmt, params)
312
323
  except duckdb.ParserException as e:
@@ -357,3 +368,74 @@ async def asyncio_gather(coroutines: list):
357
368
  # Wait for tasks to be cancelled
358
369
  await asyncio.gather(*tasks, return_exceptions=True)
359
370
  raise
371
+
372
+
373
+ def hash_string(input_str: str, salt: str) -> str:
374
+ """
375
+ Hashes a string using SHA-256
376
+ """
377
+ return hashlib.sha256((input_str + salt).encode()).hexdigest()
378
+
379
+
380
+ T = TypeVar('T')
381
+ def call_func(func: Callable[..., T], **kwargs) -> T:
382
+ """
383
+ Calls a function with the given arguments if func expects arguments, otherwise calls func without arguments
384
+ """
385
+ sig = inspect.signature(func)
386
+ # Filter kwargs to only include parameters that the function accepts
387
+ filtered_kwargs = {k: v for k, v in kwargs.items() if k in sig.parameters}
388
+ return func(**filtered_kwargs)
389
+
390
+
391
+ def generate_pkce_challenge(code_verifier: str) -> str:
392
+ """Generate PKCE code challenge from code verifier"""
393
+ # Generate SHA256 hash of code_verifier
394
+ verifier_hash = hashlib.sha256(code_verifier.encode('utf-8')).digest()
395
+ # Base64 URL encode (without padding)
396
+ expected_challenge = base64.urlsafe_b64encode(verifier_hash).decode('utf-8').rstrip('=')
397
+ return expected_challenge
398
+
399
+ def validate_pkce_challenge(code_verifier: str, code_challenge: str) -> bool:
400
+ """Validate PKCE code verifier against code challenge"""
401
+ # Generate expected challenge
402
+ expected_challenge = generate_pkce_challenge(code_verifier)
403
+ return expected_challenge == code_challenge
404
+
405
+
406
+ def get_scheme(hostname: str | None) -> str:
407
+ """Get the scheme of the request"""
408
+ return "http" if hostname in ("localhost", "127.0.0.1") else "https"
409
+
410
+
411
+ def to_title_case(input_str: str) -> str:
412
+ """Convert a string to title case"""
413
+ spaced_str = input_str.replace('_', ' ').replace('-', ' ')
414
+ return spaced_str.title()
415
+
416
+
417
+ def to_bool(val: object) -> bool:
418
+ """Convert common truthy/falsey representations to a boolean.
419
+
420
+ Accepted truthy values (case-insensitive): "1", "true", "t", "yes", "y", "on".
421
+ All other values are considered falsey. None is falsey.
422
+ """
423
+ if isinstance(val, bool):
424
+ return val
425
+ if val is None:
426
+ return False
427
+ s = str(val).strip().lower()
428
+ return s in ("1", "true", "t", "yes", "y", "on")
429
+
430
+
431
+ ACCESS_LEVEL = Literal["admin", "member", "guest"]
432
+
433
+ def get_access_level_rank(access_level: ACCESS_LEVEL) -> int:
434
+ """Get the rank of an access level. Lower ranks have more privileges."""
435
+ return { "admin": 1, "member": 2, "guest": 3 }.get(access_level.lower(), 1)
436
+
437
+ def user_has_elevated_privileges(user_access_level: ACCESS_LEVEL, required_access_level: ACCESS_LEVEL) -> bool:
438
+ """Check if a user has privilege to access a resource"""
439
+ user_access_level_rank = get_access_level_rank(user_access_level)
440
+ required_access_level_rank = get_access_level_rank(required_access_level)
441
+ return user_access_level_rank <= required_access_level_rank
squirrels/_version.py CHANGED
@@ -1,3 +1,3 @@
1
- __version__ = '0.5.0'
1
+ __version__ = '0.5.1'
2
2
 
3
3
  sq_major_version, sq_minor_version, sq_patch_version = __version__.split('.')[:3]
squirrels/arguments.py ADDED
@@ -0,0 +1,7 @@
1
+ from ._arguments.init_time_args import ConnectionsArgs, AuthProviderArgs, ParametersArgs, BuildModelArgs
2
+ from ._arguments.run_time_args import ContextArgs, ModelArgs, DashboardArgs
3
+
4
+ __all__ = [
5
+ "ConnectionsArgs", "AuthProviderArgs", "ParametersArgs", "BuildModelArgs",
6
+ "ContextArgs", "ModelArgs", "DashboardArgs"
7
+ ]
squirrels/auth.py ADDED
@@ -0,0 +1,4 @@
1
+ from ._schemas.auth_models import CustomUserFields, RegisteredUser
2
+ from ._auth import ProviderConfigs, provider
3
+
4
+ __all__ = ["CustomUserFields", "RegisteredUser", "ProviderConfigs", "provider"]
@@ -0,0 +1,3 @@
1
+ from ._manifest import ConnectionProperties, ConnectionTypeEnum
2
+
3
+ __all__ = ["ConnectionProperties", "ConnectionTypeEnum"]
squirrels/dashboards.py CHANGED
@@ -1,82 +1,3 @@
1
- import matplotlib.figure as _figure, io as _io, abc as _abc, typing as _t
1
+ from ._dashboards import PngDashboard, HtmlDashboard
2
2
 
3
- from . import _constants as c
4
-
5
-
6
- class Dashboard(metaclass=_abc.ABCMeta):
7
- """
8
- Abstract parent class for all Dashboard classes.
9
- """
10
-
11
- @property
12
- @_abc.abstractmethod
13
- def _content(self) -> bytes | str:
14
- pass
15
-
16
- @property
17
- @_abc.abstractmethod
18
- def _format(self) -> str:
19
- pass
20
-
21
-
22
- class PngDashboard(Dashboard):
23
- """
24
- Instantiate a Dashboard in PNG format from a matplotlib figure or bytes
25
- """
26
-
27
- def __init__(self, content: _figure.Figure | _io.BytesIO | bytes) -> None:
28
- """
29
- Constructor for PngDashboard
30
-
31
- Arguments:
32
- content: The content of the dashboard as a matplotlib.figure.Figure or bytes
33
- """
34
- if isinstance(content, _figure.Figure):
35
- buffer = _io.BytesIO()
36
- content.savefig(buffer, format=c.PNG)
37
- content = buffer.getvalue()
38
-
39
- if isinstance(content, _io.BytesIO):
40
- content = content.getvalue()
41
-
42
- self.__content = content
43
-
44
- @property
45
- def _content(self) -> bytes:
46
- return self.__content
47
-
48
- @property
49
- def _format(self) -> _t.Literal['png']:
50
- return c.PNG
51
-
52
- def _repr_png_(self):
53
- return self._content
54
-
55
-
56
- class HtmlDashboard(Dashboard):
57
- """
58
- Instantiate a Dashboard from an HTML string
59
- """
60
-
61
- def __init__(self, content: _io.StringIO | str) -> None:
62
- """
63
- Constructor for HtmlDashboard
64
-
65
- Arguments:
66
- content: The content of the dashboard as HTML string
67
- """
68
- if isinstance(content, _io.StringIO):
69
- content = content.getvalue()
70
-
71
- self.__content = content
72
-
73
- @property
74
- def _content(self) -> str:
75
- return self.__content
76
-
77
- @property
78
- def _format(self) -> _t.Literal['html']:
79
- return c.HTML
80
-
81
- def _repr_html_(self):
82
- return self._content
3
+ __all__ = ["PngDashboard", "HtmlDashboard"]