squirrels 0.5.0b3__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.
- squirrels/__init__.py +2 -0
- 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 +142 -745
- squirrels/_arguments/__init__.py +0 -0
- squirrels/_arguments/{_init_time_args.py → init_time_args.py} +5 -0
- squirrels/_arguments/{_run_time_args.py → run_time_args.py} +1 -1
- squirrels/_auth.py +645 -92
- squirrels/_connection_set.py +1 -1
- squirrels/_constants.py +6 -0
- squirrels/{_dashboards_io.py → _dashboards.py} +87 -6
- squirrels/_exceptions.py +9 -37
- squirrels/_model_builder.py +1 -1
- squirrels/_model_queries.py +1 -1
- squirrels/_models.py +13 -12
- squirrels/_package_data/base_project/.env +1 -0
- squirrels/_package_data/base_project/.env.example +1 -0
- squirrels/_package_data/base_project/pyconfigs/parameters.py +84 -76
- squirrels/_package_data/base_project/pyconfigs/user.py +30 -2
- squirrels/_package_data/templates/dataset_results.html +112 -0
- squirrels/_package_data/templates/oauth_login.html +271 -0
- squirrels/_parameter_configs.py +1 -1
- squirrels/_parameter_sets.py +31 -21
- squirrels/_parameters.py +521 -123
- squirrels/_project.py +43 -24
- 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 +34 -2
- squirrels/arguments.py +2 -2
- squirrels/auth.py +1 -0
- squirrels/dashboards.py +1 -1
- squirrels/types.py +3 -3
- {squirrels-0.5.0b3.dist-info → squirrels-0.5.0b4.dist-info}/METADATA +4 -1
- {squirrels-0.5.0b3.dist-info → squirrels-0.5.0b4.dist-info}/RECORD +46 -32
- squirrels/_dashboard_types.py +0 -82
- {squirrels-0.5.0b3.dist-info → squirrels-0.5.0b4.dist-info}/WHEEL +0 -0
- {squirrels-0.5.0b3.dist-info → squirrels-0.5.0b4.dist-info}/entry_points.txt +0 -0
- {squirrels-0.5.0b3.dist-info → squirrels-0.5.0b4.dist-info}/licenses/LICENSE +0 -0
squirrels/_project.py
CHANGED
|
@@ -5,14 +5,16 @@ import asyncio, typing as t, functools as ft, shutil, json, os
|
|
|
5
5
|
import logging as l, matplotlib.pyplot as plt, networkx as nx, polars as pl
|
|
6
6
|
import sqlglot, sqlglot.expressions
|
|
7
7
|
|
|
8
|
-
from ._auth import Authenticator, BaseUser
|
|
8
|
+
from ._auth import Authenticator, BaseUser, AuthProviderArgs, ProviderFunctionType
|
|
9
|
+
from ._schemas import response_models as rm
|
|
9
10
|
from ._model_builder import ModelBuilder
|
|
10
11
|
from ._exceptions import InvalidInputError, ConfigurationError
|
|
11
|
-
from . import
|
|
12
|
+
from ._py_module import PyModule
|
|
13
|
+
from . import _dashboards as d, _utils as u, _constants as c, _manifest as mf, _connection_set as cs
|
|
12
14
|
from . import _seeds as s, _models as m, _model_configs as mc, _model_queries as mq, _sources as so
|
|
13
|
-
from . import _parameter_sets as ps,
|
|
15
|
+
from . import _parameter_sets as ps, _dataset_types as dr
|
|
14
16
|
|
|
15
|
-
T = t.TypeVar("T", bound=
|
|
17
|
+
T = t.TypeVar("T", bound=d.Dashboard)
|
|
16
18
|
M = t.TypeVar("M", bound=m.DataModel)
|
|
17
19
|
|
|
18
20
|
|
|
@@ -129,16 +131,33 @@ class SquirrelsProject:
|
|
|
129
131
|
return cs.ConnectionSetIO.load_from_file(self._logger, self._filepath, self._manifest_cfg, self._conn_args)
|
|
130
132
|
|
|
131
133
|
@ft.cached_property
|
|
132
|
-
def
|
|
133
|
-
|
|
134
|
+
def _user_cls_and_provider_functions(self) -> tuple[type[BaseUser], list[ProviderFunctionType]]:
|
|
135
|
+
user_module_path = u.Path(self._filepath, c.PYCONFIGS_FOLDER, c.USER_FILE)
|
|
136
|
+
user_module = PyModule(user_module_path)
|
|
137
|
+
|
|
138
|
+
User = user_module.get_func_or_class("User", default_attr=BaseUser) # adds to Authenticator.providers as side effect
|
|
139
|
+
provider_functions = Authenticator.providers
|
|
140
|
+
Authenticator.providers = []
|
|
141
|
+
|
|
142
|
+
if not issubclass(User, BaseUser):
|
|
143
|
+
raise ConfigurationError(f"User class in '{c.USER_FILE}' must inherit from BaseUser")
|
|
144
|
+
|
|
145
|
+
return User, provider_functions
|
|
146
|
+
|
|
147
|
+
@ft.cached_property
|
|
148
|
+
def _auth_args(self) -> AuthProviderArgs:
|
|
149
|
+
conn_args = self._conn_args
|
|
150
|
+
return AuthProviderArgs(conn_args.project_path, conn_args.proj_vars, conn_args.env_vars)
|
|
134
151
|
|
|
135
152
|
@ft.cached_property
|
|
136
|
-
def
|
|
137
|
-
|
|
153
|
+
def _auth(self) -> Authenticator[BaseUser]:
|
|
154
|
+
User, provider_functions = self._user_cls_and_provider_functions
|
|
155
|
+
return Authenticator(self._logger, self._filepath, self._auth_args, provider_functions, user_cls=User)
|
|
138
156
|
|
|
139
157
|
@ft.cached_property
|
|
140
158
|
def _param_args(self) -> ps.ParametersArgs:
|
|
141
|
-
|
|
159
|
+
conn_args = self._conn_args
|
|
160
|
+
return ps.ParametersArgs(conn_args.project_path, conn_args.proj_vars, conn_args.env_vars)
|
|
142
161
|
|
|
143
162
|
@ft.cached_property
|
|
144
163
|
def _param_cfg_set(self) -> ps.ParameterConfigsSet:
|
|
@@ -261,7 +280,7 @@ class SquirrelsProject:
|
|
|
261
280
|
for model_name in dependencies:
|
|
262
281
|
model = models_dict[model_name]
|
|
263
282
|
if isinstance(model, m.SourceModel) and not model.model_config.load_to_duckdb:
|
|
264
|
-
raise InvalidInputError(
|
|
283
|
+
raise InvalidInputError(400, "Unqueryable source model", f"Source model '{model_name}' cannot be queried with DuckDB")
|
|
265
284
|
if isinstance(model, (m.SourceModel, m.BuildModel)):
|
|
266
285
|
substitutions[model_name] = f"venv.{model_name}"
|
|
267
286
|
|
|
@@ -310,18 +329,18 @@ class SquirrelsProject:
|
|
|
310
329
|
await dag.execute(self._param_args, self._param_cfg_set, self._context_func, user, selections, runquery=False, default_traits=default_traits)
|
|
311
330
|
return dag
|
|
312
331
|
|
|
313
|
-
def _get_all_connections(self) -> list[
|
|
332
|
+
def _get_all_connections(self) -> list[rm.ConnectionItemModel]:
|
|
314
333
|
connections = []
|
|
315
334
|
for conn_name, conn_props in self._conn_set.get_connections_as_dict().items():
|
|
316
335
|
if isinstance(conn_props, mf.ConnectionProperties):
|
|
317
336
|
label = conn_props.label if conn_props.label is not None else conn_name
|
|
318
|
-
connections.append(
|
|
337
|
+
connections.append(rm.ConnectionItemModel(name=conn_name, label=label))
|
|
319
338
|
return connections
|
|
320
339
|
|
|
321
|
-
def _get_all_data_models(self, compiled_dag: m.DAG) -> list[
|
|
340
|
+
def _get_all_data_models(self, compiled_dag: m.DAG) -> list[rm.DataModelItem]:
|
|
322
341
|
return compiled_dag.get_all_data_models()
|
|
323
342
|
|
|
324
|
-
async def get_all_data_models(self) -> list[
|
|
343
|
+
async def get_all_data_models(self) -> list[rm.DataModelItem]:
|
|
325
344
|
"""
|
|
326
345
|
Get all data models in the project
|
|
327
346
|
|
|
@@ -331,26 +350,26 @@ class SquirrelsProject:
|
|
|
331
350
|
compiled_dag = await self._get_compiled_dag()
|
|
332
351
|
return self._get_all_data_models(compiled_dag)
|
|
333
352
|
|
|
334
|
-
def _get_all_data_lineage(self, compiled_dag: m.DAG) -> list[
|
|
353
|
+
def _get_all_data_lineage(self, compiled_dag: m.DAG) -> list[rm.LineageRelation]:
|
|
335
354
|
all_lineage = compiled_dag.get_all_model_lineage()
|
|
336
355
|
|
|
337
356
|
# Add dataset nodes to the lineage
|
|
338
357
|
for dataset in self._manifest_cfg.datasets.values():
|
|
339
|
-
target_dataset =
|
|
340
|
-
source_model =
|
|
341
|
-
all_lineage.append(
|
|
358
|
+
target_dataset = rm.LineageNode(name=dataset.name, type="dataset")
|
|
359
|
+
source_model = rm.LineageNode(name=dataset.model, type="model")
|
|
360
|
+
all_lineage.append(rm.LineageRelation(type="runtime", source=source_model, target=target_dataset))
|
|
342
361
|
|
|
343
362
|
# Add dashboard nodes to the lineage
|
|
344
363
|
for dashboard in self._dashboards.values():
|
|
345
|
-
target_dashboard =
|
|
364
|
+
target_dashboard = rm.LineageNode(name=dashboard.dashboard_name, type="dashboard")
|
|
346
365
|
datasets = set(x.dataset for x in dashboard.config.depends_on)
|
|
347
366
|
for dataset in datasets:
|
|
348
|
-
source_dataset =
|
|
349
|
-
all_lineage.append(
|
|
367
|
+
source_dataset = rm.LineageNode(name=dataset, type="dataset")
|
|
368
|
+
all_lineage.append(rm.LineageRelation(type="runtime", source=source_dataset, target=target_dashboard))
|
|
350
369
|
|
|
351
370
|
return all_lineage
|
|
352
371
|
|
|
353
|
-
async def get_all_data_lineage(self) -> list[
|
|
372
|
+
async def get_all_data_lineage(self) -> list[rm.LineageRelation]:
|
|
354
373
|
"""
|
|
355
374
|
Get all data lineage in the project
|
|
356
375
|
|
|
@@ -480,7 +499,7 @@ class SquirrelsProject:
|
|
|
480
499
|
|
|
481
500
|
def _permission_error(self, user: BaseUser | None, data_type: str, data_name: str, scope: str) -> InvalidInputError:
|
|
482
501
|
username = "" if user is None else f" '{user.username}'"
|
|
483
|
-
return InvalidInputError(
|
|
502
|
+
return InvalidInputError(403, f"Unauthorized access to {data_type}", f"User{username} does not have permission to access {scope} {data_type}: {data_name}")
|
|
484
503
|
|
|
485
504
|
def seed(self, name: str) -> pl.LazyFrame:
|
|
486
505
|
"""
|
|
@@ -545,7 +564,7 @@ class SquirrelsProject:
|
|
|
545
564
|
)
|
|
546
565
|
|
|
547
566
|
async def dashboard(
|
|
548
|
-
self, name: str, *, selections: dict[str, t.Any] = {}, user: BaseUser | None = None, dashboard_type: t.Type[T] =
|
|
567
|
+
self, name: str, *, selections: dict[str, t.Any] = {}, user: BaseUser | None = None, dashboard_type: t.Type[T] = d.PngDashboard
|
|
549
568
|
) -> T:
|
|
550
569
|
"""
|
|
551
570
|
Async method to retrieve a dashboard given parameter selections.
|
squirrels/_py_module.py
CHANGED
|
@@ -43,11 +43,12 @@ class PyModule:
|
|
|
43
43
|
return func_or_class
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
def run_pyconfig_main(base_path: str, filename: str, kwargs: dict[str, Any] = {}) -> None:
|
|
46
|
+
def run_pyconfig_main(base_path: str, filename: str, kwargs: dict[str, Any] = {}) -> Any | None:
|
|
47
47
|
"""
|
|
48
48
|
Given a python file in the 'pyconfigs' folder, run its main function
|
|
49
49
|
|
|
50
50
|
Arguments:
|
|
51
|
+
base_path: The base path of the project
|
|
51
52
|
filename: The name of the file to run main function
|
|
52
53
|
kwargs: Dictionary of the main function arguments
|
|
53
54
|
"""
|
|
@@ -56,6 +57,6 @@ def run_pyconfig_main(base_path: str, filename: str, kwargs: dict[str, Any] = {}
|
|
|
56
57
|
main_function = module.get_func_or_class(c.MAIN_FUNC, is_required=False)
|
|
57
58
|
if main_function:
|
|
58
59
|
try:
|
|
59
|
-
main_function(**kwargs)
|
|
60
|
+
return main_function(**kwargs)
|
|
60
61
|
except Exception as e:
|
|
61
62
|
raise FileExecutionError(f'Failed to run python file "{filepath}"', e) from e
|
|
File without changes
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from typing import Callable, Any
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from pydantic import BaseModel, ConfigDict, Field, field_serializer
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BaseUser(BaseModel):
|
|
7
|
+
model_config = ConfigDict(from_attributes=True)
|
|
8
|
+
username: str
|
|
9
|
+
is_admin: bool = False
|
|
10
|
+
|
|
11
|
+
@classmethod
|
|
12
|
+
def dropped_columns(cls):
|
|
13
|
+
return []
|
|
14
|
+
|
|
15
|
+
def __hash__(self):
|
|
16
|
+
return hash(self.username)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ApiKey(BaseModel):
|
|
20
|
+
model_config = ConfigDict(from_attributes=True)
|
|
21
|
+
id: str
|
|
22
|
+
title: str
|
|
23
|
+
username: str
|
|
24
|
+
created_at: datetime
|
|
25
|
+
expires_at: datetime
|
|
26
|
+
|
|
27
|
+
@field_serializer('created_at', 'expires_at')
|
|
28
|
+
def serialize_datetime(self, dt: datetime) -> str:
|
|
29
|
+
return dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class UserField(BaseModel):
|
|
33
|
+
name: str
|
|
34
|
+
type: str
|
|
35
|
+
nullable: bool
|
|
36
|
+
enum: list[str] | None
|
|
37
|
+
default: Any | None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ProviderConfigs(BaseModel):
|
|
41
|
+
client_id: str
|
|
42
|
+
client_secret: str
|
|
43
|
+
server_metadata_url: str
|
|
44
|
+
client_kwargs: dict = Field(default_factory=dict)
|
|
45
|
+
get_user: Callable[[dict], BaseUser]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AuthProvider(BaseModel):
|
|
49
|
+
name: str
|
|
50
|
+
label: str
|
|
51
|
+
icon: str
|
|
52
|
+
provider_configs: ProviderConfigs
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# OAuth 2.1 Models
|
|
56
|
+
|
|
57
|
+
class OAuthClientModel(BaseModel):
|
|
58
|
+
"""OAuth client details"""
|
|
59
|
+
model_config = ConfigDict(from_attributes=True)
|
|
60
|
+
client_id: str
|
|
61
|
+
client_name: str
|
|
62
|
+
redirect_uris: list[str]
|
|
63
|
+
scope: str
|
|
64
|
+
grant_types: list[str]
|
|
65
|
+
response_types: list[str]
|
|
66
|
+
created_at: datetime
|
|
67
|
+
is_active: bool
|
|
68
|
+
|
|
69
|
+
@field_serializer('created_at')
|
|
70
|
+
def serialize_datetime(self, dt: datetime) -> str:
|
|
71
|
+
return dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ClientRegistrationRequest(BaseModel):
|
|
75
|
+
"""Request model for OAuth client registration"""
|
|
76
|
+
client_name: str = Field(description="Human-readable name for the OAuth client")
|
|
77
|
+
redirect_uris: list[str] = Field(description="List of allowed redirect URIs for the client")
|
|
78
|
+
scope: str = Field(default="read", description="Default scope for the client")
|
|
79
|
+
grant_types: list[str] = Field(default=["authorization_code", "refresh_token"], description="Allowed grant types")
|
|
80
|
+
response_types: list[str] = Field(default=["code"], description="Allowed response types")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class ClientUpdateRequest(BaseModel):
|
|
84
|
+
"""Request model for OAuth client update"""
|
|
85
|
+
client_name: str | None = Field(default=None, description="Human-readable name for the OAuth client")
|
|
86
|
+
redirect_uris: list[str] | None = Field(default=None, description="List of allowed redirect URIs for the client")
|
|
87
|
+
scope: str | None = Field(default=None, description="Default scope for the client")
|
|
88
|
+
grant_types: list[str] | None = Field(default=None, description="Allowed grant types")
|
|
89
|
+
response_types: list[str] | None = Field(default=None, description="Allowed response types")
|
|
90
|
+
is_active: bool | None = Field(default=None, description="Whether the client is active")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ClientDetailsResponse(BaseModel):
|
|
94
|
+
"""Response model for OAuth client details (without client_secret)"""
|
|
95
|
+
client_id: str = Field(description="Client ID")
|
|
96
|
+
client_name: str = Field(description="Client name")
|
|
97
|
+
redirect_uris: list[str] = Field(description="Registered redirect URIs")
|
|
98
|
+
scope: str = Field(description="Default scope")
|
|
99
|
+
grant_types: list[str] = Field(description="Allowed grant types")
|
|
100
|
+
response_types: list[str] = Field(description="Allowed response types")
|
|
101
|
+
created_at: datetime = Field(description="Registration timestamp")
|
|
102
|
+
is_active: bool = Field(description="Whether the client is active")
|
|
103
|
+
|
|
104
|
+
@field_serializer('created_at')
|
|
105
|
+
def serialize_datetime(self, dt: datetime) -> str:
|
|
106
|
+
return dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class ClientUpdateResponse(ClientDetailsResponse):
|
|
110
|
+
"""Response model for OAuth client update"""
|
|
111
|
+
registration_access_token: str | None = Field(default=None, description="Token for managing this client registration (store securely)")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class ClientRegistrationResponse(ClientUpdateResponse):
|
|
115
|
+
"""Response model for OAuth client registration"""
|
|
116
|
+
client_secret: str = Field(description="Generated client secret (store securely)")
|
|
117
|
+
registration_client_uri: str | None = Field(default=None, description="URI for managing this client registration")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TokenResponse(BaseModel):
|
|
121
|
+
access_token: str
|
|
122
|
+
token_type: str = "bearer"
|
|
123
|
+
expires_in: int
|
|
124
|
+
refresh_token: str | None = None
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class OAuthServerMetadata(BaseModel):
|
|
128
|
+
"""OAuth 2.1 Authorization Server Metadata (RFC 8414)"""
|
|
129
|
+
issuer: str = Field(description="Authorization server's issuer identifier URL")
|
|
130
|
+
authorization_endpoint: str = Field(description="URL of the authorization endpoint")
|
|
131
|
+
token_endpoint: str = Field(description="URL of the token endpoint")
|
|
132
|
+
revocation_endpoint: str = Field(description="URL of the token revocation endpoint")
|
|
133
|
+
registration_endpoint: str = Field(description="URL of the client registration endpoint")
|
|
134
|
+
scopes_supported: list[str] = Field(description="List of OAuth 2.1 scope values supported")
|
|
135
|
+
response_types_supported: list[str] = Field(description="List of OAuth 2.1 response_type values supported")
|
|
136
|
+
grant_types_supported: list[str] = Field(description="List of OAuth 2.1 grant type values supported")
|
|
137
|
+
token_endpoint_auth_methods_supported: list[str] = Field(
|
|
138
|
+
default=["client_secret_basic", "client_secret_post"],
|
|
139
|
+
description="List of client authentication methods supported by the token endpoint"
|
|
140
|
+
)
|
|
141
|
+
code_challenge_methods_supported: list[str] = Field(
|
|
142
|
+
default=["S256"],
|
|
143
|
+
description="List of PKCE code challenge methods supported"
|
|
144
|
+
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Query model generation utilities for API routes
|
|
3
|
+
"""
|
|
4
|
+
from typing import Annotated
|
|
5
|
+
from dataclasses import make_dataclass
|
|
6
|
+
from fastapi import Depends
|
|
7
|
+
from pydantic import create_model
|
|
8
|
+
|
|
9
|
+
from .._parameter_configs import APIParamFieldInfo
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_query_models_helper(widget_parameters: list[str] | None, predefined_params: list[APIParamFieldInfo], param_fields: dict):
|
|
13
|
+
"""Helper function to generate query models"""
|
|
14
|
+
if widget_parameters is None:
|
|
15
|
+
widget_parameters = list(param_fields.keys())
|
|
16
|
+
|
|
17
|
+
QueryModelForGetRaw = make_dataclass("QueryParams", [
|
|
18
|
+
param_fields[param].as_query_info() for param in widget_parameters
|
|
19
|
+
] + [param.as_query_info() for param in predefined_params])
|
|
20
|
+
QueryModelForGet = Annotated[QueryModelForGetRaw, Depends()]
|
|
21
|
+
|
|
22
|
+
field_definitions = {param: param_fields[param].as_body_info() for param in widget_parameters}
|
|
23
|
+
for param in predefined_params:
|
|
24
|
+
field_definitions[param.name] = param.as_body_info()
|
|
25
|
+
QueryModelForPost = create_model("RequestBodyParams", **field_definitions) # type: ignore
|
|
26
|
+
return QueryModelForGet, QueryModelForPost
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_query_models_for_parameters(widget_parameters: list[str] | None, param_fields: dict):
|
|
30
|
+
"""Generate query models for parameter endpoints"""
|
|
31
|
+
predefined_params = [
|
|
32
|
+
APIParamFieldInfo("x_verify_params", bool, default=False, description="If true, the query parameters are verified to be valid for the dataset"),
|
|
33
|
+
APIParamFieldInfo("x_parent_param", str, description="The parameter name used for parameter updates. If not provided, then all parameters are retrieved"),
|
|
34
|
+
]
|
|
35
|
+
return _get_query_models_helper(widget_parameters, predefined_params, param_fields)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_query_models_for_dataset(widget_parameters: list[str] | None, param_fields: dict):
|
|
39
|
+
"""Generate query models for dataset endpoints"""
|
|
40
|
+
predefined_params = [
|
|
41
|
+
APIParamFieldInfo("x_verify_params", bool, default=False, description="If true, the query parameters are verified to be valid for the dataset"),
|
|
42
|
+
APIParamFieldInfo("x_orientation", str, default="records", description="The orientation of the data to return, one of: 'records', 'rows', or 'columns'"),
|
|
43
|
+
APIParamFieldInfo("x_select", list[str], examples=[[]], description="The columns to select from the dataset. All are returned if not specified"),
|
|
44
|
+
APIParamFieldInfo("x_offset", int, default=0, description="The number of rows to skip before returning data (applied after data caching)"),
|
|
45
|
+
APIParamFieldInfo("x_limit", int, default=1000, description="The maximum number of rows to return (applied after data caching and offset)"),
|
|
46
|
+
]
|
|
47
|
+
return _get_query_models_helper(widget_parameters, predefined_params, param_fields)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_query_models_for_dashboard(widget_parameters: list[str] | None, param_fields: dict):
|
|
51
|
+
"""Generate query models for dashboard endpoints"""
|
|
52
|
+
predefined_params = [
|
|
53
|
+
APIParamFieldInfo("x_verify_params", bool, default=False, description="If true, the query parameters are verified to be valid for the dashboard"),
|
|
54
|
+
]
|
|
55
|
+
return _get_query_models_helper(widget_parameters, predefined_params, param_fields)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_query_models_for_querying_models(param_fields: dict):
|
|
59
|
+
"""Generate query models for querying data models"""
|
|
60
|
+
predefined_params = [
|
|
61
|
+
APIParamFieldInfo("x_verify_params", bool, default=False, description="If true, the query parameters are verified to be valid"),
|
|
62
|
+
APIParamFieldInfo("x_orientation", str, default="records", description="The orientation of the data to return, one of: 'records', 'rows', or 'columns'"),
|
|
63
|
+
APIParamFieldInfo("x_offset", int, default=0, description="The number of rows to skip before returning data (applied after data caching)"),
|
|
64
|
+
APIParamFieldInfo("x_limit", int, default=1000, description="The maximum number of rows to return (applied after data caching and offset)"),
|
|
65
|
+
APIParamFieldInfo("x_sql_query", str, description="The SQL query to execute on the data models"),
|
|
66
|
+
]
|
|
67
|
+
return _get_query_models_helper(None, predefined_params, param_fields)
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
from typing import Annotated, Literal
|
|
2
2
|
from pydantic import BaseModel, Field
|
|
3
|
-
from datetime import
|
|
3
|
+
from datetime import date
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from .. import _model_configs as mc, _sources as s
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
## Simple Auth Response Models
|
|
9
|
+
|
|
10
|
+
class ApiKeyResponse(BaseModel):
|
|
11
|
+
api_key: Annotated[str, Field(examples=["sqrl-12345678"], description="The API key to use subsequent API requests")]
|
|
12
|
+
|
|
13
|
+
class ProviderResponse(BaseModel):
|
|
14
|
+
name: Annotated[str, Field(examples=["my_provider"], description="The name of the provider")]
|
|
15
|
+
label: Annotated[str, Field(examples=["My Provider"], description="The human-friendly display name for the provider")]
|
|
16
|
+
icon: Annotated[str, Field(examples=["https://example.com/my_provider_icon.png"], description="The URL of the provider's icon")]
|
|
17
|
+
login_url: Annotated[str, Field(examples=["https://example.com/my_provider_login"], description="The URL to redirect to for provider login")]
|
|
14
18
|
|
|
15
19
|
|
|
16
20
|
## Parameters Response Models
|
squirrels/_utils.py
CHANGED
|
@@ -2,10 +2,9 @@ from typing import Sequence, Optional, Union, TypeVar, Callable, Any, Iterable
|
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from functools import lru_cache
|
|
5
|
-
from pydantic import BaseModel
|
|
6
5
|
import os, time, logging, json, duckdb, polars as pl, yaml
|
|
7
6
|
import jinja2 as j2, jinja2.nodes as j2_nodes
|
|
8
|
-
import sqlglot, sqlglot.expressions, asyncio
|
|
7
|
+
import sqlglot, sqlglot.expressions, asyncio, hashlib, inspect, base64
|
|
9
8
|
|
|
10
9
|
from . import _constants as c
|
|
11
10
|
from ._exceptions import ConfigurationError
|
|
@@ -359,3 +358,36 @@ async def asyncio_gather(coroutines: list):
|
|
|
359
358
|
# Wait for tasks to be cancelled
|
|
360
359
|
await asyncio.gather(*tasks, return_exceptions=True)
|
|
361
360
|
raise
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def hash_string(input_str: str, salt: str) -> str:
|
|
364
|
+
"""
|
|
365
|
+
Hashes a string using SHA-256
|
|
366
|
+
"""
|
|
367
|
+
return hashlib.sha256((input_str + salt).encode()).hexdigest()
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
T = TypeVar('T')
|
|
371
|
+
def call_func(func: Callable[..., T], **kwargs) -> T:
|
|
372
|
+
"""
|
|
373
|
+
Calls a function with the given arguments if func expects arguments, otherwise calls func without arguments
|
|
374
|
+
"""
|
|
375
|
+
sig = inspect.signature(func)
|
|
376
|
+
# Filter kwargs to only include parameters that the function accepts
|
|
377
|
+
filtered_kwargs = {k: v for k, v in kwargs.items() if k in sig.parameters}
|
|
378
|
+
return func(**filtered_kwargs)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def generate_pkce_challenge(code_verifier: str) -> str:
|
|
382
|
+
"""Generate PKCE code challenge from code verifier"""
|
|
383
|
+
# Generate SHA256 hash of code_verifier
|
|
384
|
+
verifier_hash = hashlib.sha256(code_verifier.encode('utf-8')).digest()
|
|
385
|
+
# Base64 URL encode (without padding)
|
|
386
|
+
expected_challenge = base64.urlsafe_b64encode(verifier_hash).decode('utf-8').rstrip('=')
|
|
387
|
+
return expected_challenge
|
|
388
|
+
|
|
389
|
+
def validate_pkce_challenge(code_verifier: str, code_challenge: str) -> bool:
|
|
390
|
+
"""Validate PKCE code verifier against code challenge"""
|
|
391
|
+
# Generate expected challenge
|
|
392
|
+
expected_challenge = generate_pkce_challenge(code_verifier)
|
|
393
|
+
return expected_challenge == code_challenge
|
squirrels/arguments.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
from ._arguments.
|
|
2
|
-
from ._arguments.
|
|
1
|
+
from ._arguments.init_time_args import ConnectionsArgs, AuthProviderArgs, ParametersArgs, BuildModelArgs
|
|
2
|
+
from ._arguments.run_time_args import ContextArgs, ModelArgs, DashboardArgs
|
squirrels/auth.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from ._auth import BaseUser, ProviderConfigs, provider
|
squirrels/dashboards.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
from .
|
|
1
|
+
from ._dashboards import PngDashboard, HtmlDashboard
|
squirrels/types.py
CHANGED
|
@@ -4,8 +4,8 @@ from ._parameter_options import ParameterOption
|
|
|
4
4
|
|
|
5
5
|
from ._parameters import Parameter, TextValue
|
|
6
6
|
|
|
7
|
-
from ._auth import BaseUser
|
|
8
|
-
|
|
9
7
|
from ._dataset_types import DatasetMetadata, DatasetResult
|
|
10
8
|
|
|
11
|
-
from .
|
|
9
|
+
from ._dashboards import Dashboard
|
|
10
|
+
|
|
11
|
+
from ._parameter_configs import ParameterConfigBase
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: squirrels
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.0b4
|
|
4
4
|
Summary: Squirrels - API Framework for Data Analytics
|
|
5
5
|
Project-URL: Homepage, https://squirrels-analytics.github.io
|
|
6
6
|
Project-URL: Repository, https://github.com/squirrels-analytics/squirrels
|
|
@@ -13,15 +13,18 @@ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
|
13
13
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
14
14
|
Classifier: Typing :: Typed
|
|
15
15
|
Requires-Python: ~=3.10
|
|
16
|
+
Requires-Dist: authlib<2,>=1.5.2
|
|
16
17
|
Requires-Dist: bcrypt<5,>=4.0.1
|
|
17
18
|
Requires-Dist: cachetools<6,>=5.3.2
|
|
18
19
|
Requires-Dist: duckdb<2,>=1.1.3
|
|
19
20
|
Requires-Dist: fastapi<1,>=0.112.1
|
|
20
21
|
Requires-Dist: gitpython<4,>=3.1.41
|
|
21
22
|
Requires-Dist: inquirer<4,>=3.2.1
|
|
23
|
+
Requires-Dist: itsdangerous<3,>=2.2.0
|
|
22
24
|
Requires-Dist: jinja2<4,>=3.1.3
|
|
23
25
|
Requires-Dist: libpass<2,>=1.9.0
|
|
24
26
|
Requires-Dist: matplotlib<4,>=3.8.3
|
|
27
|
+
Requires-Dist: mcp>=1.9.2
|
|
25
28
|
Requires-Dist: networkx<4,>=3.2.1
|
|
26
29
|
Requires-Dist: pandas<3,>=2.1.4
|
|
27
30
|
Requires-Dist: polars<2,>=1.14.0
|
|
@@ -2,46 +2,54 @@ dateutils/__init__.py,sha256=dq4VSlJ5ztaDPdvYBRAvXSyanT_CZif3I4O0YVmWfa8,277
|
|
|
2
2
|
dateutils/_enums.py,sha256=WBrnLqta_iMMhGMEn24cCO1Vlr7bST0E8oEfAL3z0P8,373
|
|
3
3
|
dateutils/_implementation.py,sha256=PVJAdNolDdTpCZXwvokKoMIZzTSNpCUvZlVLWpMeSho,15173
|
|
4
4
|
dateutils/types.py,sha256=xcRwBoftQi-uHM1tlVTW5xgN84qdrqCo5tQT7A34S8Y,124
|
|
5
|
-
squirrels/__init__.py,sha256=
|
|
6
|
-
squirrels/
|
|
7
|
-
squirrels/
|
|
8
|
-
squirrels/_auth.py,sha256=u3ISed2yavktOseN9rf5uXcPupfS4pK6rlC60RvyrgI,19305
|
|
5
|
+
squirrels/__init__.py,sha256=O9QBS0WHdmWM7SUCTnEk1VU8ElnJKKrFHGAEd8W5CkM,288
|
|
6
|
+
squirrels/_api_server.py,sha256=y-GW7e7vKgeZsL50Md9dwsc7WR5Oe3Mu-yAZOK96_wE,14071
|
|
7
|
+
squirrels/_auth.py,sha256=3VWbB4-mMEqUp3YL1-T6R-kOswZ4xx29H0SSSlp2ezM,45592
|
|
9
8
|
squirrels/_command_line.py,sha256=ZB7cTJabWiYEV6tFi1l87M-w468LJ-DV7TNgQ_bqFbY,11151
|
|
10
|
-
squirrels/_connection_set.py,sha256
|
|
11
|
-
squirrels/_constants.py,sha256=
|
|
12
|
-
squirrels/
|
|
13
|
-
squirrels/_dashboards_io.py,sha256=bWgXdqTNV57x3zlptmFnG_RX5s1zO-TV2CvrQpcCTs8,3001
|
|
9
|
+
squirrels/_connection_set.py,sha256=-krmn3RJkPOmh6oP3t8PhGhInyQ2pLu5HulYDi-aUrI,4008
|
|
10
|
+
squirrels/_constants.py,sha256=6sBvx5ijWgLz4KJGZo-VZ8NPZvw1aVROsYxA6ac2pZ8,3337
|
|
11
|
+
squirrels/_dashboards.py,sha256=FF6KG-GXohR5Hp-BMYRwEZ2hUQ_jbkFpcK5epwoD64w,4891
|
|
14
12
|
squirrels/_data_sources.py,sha256=W1aUAHkkjPpBRjZGBrc9RJIxEwICfnzB8HAyknCoZ3E,25988
|
|
15
13
|
squirrels/_dataset_types.py,sha256=wZvvRs4U1Hva_iyoFosAAu76S1yfv1Cd5SX3UMIw2PA,2838
|
|
16
|
-
squirrels/_exceptions.py,sha256
|
|
14
|
+
squirrels/_exceptions.py,sha256=-hfDZoV-JhvuNRAERTFg-5NFjr0uNk4i9ogNgJfAyyc,1171
|
|
17
15
|
squirrels/_initializer.py,sha256=bJ0AeFIHYIYVT6o_kw5oWQJ9Qm4-n1RVNk3iYi2qbH0,14270
|
|
18
16
|
squirrels/_manifest.py,sha256=ag902b0SNbra6XrBzkaKizmJGwRgFKPjEjtnkJgfFio,10232
|
|
19
|
-
squirrels/_model_builder.py,sha256=
|
|
17
|
+
squirrels/_model_builder.py,sha256=5aLL_DxOv_wgJc3ml26q3hZSpmzkbniBC34Dh-f2ML4,5096
|
|
20
18
|
squirrels/_model_configs.py,sha256=eJne5L-QLv42s7uodUOOASMRn1NbzGbiBmwMllkO5C4,3303
|
|
21
|
-
squirrels/_model_queries.py,sha256=
|
|
22
|
-
squirrels/_models.py,sha256=
|
|
19
|
+
squirrels/_model_queries.py,sha256=2fl07feHtzddBpiXUWGfNBYeoN_EZ7K5mAz2Jc-vCvs,1087
|
|
20
|
+
squirrels/_models.py,sha256=_oudu_szkcBSAXYloxAd2LV2-gbTY6d8-E1kQuc89c0,50017
|
|
23
21
|
squirrels/_package_loader.py,sha256=xcIur5Z38OWd-OVjsueFstVV567zlkK9UBnLS4NawJY,1158
|
|
24
|
-
squirrels/_parameter_configs.py,sha256=
|
|
22
|
+
squirrels/_parameter_configs.py,sha256=wxF51hMOewtf5SZgo6dEHa3vzU81vXN4LNmIK0UK9po,23981
|
|
25
23
|
squirrels/_parameter_options.py,sha256=cWYKNoBUopHq6VfaeBu-nN2V0_IY3OgYpmYhKODNCew,16956
|
|
26
|
-
squirrels/_parameter_sets.py,sha256=
|
|
27
|
-
squirrels/_parameters.py,sha256=
|
|
28
|
-
squirrels/_project.py,sha256=
|
|
29
|
-
squirrels/_py_module.py,sha256=
|
|
24
|
+
squirrels/_parameter_sets.py,sha256=xTiwDBT3LONILHlitA12J2b9CS4NU6-S1IQSILT2v8A,10315
|
|
25
|
+
squirrels/_parameters.py,sha256=Ta929_a-b8r1miDKXT_SZtHT_d8jJ2W5ABvkXL7k074,76372
|
|
26
|
+
squirrels/_project.py,sha256=y6r_ppujlSbxboS52zSIKfyzRo1kC1eQ9nkdYcHJc30,30391
|
|
27
|
+
squirrels/_py_module.py,sha256=NW86sv1oYWh8oMqgGh2oIUwuFcLXoq3cwEuKRxPfEwY,2683
|
|
30
28
|
squirrels/_seeds.py,sha256=yyIYp4bn9Sg6lhgvsOYIJQHIpPbvLNsyGHVfswEyVd8,2225
|
|
31
29
|
squirrels/_sources.py,sha256=j5mY_EtA5cxoHwtk8RwTVHO74hleik2lS7mF9gVnG_A,4840
|
|
32
|
-
squirrels/_utils.py,sha256=
|
|
30
|
+
squirrels/_utils.py,sha256=yZ7pyWUZxCkAtOZO00QAN9kRM9KNpIUwERnnaiXCpxg,13552
|
|
33
31
|
squirrels/_version.py,sha256=M8aFbJ4vlAi3Sk9b7leRuEfkNBjkkX5S_F9lA4h8GK4,105
|
|
34
|
-
squirrels/arguments.py,sha256=
|
|
32
|
+
squirrels/arguments.py,sha256=s8Ud0ahdgyMO9jO_qV9uYzJ3rCApAeRKpNKCz_EfQjI,181
|
|
33
|
+
squirrels/auth.py,sha256=GNalDnQo4I1ONSYUGWwAxhJmgEwLYy688Fby-ksvnHk,54
|
|
35
34
|
squirrels/connections.py,sha256=dpjR00DjLiPfF0iz9HfL9PyjWYtYQq67zk2LyT2bnhg,64
|
|
36
|
-
squirrels/dashboards.py,sha256=
|
|
35
|
+
squirrels/dashboards.py,sha256=UA-mksRXtL5dyzewoGTnR5bz26KzvrVgKsIpNIPkLe0,52
|
|
37
36
|
squirrels/data_sources.py,sha256=6a4E1m-Zcx-o2SERwKJiz8n6LFsI2CQ5MbmDwKhVq1o,170
|
|
38
37
|
squirrels/parameter_options.py,sha256=TmvoESS1BSPx-73bNzo6W0bSztj7-ye0_vssmii0cBk,205
|
|
39
38
|
squirrels/parameters.py,sha256=7Go5jetD3J7NDrkk2a_7ExxJv0mT4rwo1B9sAkuxPPg,195
|
|
40
|
-
squirrels/types.py,sha256=
|
|
41
|
-
squirrels/
|
|
42
|
-
squirrels/
|
|
43
|
-
squirrels/
|
|
44
|
-
squirrels/
|
|
39
|
+
squirrels/types.py,sha256=snP-ZLW_YY1TCCPVQTcDQMQvU6159yMRL-Hou9L7QJU,282
|
|
40
|
+
squirrels/_api_routes/__init__.py,sha256=-oGMDfM5Qo7NDiwpjHt8BKVZNNZ4798-NZCJzYCUdbc,106
|
|
41
|
+
squirrels/_api_routes/auth.py,sha256=BzThmdxugavFDW_RF2LNpPf8Z4fQcxrHL52TLPrsj54,13917
|
|
42
|
+
squirrels/_api_routes/base.py,sha256=7H4SzhBJ-yWtC70kjzZneW_ZkejVzwTSnMlunSBt8ig,7067
|
|
43
|
+
squirrels/_api_routes/dashboards.py,sha256=tasnHUmgkm-ZcpEviRoCiNtR9TgY6xjBZac7SkyEJBg,8097
|
|
44
|
+
squirrels/_api_routes/data_management.py,sha256=Gr6JVFpMaFhlQCUICGxuDNWrz4BGPydrMou0elBg5tU,5347
|
|
45
|
+
squirrels/_api_routes/datasets.py,sha256=Xcf-sTI8Noe06Tc-Nm5tJt4Y94ljl8ZXSxVrTOrN9gw,13592
|
|
46
|
+
squirrels/_api_routes/oauth2.py,sha256=1Q0mgbOw39iOGKy_Wwuuxi_Iw5rRCXhZPKc_CXYsBH4,14855
|
|
47
|
+
squirrels/_api_routes/project.py,sha256=gEtfDa1RyjxGEcUZTnNZSgQbLeIb5wUv2n65fZd6d40,11546
|
|
48
|
+
squirrels/_arguments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
|
+
squirrels/_arguments/init_time_args.py,sha256=Y5gVbrU_yd1r5JtAChcL1xTpfz0i8gwNtZMe5QBT2mc,3511
|
|
50
|
+
squirrels/_arguments/run_time_args.py,sha256=Izh7nxifrzEmMCY41vzz8TWBJ5cSAHuXkjPRZRx6mHM,5016
|
|
51
|
+
squirrels/_package_data/base_project/.env,sha256=CX13teP-pc3ymHIg9oERWcSGvxvMWRDOylLmI1Oiqpg,1105
|
|
52
|
+
squirrels/_package_data/base_project/.env.example,sha256=eN9fIwZF0CbK5mz0zJ4xsZ2I2MwY7Tj6f1ZsudfJKkA,1157
|
|
45
53
|
squirrels/_package_data/base_project/connections.yml,sha256=qZxh7OuI2xqf2cFKwpMo5TONrJXGVzQ7YfcWh4Go7Oo,1011
|
|
46
54
|
squirrels/_package_data/base_project/duckdb_init.sql,sha256=iwKDoHbKhOEMe-Pu_sX5a9OauCgqxfZLD70S7RduBrE,196
|
|
47
55
|
squirrels/_package_data/base_project/gitignore,sha256=B9OEkQ_j9fZGA2IAyVUvXeylxpya-AUwzLzqzMN4Bfc,155
|
|
@@ -66,15 +74,21 @@ squirrels/_package_data/base_project/models/federates/federate_example.sql,sha25
|
|
|
66
74
|
squirrels/_package_data/base_project/models/federates/federate_example.yml,sha256=FpSVZV7xNSH5iUAXMkyd_jynFxWryutz5Izlgsq3aP0,2279
|
|
67
75
|
squirrels/_package_data/base_project/pyconfigs/connections.py,sha256=IRL9D2HG9pdwp12S896b0Q-hKnFfVck8we1q5sifX10,601
|
|
68
76
|
squirrels/_package_data/base_project/pyconfigs/context.py,sha256=8zsQ0tf0EhfeRDPwK7ANNaJWAubT3AYVCzcdaOz0xwY,3670
|
|
69
|
-
squirrels/_package_data/base_project/pyconfigs/parameters.py,sha256=
|
|
70
|
-
squirrels/_package_data/base_project/pyconfigs/user.py,sha256=
|
|
77
|
+
squirrels/_package_data/base_project/pyconfigs/parameters.py,sha256=VEHGP8UsoV7KQQAQB-K_6ciTOLZwO6h7OgW2Q4QJk5I,4496
|
|
78
|
+
squirrels/_package_data/base_project/pyconfigs/user.py,sha256=5gRP_I5C2-z-gTaNt0F9yqk0yLzu6xrS0pdGvb8ZET4,2030
|
|
71
79
|
squirrels/_package_data/base_project/seeds/seed_categories.csv,sha256=jppjf1nOIxy7-bi5lJn5CVqmnLfJHHq0ABgp6UqbXnw,104
|
|
72
80
|
squirrels/_package_data/base_project/seeds/seed_categories.yml,sha256=NZ4BVvYYCEq6OnjRLrE_WOMhYsW0BQhRPWOgUchzdp4,435
|
|
73
81
|
squirrels/_package_data/base_project/seeds/seed_subcategories.csv,sha256=Tta1oIgnc2nukNMDlUkIErRKNH_8YT5EPp1A2kQKcow,327
|
|
74
82
|
squirrels/_package_data/base_project/seeds/seed_subcategories.yml,sha256=QTgw8Eld-p6Kntf53FyXyn7-7vKYI7IOJVu-Lr-FHCY,583
|
|
75
83
|
squirrels/_package_data/base_project/tmp/.gitignore,sha256=XImoqcWvJY0C0L_TWCx1ljvqU7qh9fUTJmK4ACCmNFI,13
|
|
76
|
-
squirrels
|
|
77
|
-
squirrels
|
|
78
|
-
squirrels
|
|
79
|
-
squirrels
|
|
80
|
-
squirrels
|
|
84
|
+
squirrels/_package_data/templates/dataset_results.html,sha256=Y-1xtw4ZzHcoW1vfWR1amnFJqFUMvwJDY3eeY7bfKJw,2793
|
|
85
|
+
squirrels/_package_data/templates/oauth_login.html,sha256=LmR7zbjFUYxWIhB9nhIkoGDmqUgEtSXCqo0pKrWqwXw,7108
|
|
86
|
+
squirrels/_schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
87
|
+
squirrels/_schemas/auth_models.py,sha256=3EpMtGip5iPfA4ixWBa5ipOy9LrQafC047nc1Mhjz3w,5625
|
|
88
|
+
squirrels/_schemas/query_param_models.py,sha256=zPfvr2kHEhGqbFDDs4oGsy8zYt-acwO0mfq1ELIpEI8,4094
|
|
89
|
+
squirrels/_schemas/response_models.py,sha256=lvz37EPPDkj6ZNb8ZprnCgpmFHrZA4Xg-04MVxBqfLg,13616
|
|
90
|
+
squirrels-0.5.0b4.dist-info/METADATA,sha256=QCKmfVXmjXUPl2qya_8KIyolw072cgjrWt61LYOXZYc,4496
|
|
91
|
+
squirrels-0.5.0b4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
92
|
+
squirrels-0.5.0b4.dist-info/entry_points.txt,sha256=i6vgjhJ3o_cdSFYofFcNY9DFMPr4MIcuwnkskSTXfJc,95
|
|
93
|
+
squirrels-0.5.0b4.dist-info/licenses/LICENSE,sha256=qqERuumQtQVsMrEXvJHuecJvV2sLxbleEubd_Zk8dY8,11338
|
|
94
|
+
squirrels-0.5.0b4.dist-info/RECORD,,
|