solara-enterprise 1.45.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.
@@ -0,0 +1,25 @@
1
+ from typing import ChainMap
2
+
3
+
4
+ class MultiLevel(ChainMap):
5
+ """Use multiple caches, where we assume the first is the fastest"""
6
+
7
+ def __getitem__(self, key):
8
+ for level, mapping in enumerate(self.maps):
9
+ try:
10
+ value = mapping[key]
11
+ # write back to lower levels
12
+ for i in range(level):
13
+ self.maps[i][key] = value
14
+ return value
15
+ except KeyError:
16
+ pass
17
+ return self.__missing__(key)
18
+
19
+ def __setitem__(self, key, value):
20
+ for cache in self.maps:
21
+ cache[key] = value
22
+
23
+ def __delitem__(self, key):
24
+ for cache in self.maps:
25
+ del cache[key]
@@ -0,0 +1,25 @@
1
+ from typing import Any, Callable, Optional
2
+
3
+ import redis
4
+ import solara.settings
5
+
6
+ from solara_enterprise.cache.base import Base, make_key
7
+
8
+
9
+ class Redis(Base):
10
+ """Wraps a client such that the values are pickled/unpickled"""
11
+
12
+ def __init__(
13
+ self, client: Optional[redis.Redis] = None, clear=solara.settings.cache.clear, prefix=b"solara:cache:", make_key: Callable[[Any], bytes] = make_key
14
+ ):
15
+ self.client = client or redis.Redis()
16
+ super().__init__(self.client, prefix=prefix, clear=clear, make_key=make_key)
17
+
18
+ def clear(self):
19
+ with self.client.lock(b"lock:" + self.prefix):
20
+ keys = self.keys()
21
+ for key in keys:
22
+ del self[key]
23
+
24
+ def keys(self):
25
+ return self.client.keys(self.prefix + b"*")
@@ -0,0 +1,17 @@
1
+ import sys
2
+ from typing import Dict
3
+
4
+ from rich import print
5
+
6
+ warned: Dict[str, bool] = {}
7
+
8
+
9
+ def check(name):
10
+ if name in warned:
11
+ return
12
+ warned[name] = True
13
+ print( # noqa: T201
14
+ f"[bold yellow]Using the enterprise {name} feature requires a license, unless used for non-commerical use. "
15
+ "Please contact us at contact@solara.dev to get a license.",
16
+ file=sys.stderr,
17
+ )
File without changes
@@ -0,0 +1,89 @@
1
+ import json
2
+ from pathlib import Path
3
+ from typing import Any, List
4
+
5
+ import solara
6
+ from rich import print as rprint
7
+ from solara.server import settings
8
+ from typing_extensions import TypedDict
9
+
10
+ from .. import license
11
+
12
+
13
+ class DocumentData(TypedDict):
14
+ id: str
15
+ text: str
16
+ location: str
17
+ title: str
18
+
19
+
20
+ def build_index(base_url: str):
21
+ license.check("search")
22
+ import solara.server.app
23
+ import solara.server.kernel
24
+
25
+ build_path = settings.ssg.build_path
26
+ assert build_path is not None
27
+ app_script = solara.server.app.apps["__default__"]
28
+ rprint(f"Building search index for {app_script.name} by using {build_path}")
29
+ routes = app_script.routes
30
+
31
+ documents: List[DocumentData] = []
32
+
33
+ for route in routes:
34
+ add_document(f"{base_url}/", route, build_path, documents)
35
+ index_file = app_script.directory.parent / "assets" / "search.json"
36
+ rprint("Writing search index to", index_file)
37
+ with open(index_file, "w") as f:
38
+ json.dump(documents, f, indent=4)
39
+
40
+
41
+ def add_document(base_url: str, route: solara.Route, build_path: Path, documents: List[DocumentData]):
42
+ url = base_url + (route.path if route.path != "/" else "")
43
+ if not route.children:
44
+ path = build_path / ("index.html" if route.path == "/" else route.path + ".html")
45
+ rprint("Processing", path)
46
+ from bs4 import BeautifulSoup # , Tag
47
+
48
+ if not path.exists():
49
+ rprint(f"Warning: {path} does not exist")
50
+ else:
51
+ soup = BeautifulSoup(path.read_text("utf8"), "html.parser")
52
+ node = soup.find(class_="solara-page-content-search")
53
+
54
+ if node is None:
55
+ rprint(f"Warning: {path} has no solara-page-content-search")
56
+ else:
57
+ # split by h1 and h2
58
+ parts: List[List[Any]] = [[]]
59
+ ids = []
60
+ titles = [soup.title.string if soup.title else ""]
61
+ current = parts[-1]
62
+ # remove invisible title elements
63
+ for el in node.find_all("span", attrs={"style": "display: none;"}):
64
+ el.string = ""
65
+ for el in node.descendants:
66
+ if el.name == "h1" or el.name == "h2":
67
+ ids.append(el.get("id"))
68
+ parts.append([])
69
+ titles.append(el.string)
70
+ current = parts[-1]
71
+ else:
72
+ current.append(el)
73
+ # join the next node
74
+ text = ""
75
+ for i, part in enumerate(parts):
76
+ for el in part:
77
+ if el.string:
78
+ text += el.string.strip() + " "
79
+ # texts.append("")
80
+ id = ids[i]
81
+ location = f"{url}#{id}" if id else url
82
+ text = text.strip()
83
+ title = titles[i].strip()
84
+ if title != text:
85
+ documents.append({"id": id, "text": text, "location": location, "title": title})
86
+ text = ""
87
+ # documents.append({"id": url, "html": str(node), "location": url})
88
+ for child in route.children:
89
+ add_document(url + "/", child, build_path / Path(route.path), documents)
@@ -0,0 +1,25 @@
1
+ import ipyvue
2
+ import solara
3
+ import traitlets
4
+
5
+
6
+ class SearchWidget(ipyvue.VueTemplate):
7
+ template_file = (__file__, "search.vue")
8
+ forceUpdateList = traitlets.Int(0).tag(sync=True)
9
+ item = traitlets.Any().tag(sync=True)
10
+ query = traitlets.Unicode("", allow_none=True).tag(sync=True)
11
+ search_open = traitlets.Bool(False).tag(sync=True)
12
+ failed = traitlets.Bool(False).tag(sync=True)
13
+ cdn = traitlets.Unicode(None, allow_none=True).tag(sync=True)
14
+
15
+ @traitlets.default("cdn")
16
+ def _cdn(self):
17
+ import solara.settings
18
+
19
+ if not solara.settings.assets.proxy:
20
+ return solara.settings.assets.cdn
21
+
22
+
23
+ @solara.component
24
+ def Search():
25
+ return SearchWidget.element()
@@ -0,0 +1,170 @@
1
+ <template>
2
+ <div>
3
+ <v-menu v-model="search_open" offset-y>
4
+ <template v-slot:activator="on">
5
+ <v-text-field prepend-icon="mdi-magnify" v-model="query" return-object append-icon="" no-filter
6
+ clearable @click="onClick" hide-details class="d-none d-md-flex">
7
+ </v-text-field>
8
+
9
+ </template>
10
+ <v-card class="solara-search-menu">
11
+ <div v-if="failed">
12
+ <v-alert type="error">
13
+ Failed to load search index, maybe the server is still indexing.
14
+ <v-btn text @click="fetchData">Retry</v-btn>
15
+ </v-alert>
16
+ </div>
17
+ <v-list class="pa-3" :key="forceUpdateList" v-if="!failed">
18
+ <v-list-item v-for="item in items.slice(0, 20)" v-if="items" @click="onClickItem(item)">
19
+ <v-list-item-content class="solara-search-list-item">
20
+ <v-list-item-title>{{ item.title }}</v-list-item-title>
21
+ <v-list-item-subtitle>{{ item.location }}</v-list-item-subtitle>
22
+ <v-list-item-subtitle>{{ item.text }}</v-list-item-subtitle>
23
+ </v-list-item-content>
24
+ </v-list-item>
25
+ <v-list-item v-if="items.length > 20">
26
+ <v-list-item-content>
27
+ And {{ items.length - 20 }} more pages.
28
+ </v-list-item-content>
29
+
30
+ </v-list-item>
31
+ <v-list-item v-if="items.length == 0">
32
+ No search results found.
33
+ </v-list-item>
34
+ </v-list>
35
+ </v-card>
36
+ </v-menu>
37
+ </div>
38
+ </template>
39
+ <script>
40
+ module.exports = {
41
+ created() {
42
+ this.items = [];
43
+ },
44
+ async mounted() {
45
+ window.search = this;
46
+ await this.loadRequire();
47
+ this.lunr = (await this.import([`${this.getCdn()}/lunr/lunr.js`]))[0];
48
+ this.fetchData();
49
+ window.search = this;
50
+
51
+ },
52
+ watch: {
53
+ query(value) {
54
+ this.search_open = true;
55
+ this.search();
56
+ },
57
+ item(value) {
58
+ solara.router.push(value.location);
59
+ this.$nextTick(() => {
60
+ // we cannot do this directly it seems
61
+ // this.query = "";
62
+ })
63
+ },
64
+ },
65
+ methods: {
66
+ async fetchData() {
67
+ const url = `${window.solara.rootPath}/static/assets/search.json`
68
+ let documents = [];
69
+ try {
70
+ documents = await (await fetch(url)).json();
71
+ this.failed = false;
72
+ } catch (e) {
73
+ this.failed = true;
74
+ return;
75
+ }
76
+
77
+ this.documents = {}
78
+ documents.forEach((document) => {
79
+ this.documents[document.location] = document;
80
+ })
81
+ this.idx = this.lunr(function () {
82
+ this.ref('location')
83
+ this.field('title')
84
+
85
+ this.field('text')
86
+ documents.forEach(function (doc) {
87
+ this.add(doc)
88
+ }, this)
89
+ })
90
+
91
+ },
92
+ onClickItem(item) {
93
+ // console.log(item)
94
+ this.item = item;
95
+ },
96
+ onClick() {
97
+ this.search_open = true;
98
+ },
99
+ search() {
100
+ if (this.idx) {
101
+ const searchResult = this.idx.search(this.query || "");
102
+ items = []
103
+ searchResult.forEach((item) => {
104
+ items.push(this.documents[item.ref])
105
+ })
106
+ this.items = items;
107
+ // items is not reactive, so use this proxy
108
+ this.forceUpdateList += 1
109
+ }
110
+ },
111
+ import(deps) {
112
+ return this.loadRequire().then(
113
+ () => {
114
+ if (window.jupyterVue) {
115
+ // in jupyterlab, we take Vue from ipyvue/jupyterVue
116
+ define("vue", [], () => window.jupyterVue.Vue);
117
+ } else {
118
+ define("vue", ['jupyter-vue'], jupyterVue => jupyterVue.Vue);
119
+ }
120
+ return new Promise((resolve, reject) => {
121
+ requirejs(deps, (...modules) => resolve(modules));
122
+ })
123
+ }
124
+ );
125
+ },
126
+ loadRequire() {
127
+ /* Needed in lab */
128
+ if (window.requirejs) {
129
+ console.log('require found');
130
+ return Promise.resolve()
131
+ }
132
+ return new Promise((resolve, reject) => {
133
+ const script = document.createElement('script');
134
+ script.src = `${this.getCdn()}/requirejs@2.3.6/require.js`;
135
+ script.onload = resolve;
136
+ script.onerror = reject;
137
+ document.head.appendChild(script);
138
+ });
139
+ },
140
+ getJupyterBaseUrl() {
141
+ // if base url is set, we use ./ for relative paths compared to the base url
142
+ if (document.getElementsByTagName("base").length) {
143
+ return document.baseURI;
144
+ }
145
+ const labConfigData = document.getElementById('jupyter-config-data');
146
+ if (labConfigData) {
147
+ /* lab and Voila */
148
+ return JSON.parse(labConfigData.textContent).baseUrl;
149
+ }
150
+ let base = document.body.dataset.baseUrl || document.baseURI;
151
+ if (!base.endsWith('/')) {
152
+ base += '/';
153
+ }
154
+ return base
155
+ },
156
+ getCdn() {
157
+ return this.cdn || (window.solara ? window.solara.cdn : `${this.getJupyterBaseUrl()}_solara/cdn`);
158
+ },
159
+ }
160
+ }
161
+ </script>
162
+ <style id="solara-search">
163
+ .solara-search-list-item {
164
+ max-width: 400px;
165
+ }
166
+
167
+ .solara-search-menu {
168
+ max-height: 80vh;
169
+ }
170
+ </style>
@@ -0,0 +1,249 @@
1
+ import concurrent.futures.thread
2
+ import logging
3
+ import threading
4
+ import time
5
+ import typing
6
+ import urllib
7
+ import weakref
8
+ from pathlib import Path
9
+ from typing import List, Optional, Tuple
10
+
11
+ import solara
12
+ from rich import print as rprint
13
+ from solara.server import settings
14
+ from typing_extensions import TypedDict
15
+
16
+ from . import license
17
+
18
+ logger = logging.getLogger("solara.server.ssg")
19
+
20
+ if typing.TYPE_CHECKING:
21
+ import playwright.sync_api
22
+ import playwright.sync_api._context_manager
23
+
24
+
25
+ class Playwright(threading.local):
26
+ browser: Optional["playwright.sync_api.Browser"] = None
27
+ sync_playwright: Optional["playwright.sync_api.Playwright"] = None
28
+ context_manager: Optional["playwright.sync_api._context_manager.PlaywrightContextManager"] = None
29
+ page: Optional["playwright.sync_api.Page"] = None
30
+
31
+
32
+ pw = Playwright()
33
+ _used: List[Tuple["playwright.sync_api.Browser", "playwright.sync_api._context_manager.PlaywrightContextManager"]] = []
34
+
35
+
36
+ class SSGData(TypedDict):
37
+ title: str
38
+ html: str
39
+ styles: List[str]
40
+ metas: List[str]
41
+
42
+
43
+ def _get_playwright():
44
+ if hasattr(pw, "browser") and pw.browser is not None:
45
+ return pw
46
+ from playwright.sync_api import sync_playwright
47
+
48
+ pw.number = 42
49
+ pw.context_manager = sync_playwright()
50
+ pw.sync_playwright = pw.context_manager.start()
51
+
52
+ pw.browser = pw.sync_playwright.chromium.launch(headless=not settings.ssg.headed)
53
+ pw.page = pw.browser.new_page()
54
+ _used.append((pw.browser, pw.context_manager))
55
+ return pw
56
+
57
+
58
+ def _worker_with_cleanup(*args, **kwargs):
59
+ try:
60
+ concurrent.futures.thread._worker(*args, **kwargs)
61
+ finally:
62
+ pw = _get_playwright()
63
+ pw.browser.close()
64
+ pw.context_manager.__exit__(None, None, None)
65
+
66
+
67
+ class CleanupThreadPoolExecutor(concurrent.futures.ThreadPoolExecutor):
68
+ def _adjust_thread_count(self):
69
+ # copy of the original code with _worker replaced
70
+ # if idle threads are available, don't spin new threads
71
+ if self._idle_semaphore.acquire(timeout=0):
72
+ return
73
+
74
+ # When the executor gets lost, the weakref callback will wake up
75
+ # the worker threads.
76
+ def weakref_cb(_, q=self._work_queue):
77
+ q.put(None)
78
+
79
+ num_threads = len(self._threads)
80
+ if num_threads < self._max_workers:
81
+ thread_name = "%s_%d" % (self._thread_name_prefix or self, num_threads)
82
+ t = threading.Thread(
83
+ name=thread_name, target=_worker_with_cleanup, args=(weakref.ref(self, weakref_cb), self._work_queue, self._initializer, self._initargs)
84
+ )
85
+ t.start()
86
+ self._threads.add(t) # type: ignore
87
+ concurrent.futures.thread._threads_queues[t] = self._work_queue # type: ignore
88
+
89
+
90
+ def ssg_crawl(base_url: str):
91
+ license.check("SSG")
92
+ import solara.server.app
93
+ import solara.server.kernel
94
+
95
+ build_path = settings.ssg.build_path
96
+ assert build_path is not None
97
+ build_path.mkdir(exist_ok=True)
98
+
99
+ app_script = solara.server.app.apps["__default__"]
100
+ rprint(f"Building {app_script.name} at {build_path}")
101
+ routes = app_script.routes
102
+
103
+ # although in theory we should be able to run this with multiple threads
104
+ # there are issues with uvloop:
105
+ # e.g.: "Racing with another loop to spawn a process."
106
+ thread_pool = CleanupThreadPoolExecutor(max_workers=1)
107
+
108
+ results = []
109
+ for route in routes:
110
+ results.append(thread_pool.submit(ssg_crawl_route, f"{base_url}/", route, build_path, thread_pool))
111
+
112
+ def wait(async_result):
113
+ results = async_result.result()
114
+ for result in results:
115
+ wait(result)
116
+
117
+ for result in results:
118
+ wait(result)
119
+
120
+ thread_pool.shutdown()
121
+
122
+ rprint("Done building SSG")
123
+
124
+
125
+ def ssg_crawl_route(base_url: str, route: solara.Route, build_path: Path, thread_pool: CleanupThreadPoolExecutor):
126
+ # if route
127
+ url = base_url + (route.path if route.path != "/" else "")
128
+ if not route.children:
129
+ rprint("Check SSG for URL", url)
130
+ build_path.mkdir(exist_ok=True, parents=True)
131
+ path = build_path / ("index.html" if route.path == "/" else route.path + ".html")
132
+ stale = False
133
+ pw = _get_playwright()
134
+ page = pw.page
135
+ if path.exists():
136
+ if route.file is None:
137
+ rprint(f"File corresponding to {url} is not found (route: {route})")
138
+ else:
139
+ assert route.file is not None
140
+ stale = path.stat().st_mtime < route.file.stat().st_mtime
141
+ if stale:
142
+ rprint(f"Path {path} is stale: mtime {path} is older than {route.file} mtime {route.file.stat().st_mtime}")
143
+ if not path.exists() or stale:
144
+ rprint(f"Will generate {path}")
145
+ response = page.goto(url, wait_until="networkidle")
146
+ if response.status != 200:
147
+ raise Exception(f"Failed to load {url} with status {response.status}")
148
+ # TODO: if we don't want to detached, we get stack trace showing errors in solara
149
+ # make sure the html is loaded
150
+ try:
151
+ page.locator("#app").wait_for()
152
+ # make sure vue took over
153
+ page.locator("#pre-rendered-html-present").wait_for(state="detached")
154
+ # and wait for the
155
+ page.locator("text=Loading app").wait_for(state="detached")
156
+ page.locator("#kernel-busy-indicator").wait_for(state="hidden")
157
+ # page.wait_
158
+ time.sleep(0.5)
159
+ raw_html = page.content()
160
+ except Exception:
161
+ logger.exception("Failure retrieving content for url: %s", url)
162
+ raise
163
+ request_path = urllib.parse.urlparse(url).path
164
+
165
+ import solara.server.server
166
+
167
+ # the html from playwright is not what we want, pass it through the jinja template again
168
+ html = solara.server.server.read_root(request_path, ssg_data=_ssg_data(raw_html))
169
+ if html is None:
170
+ raise Exception(f"Failed to render {url}")
171
+ path.write_text(html, encoding="utf-8")
172
+ rprint(f"Wrote to {path}")
173
+ page.goto("about:blank")
174
+ else:
175
+ rprint(f"Skipping existing render: {path}")
176
+ results = []
177
+ for child in route.children:
178
+ result = thread_pool.submit(ssg_crawl_route, url + "/", child, build_path / Path(route.path), thread_pool)
179
+ results.append(result)
180
+ return results
181
+
182
+
183
+ def ssg_content(path: str) -> Optional[str]:
184
+ license.check("SSG")
185
+ # still not sure why we sometimes end with a double slash
186
+ if path.endswith("//"):
187
+ path = path[:-2]
188
+ if path.endswith("/"):
189
+ path = path[:-1]
190
+ if path.startswith("/"):
191
+ # remove / so we don't get absolute paths on disk
192
+ path = path[1:]
193
+ # TODO: how do we know the app?
194
+ build_path = settings.ssg.build_path
195
+ if build_path and settings.ssg.enabled:
196
+ html_path = build_path / path
197
+ if (html_path / "index.html").exists():
198
+ html_path = html_path / "index.html"
199
+ else:
200
+ html_path = html_path.with_suffix(".html")
201
+ if html_path.exists() and html_path.is_file():
202
+ logger.info("Using pre-rendered html at %r", html_path)
203
+ return html_path.read_text("utf8")
204
+ else:
205
+ logger.error("Count not find html at %r", html_path)
206
+ return None
207
+
208
+
209
+ def _ssg_data(html: str) -> Optional[SSGData]:
210
+ license.check("SSG")
211
+ from bs4 import BeautifulSoup, Tag
212
+
213
+ # pre_rendered_css = ""
214
+ styles = []
215
+ title = "Solara ☀️"
216
+
217
+ soup = BeautifulSoup(html, "html.parser")
218
+ node = soup.find(id="app")
219
+ # TODO: add classes...
220
+ if node and isinstance(node, Tag):
221
+ # only render children
222
+ html = "".join(str(x) for x in node.contents)
223
+ title_tag = soup.find("title")
224
+ if title_tag:
225
+ title = title_tag.text
226
+
227
+ # include all meta tags
228
+ rendered_metas = soup.find_all("meta")
229
+ metas = []
230
+ for meta in rendered_metas:
231
+ # but only the ones added by solara
232
+ if meta.attrs.get("data-solara-head-key"):
233
+ metas.append(str(meta))
234
+
235
+ # include all styles
236
+ rendered_styles = soup.find_all("style")
237
+ for style in rendered_styles:
238
+ style_html = str(style)
239
+ # skip css that was already in the template so we don't include it multiple times
240
+ # or such that we do not include the CSS from the theme as ssg build time
241
+ if 'class="solara-template-css"' in style_html:
242
+ continue
243
+ # in case we want to skip the mathjax css
244
+ # if "MJXZERO" in style_html:
245
+ # continue
246
+ # pre_rendered_css += style_html
247
+ styles.append(style_html)
248
+ logger.debug("Include style (size is %r mb):\n\t%r", len(style_html) / 1024**2, style_html[:200])
249
+ return SSGData(title=title, html=html, styles=styles, metas=metas)
@@ -0,0 +1,27 @@
1
+ Metadata-Version: 2.3
2
+ Name: solara-enterprise
3
+ Version: 1.45.0
4
+ Dynamic: Summary
5
+ Project-URL: Home, https://www.github.com/widgetti/solara
6
+ Author-email: "Maarten A. Breddels" <maartenbreddels@gmail.com>, Mario Buikhuizen <mariobuikhuizen@gmail.com>
7
+ License: Not open source, contact contact@solara.dev for licencing.
8
+ Classifier: License :: Free for non-commercial use
9
+ Requires-Python: >=3.7
10
+ Requires-Dist: solara-server==1.45.0
11
+ Requires-Dist: solara-ui==1.45.0
12
+ Provides-Extra: all
13
+ Requires-Dist: authlib; extra == 'all'
14
+ Requires-Dist: beautifulsoup4; extra == 'all'
15
+ Requires-Dist: diskcache; extra == 'all'
16
+ Requires-Dist: httpx; extra == 'all'
17
+ Requires-Dist: itsdangerous; extra == 'all'
18
+ Requires-Dist: playwright; extra == 'all'
19
+ Provides-Extra: auth
20
+ Requires-Dist: authlib; extra == 'auth'
21
+ Requires-Dist: httpx; extra == 'auth'
22
+ Requires-Dist: itsdangerous; extra == 'auth'
23
+ Provides-Extra: cache
24
+ Requires-Dist: diskcache; extra == 'cache'
25
+ Provides-Extra: ssg
26
+ Requires-Dist: beautifulsoup4; extra == 'ssg'
27
+ Requires-Dist: playwright; extra == 'ssg'
@@ -0,0 +1,23 @@
1
+ solara_enterprise/__init__.py,sha256=92t-t5BTHmHGobETurAJRgZyY2DaxpO8axxCnR0EhjI,57
2
+ solara_enterprise/license.py,sha256=GCGEs3x9rtKf0dYUcHkx-yteIQuRlgnS8hPbl9BDAXs,412
3
+ solara_enterprise/ssg.py,sha256=uw3397RX0Qmg-OqOsgwaePDymhUvZTTFcBKkSWy1uCk,8935
4
+ solara_enterprise/auth/__init__.py,sha256=83RGLIkVJrb6R1iZxg5YS0R3kaCmqMHu4qs8S74LJXo,513
5
+ solara_enterprise/auth/components.py,sha256=mTazvLWJrduS6CrcsY60BJgkvQztH5X67CkOCRFXYV4,4038
6
+ solara_enterprise/auth/flask.py,sha256=Y__abnQPnP1cdQAkU8BBg9FuWMZ1rNnUmTLE_x19MlE,3539
7
+ solara_enterprise/auth/middleware.py,sha256=f9yshp6e0M2RSBaJaoeZisYlnM52NEYHTmsnjrW-h3c,5027
8
+ solara_enterprise/auth/starlette.py,sha256=-vow0-dhOLn_H5QJ79F4jynZnn6O63iHVYWqgtxMz4Q,3937
9
+ solara_enterprise/auth/utils.py,sha256=E54A4NNaRWhPLxB7K-B3YlHZsaCySyf3aT0Db0khOLo,1506
10
+ solara_enterprise/cache/__init__.py,sha256=pvQocwiJeUF6V8KFtQl0xPNGo7kNnWhjWCOcChwMP_Q,47
11
+ solara_enterprise/cache/base.py,sha256=Gx-p7khDFSxbdgG83aka7zABx5dLRtt9d9j4Zv69lwU,2098
12
+ solara_enterprise/cache/disk.py,sha256=isHvt0v7KlZ-MHYlSfBclWkreL30OjmZ7ZBMWmCqGl4,1190
13
+ solara_enterprise/cache/memory_size.py,sha256=5PAUaxVvHMIFINA3Z4z48zDvAATTFkEd0ehTXT5ahIA,816
14
+ solara_enterprise/cache/multi_level.py,sha256=hMjruI1cwNwDajaRXAJ7TxQqfw5XWRvtYuAU7M4f_W8,710
15
+ solara_enterprise/cache/redis.py,sha256=_GlACNJ5hfpRJ2Hw_2k3ma6gwEVLoVNTplS0Q5cGcRk,779
16
+ solara_enterprise/search/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ solara_enterprise/search/index.py,sha256=W98DC_6SmQDCfX5YcmS6OfZbO0LheleBmo0FUI7NOqU,3303
18
+ solara_enterprise/search/search.py,sha256=MzChv6DAsG2_ehPltI87orQqBqtY1xkBzXW8eDtMusU,707
19
+ solara_enterprise/search/search.vue,sha256=PSE8KlKHQQY-ZbaNj4eOGSIB9AXH8qmdTDqnDb0l5Kc,6005
20
+ solara_enterprise-1.45.0.dist-info/METADATA,sha256=T8zMnIAR9ZtJ1bEaEuZPAHiudb2gPUtMsR3KTNI6ZSI,1045
21
+ solara_enterprise-1.45.0.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
22
+ solara_enterprise-1.45.0.dist-info/licenses/LICENSE,sha256=04_xbTWtvdcQomu6IXsIkniKk8EcD9P1GyJM2dYPoSU,59
23
+ solara_enterprise-1.45.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.26.3
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1 @@
1
+ Not open source, contact contact@solara.dev for licencing.