keboola-streamlit 0.0.1__tar.gz

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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Vojta Tůma
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.1
2
+ Name: keboola_streamlit
3
+ Version: 0.0.1
4
+ Summary: Keboola Streamlit simplifies the use of Keboola Storage API within Streamlit apps, providing easy-to-use functions for authentication, data retrieval, event logging, and data loading.
5
+ Author: pandyandy
6
+ Author-email: novakovadrea@gmail.com
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Information Technology
12
+ Classifier: Intended Audience :: Education
13
+ Classifier: Topic :: Software Development :: Libraries
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Classifier: Development Status :: 5 - Production/Stable
16
+ Requires-Python: >=3.7
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+
20
+ # KeboolaStreamlit
21
+
22
+ KeboolaStreamlit is a Python package that facilitates the integration of Streamlit applications with the Keboola Storage API. It provides various functionalities for authentication, data retrieval, event creation, and data loading in a Keboola environment.
23
+
24
+ ## Installation
25
+
26
+ To install KeboolaStreamlit, you can use `pip`:
27
+
28
+ ```bash
29
+ pip install keboola-streamlit
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ### Initialization
35
+
36
+ Create an instance of the `KeboolaStreamlit` class with the required parameters:
37
+
38
+ ```python
39
+ from keboola_streamlit import KeboolaStreamlit
40
+
41
+ keboola = KeboolaStreamlit(root_url='https://connection.keboola.com', token='YOUR_API_TOKEN')
42
+ ```
43
+
44
+ ### Authentication Check
45
+
46
+ Ensure that the user is authenticated and authorized to access the Streamlit app:
47
+
48
+ ```python
49
+ keboola.auth_check(required_role_id='YOUR_REQUIRED_ROLE_ID')
50
+ ```
51
+
52
+ ### Retrieve Data
53
+
54
+ Fetch data from a Keboola Storage table and return it as a Pandas DataFrame:
55
+
56
+ ```python
57
+ df = keboola.get_data(table_id='YOUR_TABLE_ID')
58
+ ```
59
+
60
+ ### Load Data
61
+
62
+ Load data from a Pandas DataFrame into a Keboola Storage table:
63
+
64
+ ```python
65
+ keboola.load_data(table_id='YOUR_TABLE_ID', df=your_dataframe, is_incremental=False)
66
+ ```
67
+
68
+ ### Create Event
69
+
70
+ Create an event in Keboola Storage to log activities:
71
+
72
+ ```python
73
+ keboola.create_event(message='Your event message')
74
+ ```
75
+
76
+ ### Add Keboola Table Selection
77
+
78
+ Add a Keboola table selection form to your Streamlit app sidebar:
79
+
80
+ ```python
81
+ selected_table_data = keboola.add_keboola_table_selection()
82
+ ```
83
+
84
+ ## License
85
+
86
+ This project is licensed under the MIT License. See the `LICENSE` file for more details.
87
+
88
+
@@ -0,0 +1,69 @@
1
+ # KeboolaStreamlit
2
+
3
+ KeboolaStreamlit is a Python package that facilitates the integration of Streamlit applications with the Keboola Storage API. It provides various functionalities for authentication, data retrieval, event creation, and data loading in a Keboola environment.
4
+
5
+ ## Installation
6
+
7
+ To install KeboolaStreamlit, you can use `pip`:
8
+
9
+ ```bash
10
+ pip install keboola-streamlit
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ### Initialization
16
+
17
+ Create an instance of the `KeboolaStreamlit` class with the required parameters:
18
+
19
+ ```python
20
+ from keboola_streamlit import KeboolaStreamlit
21
+
22
+ keboola = KeboolaStreamlit(root_url='https://connection.keboola.com', token='YOUR_API_TOKEN')
23
+ ```
24
+
25
+ ### Authentication Check
26
+
27
+ Ensure that the user is authenticated and authorized to access the Streamlit app:
28
+
29
+ ```python
30
+ keboola.auth_check(required_role_id='YOUR_REQUIRED_ROLE_ID')
31
+ ```
32
+
33
+ ### Retrieve Data
34
+
35
+ Fetch data from a Keboola Storage table and return it as a Pandas DataFrame:
36
+
37
+ ```python
38
+ df = keboola.get_data(table_id='YOUR_TABLE_ID')
39
+ ```
40
+
41
+ ### Load Data
42
+
43
+ Load data from a Pandas DataFrame into a Keboola Storage table:
44
+
45
+ ```python
46
+ keboola.load_data(table_id='YOUR_TABLE_ID', df=your_dataframe, is_incremental=False)
47
+ ```
48
+
49
+ ### Create Event
50
+
51
+ Create an event in Keboola Storage to log activities:
52
+
53
+ ```python
54
+ keboola.create_event(message='Your event message')
55
+ ```
56
+
57
+ ### Add Keboola Table Selection
58
+
59
+ Add a Keboola table selection form to your Streamlit app sidebar:
60
+
61
+ ```python
62
+ selected_table_data = keboola.add_keboola_table_selection()
63
+ ```
64
+
65
+ ## License
66
+
67
+ This project is licensed under the MIT License. See the `LICENSE` file for more details.
68
+
69
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,42 @@
1
+ import setuptools
2
+
3
+ with open("README.md", "r") as f:
4
+ long_description = f.read()
5
+
6
+ setuptools.setup(
7
+ name='keboola_streamlit',
8
+ version='0.0.1',
9
+ author="pandyandy",
10
+ #project_urls=project_urls,
11
+ setup_requires=['pytest-runner', 'flake8'],
12
+ tests_require=['pytest'],
13
+ install_requires=[
14
+ 'pygelf',
15
+ 'pytz',
16
+ 'deprecated',
17
+ 'streamlit',
18
+ 'pandas',
19
+ 'kbcstorage'
20
+ ],
21
+ author_email='novakovadrea@gmail.com',
22
+ description='Keboola Streamlit simplifies the use of Keboola Storage API within Streamlit apps, providing easy-to-use functions for authentication, data retrieval, event logging, and data loading.',
23
+ long_description=long_description,
24
+ long_description_content_type="text/markdown",
25
+ package_dir={'': 'src'},
26
+ packages=setuptools.find_packages(where='src'),
27
+ include_package_data=True,
28
+ zip_safe=False,
29
+ test_suite='tests',
30
+ classifiers=[
31
+ "Programming Language :: Python :: 3",
32
+ "License :: OSI Approved :: MIT License",
33
+ "Operating System :: OS Independent",
34
+ "Intended Audience :: Developers",
35
+ "Intended Audience :: Information Technology",
36
+ "Intended Audience :: Education",
37
+ "Topic :: Software Development :: Libraries",
38
+ "Topic :: Software Development :: Libraries :: Python Modules",
39
+ "Development Status :: 5 - Production/Stable"
40
+ ],
41
+ python_requires='>=3.7'
42
+ )
@@ -0,0 +1 @@
1
+ from .keboola_streamlit import KeboolaStreamlit
@@ -0,0 +1,282 @@
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import requests
4
+ import datetime
5
+ import os
6
+
7
+ from kbcstorage.client import Client
8
+ from requests.exceptions import HTTPError
9
+ from typing import Dict, List, Tuple, Optional
10
+ from streamlit.web.server.websocket_headers import _get_websocket_headers
11
+
12
+ class KeboolaStreamlit:
13
+ def __init__(self, root_url: str, token: str, tmp_data_folder: str = 'tmp/'):
14
+ """
15
+ Initializes the KeboolaStreamlit class with the provided parameters.
16
+
17
+ Args:
18
+ root_url (str): The root URL for the Keboola Storage API.
19
+ token (str): The API token for accessing Keboola Storage.
20
+ tmp_data_folder (str): The temporary data folder for storing files locally.
21
+ """
22
+ self.client = Client(root_url, token)
23
+ self.token = token
24
+ self.root_url = root_url
25
+ self.dev_mockup_headers = None
26
+ self.tmp_data_folder = tmp_data_folder
27
+
28
+ def _get_headers(self) -> dict:
29
+ """
30
+ Retrieves the headers for the current request, including development mock headers if set.
31
+
32
+ Returns:
33
+ dict: The headers for the current request.
34
+ """
35
+ headers = _get_websocket_headers()
36
+ return headers if 'X-Kbc-User-Email' in headers else (self.dev_mockup_headers or {})
37
+
38
+ def _get_sapi_client(self):
39
+ """
40
+ Getter for the Keboola Storage API client.
41
+
42
+ Returns:
43
+ Client: The Keboola Storage API client.
44
+ """
45
+ return self.client
46
+
47
+ def set_dev_mockup_headers(self, headers: dict):
48
+ """
49
+ Sets the development mock headers for local development.
50
+
51
+ Args:
52
+ headers (dict): The mock headers to set.
53
+ """
54
+ self.dev_mockup_headers = headers
55
+
56
+ def auth_check(self, required_role_id: str):
57
+ """
58
+ Checks the user's authentication and authorization based on the headers.
59
+
60
+ Args:
61
+ required_role_id (str): The required role ID for authorization.
62
+
63
+ Stops the Streamlit app if the user is not authorized.
64
+ """
65
+ headers = self._get_headers()
66
+
67
+ if 'X-Kbc-User-Email' in headers:
68
+ user_email = headers['X-Kbc-User-Email']
69
+ st.sidebar.write(f'Logged in as user: {user_email}')
70
+ st.sidebar.link_button('Logout', '/_proxy/sign_out', use_container_width=True)
71
+ with st.sidebar.expander('Show more'):
72
+ st.write(headers)
73
+
74
+ user_roles = headers.get('X-Kbc-User-Roles', [])
75
+ if required_role_id not in user_roles:
76
+ st.error("You are not authorised to use this app.")
77
+ st.stop()
78
+ else:
79
+ st.write('Not using proxy')
80
+
81
+
82
+ def create_event(self, message: str = 'Streamlit App Create Event', endpoint: str = '/v2/storage/events/create', data: Optional[str] = None, jobId: Optional[int] = None):
83
+ """
84
+ Creates an event in Keboola Storage.
85
+
86
+ Args:
87
+ jobId (int): The job ID for the event.
88
+ message (str): The message for the event.
89
+ data (str): The data associated with the event.
90
+ endpoint (str): The endpoint for the event.
91
+
92
+ Displays a success or error message in the Streamlit app.
93
+ """
94
+ headers = self._get_headers()
95
+ url = f'{self.root_url}/v2/storage/events'
96
+ requestHeaders = {
97
+ 'Content-Type': 'application/json',
98
+ 'X-StorageApi-Token': self.token
99
+ }
100
+ requestData = {
101
+ 'message': message,
102
+ 'component': 'keboola.data-apps',
103
+ 'params': {
104
+ 'user': headers.get('X-Kbc-User-Email', 'Unknown'),
105
+ 'time': datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
106
+ 'endpoint': endpoint,
107
+ 'event_type': 'keboola_data_app_write',
108
+ 'event_application': headers['Origin']
109
+ }
110
+ }
111
+
112
+ if data is not None:
113
+ requestData['params']['event_data'] = {'data': f'{data}'}
114
+
115
+ if jobId is not None:
116
+ requestData['params']['event_job_id'] = jobId
117
+
118
+ response = requests.post(url, headers=requestHeaders, json=requestData)
119
+
120
+ if response.status_code == 201:
121
+ st.success('Event sent.')
122
+ else:
123
+ st.error(f'Error: {response.status_code} - {response.text}')
124
+
125
+ def get_event_job_id(self, table_id: str, operation_name: str):
126
+ client = self._get_sapi_client()
127
+ job_list = client.jobs.list()
128
+ job_id = ''
129
+ for job in job_list:
130
+ if job['tableId'] == table_id and job['operationName'] == operation_name:
131
+ job_id = job['id']
132
+ break
133
+ return job_id
134
+
135
+ def get_data(self, table_id: str, endpoint: str = '/v2/storage/tables/export_to_file') -> pd.DataFrame:
136
+ """
137
+ Retrieves data from a Keboola Storage table and returns it as a Pandas DataFrame.
138
+
139
+ Args:
140
+ table_id (str): The ID of the table to retrieve data from.
141
+ endpoint (str): The endpoint for retrieving table details.
142
+
143
+ Returns:
144
+ pd.DataFrame: The table data as a Pandas DataFrame.
145
+ """
146
+ client = self._get_sapi_client()
147
+ try:
148
+ table_detail = client.tables.detail(table_id)
149
+ table_name = table_detail['name']
150
+ if not os.path.exists(self.tmp_data_folder):
151
+ os.makedirs(self.tmp_data_folder)
152
+
153
+ client.tables.export_to_file(table_id=table_id, path_name=self.tmp_data_folder)
154
+
155
+ csv_path = os.path.join(self.tmp_data_folder, f'{table_name}.csv')
156
+ if os.path.exists(csv_path):
157
+ os.remove(csv_path)
158
+
159
+ exported_file_path = os.path.join(self.tmp_data_folder, table_name)
160
+ os.rename(exported_file_path, csv_path)
161
+ df = pd.read_csv(csv_path)
162
+
163
+ event_job_id = self.get_event_job_id(table_id=table_id, operation_name='tableExport')
164
+ self.create_event(
165
+ jobId=event_job_id,
166
+ message='Streamlit App Download Data',
167
+ endpoint=endpoint
168
+ )
169
+ return df
170
+ except Exception as e:
171
+ st.error(f'An error occurred while retrieving data: {str(e)}')
172
+ return pd.DataFrame()
173
+
174
+
175
+ def load_data(self, table_id: str, df: pd.DataFrame, is_incremental: bool = False, endpoint: str = '/v2/storage/tables/load'):
176
+ """
177
+ Load data into an existing table.
178
+
179
+ Args:
180
+ table_id (str): The ID of the table to load data into.
181
+ df (pd.DataFrame): The DataFrame containing the data to be loaded.
182
+ is_incremental (bool): Whether to load incrementally (do not truncate the table). Defaults to False.
183
+ endpoint (str): The endpoint for loading data.
184
+
185
+ Displays a success or error message in the Streamlit app.
186
+ """
187
+ client = self._get_sapi_client()
188
+ csv_path = f'{table_id}.csv'
189
+ try:
190
+ df.to_csv(csv_path, index=False)
191
+ client.tables.load(table_id=table_id, file_path=csv_path, is_incremental=is_incremental)
192
+ event_job_id = self.get_event_job_id(table_id=table_id, operation_name='tableImport')
193
+ self.create_event(
194
+ jobId=event_job_id,
195
+ message='Streamlit App Load Data',
196
+ endpoint=endpoint,
197
+ data=df
198
+ )
199
+ except Exception as e:
200
+ st.error(f'Data upload failed with: {str(e)}')
201
+ finally:
202
+ if os.path.exists(csv_path):
203
+ os.remove(csv_path)
204
+
205
+ def add_keboola_table_selection(self):
206
+ self._add_connection_form()
207
+ if 'kbc_storage_client' in st.session_state:
208
+ self._add_bucket_form()
209
+ if 'selected_bucket' in st.session_state and 'kbc_storage_client' in st.session_state:
210
+ self._add_table_form()
211
+ if 'selected_table_id' in st.session_state and 'kbc_storage_client' in st.session_state:
212
+ selected_table_id = st.session_state['selected_table_id']
213
+ if 'tables_data' not in st.session_state:
214
+ st.session_state['tables_data'] = {}
215
+ if selected_table_id not in st.session_state['tables_data']:
216
+ st.session_state['tables_data'][selected_table_id] = self.get_data(table_id=selected_table_id)
217
+ return st.session_state['tables_data'][selected_table_id]
218
+ return pd.DataFrame()
219
+
220
+
221
+ def _add_connection_form(self):
222
+ with st.sidebar.form('Connection Details'):
223
+ if st.form_submit_button('Connect', use_container_width=True):
224
+ try:
225
+ kbc_client = self._get_sapi_client()
226
+
227
+ if 'kbc_storage_client' in st.session_state:
228
+ st.session_state.pop('kbc_storage_client')
229
+ if 'selected_table' in st.session_state:
230
+ st.session_state.pop('selected_table')
231
+ if 'selected_table_id' in st.session_state:
232
+ st.session_state.pop('selected_table_id')
233
+ if 'selected_bucket' in st.session_state:
234
+ st.session_state.pop('selected_bucket')
235
+ if 'uploaded_file' in st.session_state:
236
+ st.session_state.pop('uploaded_file')
237
+
238
+ if self._get_bucket_list(kbc_client):
239
+ st.session_state['kbc_storage_client'] = kbc_client
240
+ st.session_state['bucket_list'] = self._get_bucket_list(kbc_client)
241
+
242
+ except Exception as e:
243
+ st.error(f'Connection failed: {str(e)}')
244
+
245
+ def _add_bucket_form(self):
246
+ with st.sidebar.form('Bucket Details'):
247
+ with st.header('Select a bucket from storage'):
248
+ bucket = st.selectbox('Bucket', self._get_buckets_from_bucket_list())
249
+ if st.form_submit_button('Select Bucket',use_container_width=True):
250
+ st.session_state['selected_bucket'] = bucket
251
+
252
+ def _add_table_form(self):
253
+ with st.sidebar.form('Table Details'):
254
+ table_names, tables = self._get_tables(st.session_state['selected_bucket'])
255
+ st.session_state['selected_table'] = st.selectbox('Table', table_names)
256
+ table_id = tables[st.session_state['selected_table']]['id']
257
+ if st.form_submit_button('Select table', use_container_width=True):
258
+ st.session_state['selected_table_id'] = table_id
259
+
260
+ def _get_bucket_list(self, kbc_storage_client):
261
+ try:
262
+ project_bucket_list = kbc_storage_client.buckets.list()
263
+ return project_bucket_list
264
+ except HTTPError:
265
+ st.error('Invalid Connection settings')
266
+
267
+ def _get_buckets_from_bucket_list(self):
268
+ try:
269
+ return [bucket['id'] for bucket in st.session_state['bucket_list']]
270
+ except Exception:
271
+ st.error('Could not list buckets')
272
+
273
+ def _get_tables(self, bucket_id: str) -> Tuple[List, Dict]:
274
+ try:
275
+ tables = {}
276
+ for table in st.session_state['kbc_storage_client'].buckets.list_tables(bucket_id):
277
+ tables[table['name']] = table
278
+ table_names = list(tables.keys())
279
+ return table_names, tables
280
+ except Exception as e:
281
+ st.error('Could not list tables')
282
+ st.error(e)
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.1
2
+ Name: keboola-streamlit
3
+ Version: 0.0.1
4
+ Summary: Keboola Streamlit simplifies the use of Keboola Storage API within Streamlit apps, providing easy-to-use functions for authentication, data retrieval, event logging, and data loading.
5
+ Author: pandyandy
6
+ Author-email: novakovadrea@gmail.com
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Information Technology
12
+ Classifier: Intended Audience :: Education
13
+ Classifier: Topic :: Software Development :: Libraries
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Classifier: Development Status :: 5 - Production/Stable
16
+ Requires-Python: >=3.7
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+
20
+ # KeboolaStreamlit
21
+
22
+ KeboolaStreamlit is a Python package that facilitates the integration of Streamlit applications with the Keboola Storage API. It provides various functionalities for authentication, data retrieval, event creation, and data loading in a Keboola environment.
23
+
24
+ ## Installation
25
+
26
+ To install KeboolaStreamlit, you can use `pip`:
27
+
28
+ ```bash
29
+ pip install keboola-streamlit
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ### Initialization
35
+
36
+ Create an instance of the `KeboolaStreamlit` class with the required parameters:
37
+
38
+ ```python
39
+ from keboola_streamlit import KeboolaStreamlit
40
+
41
+ keboola = KeboolaStreamlit(root_url='https://connection.keboola.com', token='YOUR_API_TOKEN')
42
+ ```
43
+
44
+ ### Authentication Check
45
+
46
+ Ensure that the user is authenticated and authorized to access the Streamlit app:
47
+
48
+ ```python
49
+ keboola.auth_check(required_role_id='YOUR_REQUIRED_ROLE_ID')
50
+ ```
51
+
52
+ ### Retrieve Data
53
+
54
+ Fetch data from a Keboola Storage table and return it as a Pandas DataFrame:
55
+
56
+ ```python
57
+ df = keboola.get_data(table_id='YOUR_TABLE_ID')
58
+ ```
59
+
60
+ ### Load Data
61
+
62
+ Load data from a Pandas DataFrame into a Keboola Storage table:
63
+
64
+ ```python
65
+ keboola.load_data(table_id='YOUR_TABLE_ID', df=your_dataframe, is_incremental=False)
66
+ ```
67
+
68
+ ### Create Event
69
+
70
+ Create an event in Keboola Storage to log activities:
71
+
72
+ ```python
73
+ keboola.create_event(message='Your event message')
74
+ ```
75
+
76
+ ### Add Keboola Table Selection
77
+
78
+ Add a Keboola table selection form to your Streamlit app sidebar:
79
+
80
+ ```python
81
+ selected_table_data = keboola.add_keboola_table_selection()
82
+ ```
83
+
84
+ ## License
85
+
86
+ This project is licensed under the MIT License. See the `LICENSE` file for more details.
87
+
88
+
@@ -0,0 +1,11 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ src/keboola_streamlit/__init__.py
5
+ src/keboola_streamlit/keboola_streamlit.py
6
+ src/keboola_streamlit.egg-info/PKG-INFO
7
+ src/keboola_streamlit.egg-info/SOURCES.txt
8
+ src/keboola_streamlit.egg-info/dependency_links.txt
9
+ src/keboola_streamlit.egg-info/not-zip-safe
10
+ src/keboola_streamlit.egg-info/requires.txt
11
+ src/keboola_streamlit.egg-info/top_level.txt
@@ -0,0 +1,6 @@
1
+ pygelf
2
+ pytz
3
+ deprecated
4
+ streamlit
5
+ pandas
6
+ kbcstorage
@@ -0,0 +1 @@
1
+ keboola_streamlit