bfabric-web-apps 0.1.2__py3-none-any.whl → 0.1.4__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.
@@ -16,10 +16,15 @@ from .utils.get_power_user_wrapper import get_power_user_wrapper
16
16
  from .utils.create_app_in_bfabric import create_app_in_bfabric
17
17
 
18
18
  # Export callbacks
19
- from .utils.callbacks import process_url_and_token, submit_bug_report
19
+ from .utils.callbacks import (
20
+ process_url_and_token,
21
+ submit_bug_report,
22
+ populate_workunit_details
23
+ )
20
24
 
21
25
  from .utils import defaults
22
26
 
27
+ from bfabric_web_apps.utils.resource_utilities import create_workunit, create_resource, create_workunits, create_resources
23
28
  HOST = os.getenv("HOST", defaults.HOST)
24
29
  PORT = int(os.getenv("PORT", defaults.PORT)) # Convert to int since env variables are strings
25
30
  DEV = os.getenv("DEV", str(defaults.DEV)).lower() in ["true", "1", "yes"] # Convert to bool
@@ -46,52 +51,10 @@ __all__ = [
46
51
  'CONFIG_FILE_PATH',
47
52
  'DEVELOPER_EMAIL_ADDRESS',
48
53
  'BUG_REPORT_EMAIL_ADDRESS',
49
- 'create_app_in_bfabric'
54
+ 'create_app_in_bfabric',
55
+ 'create_workunit',
56
+ 'create_resource',
57
+ 'create_workunits',
58
+ 'create_resources',
59
+ 'populate_workunit_details',
50
60
  ]
51
-
52
-
53
-
54
- '''
55
- import os
56
- from .utils import defaults
57
-
58
- # Private variable for CONFIG_FILE_PATH
59
- _CONFIG_FILE_PATH = os.getenv("CONFIG_FILE_PATH", defaults.CONFIG_FILE_PATH)
60
-
61
- def set_config_file_path(path):
62
- """
63
- Setter for the CONFIG_FILE_PATH variable.
64
- """
65
- global _CONFIG_FILE_PATH
66
- if not isinstance(path, str):
67
- raise ValueError("CONFIG_FILE_PATH must be a string.")
68
- _CONFIG_FILE_PATH = path
69
-
70
- def get_config_file_path():
71
- """
72
- Getter for the CONFIG_FILE_PATH variable.
73
- """
74
- return _CONFIG_FILE_PATH
75
-
76
- # Expose CONFIG_FILE_PATH as a read-only property
77
- class Config:
78
- @property
79
- def CONFIG_FILE_PATH(self):
80
- return get_config_file_path()
81
-
82
- config = Config()
83
-
84
- '''
85
-
86
-
87
-
88
- '''
89
- from bfabric import config
90
-
91
- config.CONFIG_FILE_PATH
92
- '''
93
-
94
- '''
95
- from bfabric import set_config_file_path
96
- set_config_file_path("new/path/to/config.json")
97
- '''
@@ -144,8 +144,9 @@ def get_static_layout(base_title=None, main_content=None, documentation_content=
144
144
  dbc.Tabs(
145
145
  [
146
146
  dbc.Tab(main_content, label="Main", tab_id="main"),
147
- dbc.Tab(get_documentation_tab(documentation_content), label="Documentation", tab_id="documentation"),
148
- dbc.Tab(get_report_bug_tab(), label="Report a Bug", tab_id="report-bug"),
147
+ dbc.Tab(dcc.Loading(get_documentation_tab(documentation_content)), label="Documentation", tab_id="documentation"),
148
+ dbc.Tab(dcc.Loading(get_workunits_tab()), label="Workunits", tab_id="workunits"),
149
+ dbc.Tab(dcc.Loading(get_report_bug_tab()), label="Report a Bug", tab_id="report-bug"),
149
150
  ],
150
151
  id="tabs",
151
152
  active_tab="main",
@@ -250,6 +251,54 @@ def get_report_bug_tab():
250
251
  "margin-left": "2vw",
251
252
  "font-size": "20px",
252
253
  "padding-right": "40px",
254
+ "overflow-y": "scroll",
255
+ "max-height": "65vh",
256
+ },
257
+ ),
258
+ width=9,
259
+ ),
260
+ ],
261
+ style={"margin-top": "0px", "min-height": "40vh"},
262
+ )
263
+
264
+
265
+ def get_workunits_tab():
266
+ """
267
+ Returns the content for the Workunits tab with the upgraded layout.
268
+ """
269
+ return dbc.Row(
270
+ id="page-content-workunits",
271
+ children=[
272
+ dbc.Col(
273
+ html.Div(
274
+ id="sidebar_workunits",
275
+ children=[], # Optional: Add sidebar content here if needed
276
+ style={
277
+ "border-right": "2px solid #d4d7d9",
278
+ "height": "100%",
279
+ "padding": "20px",
280
+ "font-size": "20px",
281
+ },
282
+ ),
283
+ width=3,
284
+ ),
285
+ dbc.Col(
286
+ html.Div(
287
+ id="page-content-workunits-children",
288
+ children=[
289
+ html.H2("Workunits"),
290
+ html.Div(id="refresh-workunits", children=[]),
291
+ html.Div(
292
+ id="workunits-content"
293
+ )
294
+ ],
295
+ style={
296
+ "margin-top": "2vh",
297
+ "margin-left": "2vw",
298
+ "font-size": "20px",
299
+ "padding-right": "40px",
300
+ "overflow-y": "scroll",
301
+ "max-height": "65vh",
253
302
  },
254
303
  ),
255
304
  width=9,
@@ -9,13 +9,12 @@ from bfabric_web_apps.utils.get_logger import get_logger
9
9
  import os
10
10
  import bfabric_web_apps
11
11
 
12
-
13
-
14
12
  VALIDATION_URL = "https://fgcz-bfabric.uzh.ch/bfabric/rest/token/validate?token="
15
13
  HOST = "fgcz-bfabric.uzh.ch"
16
14
 
17
-
18
15
  class BfabricInterface( Bfabric ):
16
+ _instance = None # Singleton instance
17
+ _wrapper = None # Shared wrapper instance
19
18
  """
20
19
  A class to interface with the Bfabric API, providing methods to validate tokens,
21
20
  retrieve data, and send bug reports.
@@ -27,6 +26,29 @@ class BfabricInterface( Bfabric ):
27
26
  """
28
27
  pass
29
28
 
29
+ def __new__(cls, *args, **kwargs):
30
+ """Ensure only one instance exists (Singleton Pattern)."""
31
+ if cls._instance is None:
32
+ cls._instance = super(BfabricInterface, cls).__new__(cls)
33
+ return cls._instance
34
+
35
+ def _initialize_wrapper(self, token_data):
36
+ """Internal method to initialize the Bfabric wrapper after token validation."""
37
+ if not token_data:
38
+ raise ValueError("Token data is required to initialize the wrapper.")
39
+
40
+ # Create and store the wrapper
41
+ if self._wrapper is None:
42
+ self._wrapper = self.token_response_to_bfabric(token_data)
43
+
44
+
45
+ def get_wrapper(self):
46
+ """Return the existing wrapper or raise an error if not initialized."""
47
+ if self._wrapper is None:
48
+ raise RuntimeError("Bfabric wrapper is not initialized. Token validation must run first.")
49
+ return self._wrapper
50
+
51
+
30
52
  def token_to_data(self, token):
31
53
  """
32
54
  Validates the given token and retrieves its associated data.
@@ -83,6 +105,9 @@ class BfabricInterface( Bfabric ):
83
105
  jobId = userinfo['jobId']
84
106
  )
85
107
 
108
+ # Initialize the wrapper right after validating the token
109
+ self._initialize_wrapper(token_data)
110
+
86
111
  return json.dumps(token_data)
87
112
 
88
113
 
@@ -133,7 +158,7 @@ class BfabricInterface( Bfabric ):
133
158
  if not token_data:
134
159
  return json.dumps({})
135
160
 
136
- wrapper = self.token_response_to_bfabric(token_data)
161
+ wrapper = self.get_wrapper()
137
162
  entity_class = token_data.get('entityClass_data', None)
138
163
  endpoint = entity_class_map.get(entity_class, None)
139
164
  entity_id = token_data.get('entity_id_data', None)
@@ -209,7 +234,7 @@ class BfabricInterface( Bfabric ):
209
234
  L = get_logger(token_data)
210
235
 
211
236
  # Get API wrapper
212
- wrapper = self.token_response_to_bfabric(token_data) # Same as entity_data
237
+ wrapper = self.get_wrapper()
213
238
  if not wrapper:
214
239
  print("Failed to get Bfabric API wrapper")
215
240
  return json.dumps({})
@@ -234,9 +259,11 @@ class BfabricInterface( Bfabric ):
234
259
  )
235
260
  return json.dumps({})
236
261
 
237
- # Extract Name and Description
262
+ # Extract App ID, Name, and Description
238
263
  app_info = app_data_dict[0] # First (and only) result
264
+
239
265
  json_data = json.dumps({
266
+ "id": app_info.get("id", "Unknown"),
240
267
  "name": app_info.get("name", "Unknown"),
241
268
  "description": app_info.get("description", "No description available")
242
269
  })
@@ -279,7 +306,10 @@ class BfabricInterface( Bfabric ):
279
306
  os.system(mail)
280
307
 
281
308
  return True
309
+
282
310
 
283
-
311
+
312
+ # Create a globally accessible instance
313
+ bfabric_interface = BfabricInterface()
284
314
 
285
315
 
@@ -1,5 +1,5 @@
1
1
  from dash import Input, Output, State, html, dcc
2
- from bfabric_web_apps.objects.BfabricInterface import BfabricInterface
2
+ from bfabric_web_apps.objects.BfabricInterface import bfabric_interface
3
3
  import json
4
4
  import dash_bootstrap_components as dbc
5
5
  from datetime import datetime as dt
@@ -28,7 +28,6 @@ def process_url_and_token(url_params):
28
28
  return None, None, None, None, base_title, None, None
29
29
 
30
30
  token = "".join(url_params.split('token=')[1:])
31
- bfabric_interface = BfabricInterface()
32
31
  tdata_raw = bfabric_interface.token_to_data(token)
33
32
 
34
33
  if tdata_raw:
@@ -88,7 +87,6 @@ def process_url_and_token(url_params):
88
87
  return None, None, None, None, base_title, None, None
89
88
 
90
89
 
91
-
92
90
  def submit_bug_report(n_clicks, bug_description, token, entity_data):
93
91
  """
94
92
  Submits a bug report based on user input, token, and entity data.
@@ -103,7 +101,7 @@ def submit_bug_report(n_clicks, bug_description, token, entity_data):
103
101
  tuple: A tuple containing two boolean values indicating success and failure status of the submission.
104
102
  (is_open_success, is_open_failure)
105
103
  """
106
- bfabric_interface = BfabricInterface()
104
+
107
105
  print("submit bug report", token)
108
106
 
109
107
  # Parse token data if token is provided, otherwise set it to an empty dictionary
@@ -168,3 +166,66 @@ def submit_bug_report(n_clicks, bug_description, token, entity_data):
168
166
 
169
167
  return False, False
170
168
 
169
+
170
+ def populate_workunit_details(token_data):
171
+
172
+ """
173
+ Function to populate workunit data for the current app instance.
174
+
175
+ Args:
176
+ token_data (dict): Token metadata.
177
+
178
+ Returns:
179
+ html.Div: A div containing the populated workunit data.
180
+ """
181
+
182
+ environment_urls = {
183
+ "Test": "https://fgcz-bfabric-test.uzh.ch/bfabric/workunit/show.html?id=",
184
+ "Prod": "https://fgcz-bfabric.uzh.ch/bfabric/workunit/show.html?id="
185
+ }
186
+
187
+ if token_data:
188
+
189
+ jobId = token_data.get('jobId', None)
190
+ print("jobId", jobId)
191
+
192
+ job = bfabric_interface.get_wrapper().read("job", {"id": jobId})[0]
193
+ workunits = job.get("workunit", [])
194
+
195
+ if workunits:
196
+ wus = bfabric_interface.get_wrapper().read(
197
+ "workunit",
198
+ {"id": [wu["id"] for wu in workunits]}
199
+ )
200
+ else:
201
+ return html.Div(
202
+ [
203
+ html.P("No workunits found for the current job.")
204
+ ]
205
+ )
206
+
207
+ wu_cards = []
208
+
209
+ for wu in wus:
210
+ print(wu)
211
+ wu_card = html.A(
212
+ dbc.Card([
213
+ dbc.CardHeader(html.B(f"Workunit {wu['id']}")),
214
+ dbc.CardBody([
215
+ html.P(f"Name: {wu.get('name', 'n/a')}"),
216
+ html.P(f"Description: {wu.get('description', 'n/a')}"),
217
+ html.P(f"Num Resources: {len(wu.get('resource', []))}"),
218
+ html.P(f"Created: {wu.get('created', 'n/a')}"),
219
+ html.P(f"Status: {wu.get('status', 'n/a')}")
220
+ ])
221
+ ], style={"width": "400px", "margin":"10px"}),
222
+ href=environment_urls[token_data.get("environment", "Test")] + str(wu["id"]),
223
+ target="_blank",
224
+ style={"text-decoration": "none"}
225
+ )
226
+
227
+ wu_cards.append(wu_card)
228
+
229
+ return dbc.Container(wu_cards, style={"display": "flex", "flex-wrap": "wrap"})
230
+ else:
231
+ return html.Div()
@@ -0,0 +1,156 @@
1
+ from bfabric_web_apps.utils.get_logger import get_logger
2
+ from bfabric_web_apps.objects.BfabricInterface import bfabric_interface
3
+ from bfabric_web_apps.utils.get_power_user_wrapper import get_power_user_wrapper
4
+ from bfabric_scripts.bfabric_upload_resource import bfabric_upload_resource
5
+ from pathlib import Path
6
+
7
+ def create_workunit(token_data, application_name, application_description, application_id, container_id):
8
+ """
9
+ Create a single workunit in B-Fabric.
10
+
11
+ Args:
12
+ token_data (dict): Authentication token data.
13
+ application_name (str): Name of the application.
14
+ application_description (str): Description of the application.
15
+ application_id (int): Application ID.
16
+ container_id (int): Container ID (Order ID).
17
+
18
+ Returns:
19
+ int: Created workunit ID or None if creation fails.
20
+ """
21
+ L = get_logger(token_data)
22
+ wrapper = bfabric_interface.get_wrapper()
23
+
24
+ workunit_data = {
25
+ "name": f"{application_name} - Order {container_id}",
26
+ "description": f"{application_description} for Order {container_id}",
27
+ "applicationid": int(application_id),
28
+ "containerid": container_id,
29
+ }
30
+
31
+ try:
32
+ workunit_response = L.logthis(
33
+ api_call=wrapper.save,
34
+ endpoint="workunit",
35
+ obj=workunit_data,
36
+ params=None,
37
+ flush_logs=True
38
+ )
39
+ workunit_id = workunit_response[0].get("id")
40
+ print(f"Created Workunit ID: {workunit_id} for Order ID: {container_id}")
41
+
42
+ # First we get the existing workunit_ids for the current job object:
43
+ pre_existing_workunit_ids = [elt.get("id") for elt in wrapper.read("job", {"id": token_data.get("jobId")})[0].get("workunit", [])]
44
+
45
+ # Now we associate the job object with the workunits
46
+ job = L.logthis(
47
+ api_call=L.power_user_wrapper.save,
48
+ endpoint="job",
49
+ obj={"id": token_data.get("jobId"), "workunitid": [workunit_id] + pre_existing_workunit_ids},
50
+ params=None,
51
+ flush_logs=True
52
+ )
53
+ return workunit_id
54
+
55
+ except Exception as e:
56
+ L.log_operation(
57
+ "Error",
58
+ f"Failed to create workunit for Order {container_id}: {e}",
59
+ params=None,
60
+ flush_logs=True,
61
+ )
62
+ print(f"Failed to create workunit for Order {container_id}: {e}")
63
+ return None
64
+
65
+
66
+ def create_workunits(token_data, application_name, application_description, application_id, container_ids):
67
+ """
68
+ Create multiple workunits in B-Fabric.
69
+
70
+ Args:
71
+ token_data (dict): Authentication token data.
72
+ application_name (str): Name of the application.
73
+ application_description (str): Description of the application.
74
+ application_id (int): Application ID.
75
+ container_ids (list): List of container IDs.
76
+
77
+ Returns:
78
+ list: List of created workunit IDs.
79
+ """
80
+ if not isinstance(container_ids, list):
81
+ container_ids = [container_ids] # Ensure it's a list
82
+
83
+ workunit_ids = [
84
+ create_workunit(token_data, application_name, application_description, application_id, container_id)
85
+ for container_id in container_ids
86
+ ]
87
+
88
+ return [wu_id for wu_id in workunit_ids if wu_id is not None] # Filter out None values
89
+
90
+
91
+ def create_resource(token_data, workunit_id, gz_file_path):
92
+ """
93
+ Upload a single .gz resource to an existing B-Fabric workunit.
94
+
95
+ Args:
96
+ token_data (dict): Authentication token data.
97
+ workunit_id (int): ID of the workunit to associate the resource with.
98
+ gz_file_path (str): Full path to the .gz file to upload.
99
+
100
+ Returns:
101
+ int: Resource ID if successful, None otherwise.
102
+ """
103
+ L = get_logger(token_data)
104
+ wrapper = get_power_user_wrapper(token_data)
105
+
106
+ try:
107
+ file_path = Path(gz_file_path)
108
+
109
+ # Upload the resource
110
+ print("Uploading:", file_path, "to workunit:", workunit_id)
111
+ result = bfabric_upload_resource(wrapper, file_path, workunit_id)
112
+
113
+ if result:
114
+ print(f"Resource uploaded: {file_path.name}")
115
+ L.log_operation(
116
+ "upload_resource",
117
+ f"Resource uploaded successfully: {file_path.name}",
118
+ params=None,
119
+ flush_logs=True,
120
+ )
121
+ return result
122
+ else:
123
+ raise ValueError(f"Failed to upload resource: {file_path.name}")
124
+
125
+ except Exception as e:
126
+ L.log_operation(
127
+ "error",
128
+ f"Failed to upload resource: {e}",
129
+ params=None,
130
+ flush_logs=True,
131
+ )
132
+ print(f"Failed to upload resource: {e}")
133
+ return None
134
+
135
+
136
+ def create_resources(token_data, workunit_id, gz_file_paths):
137
+ """
138
+ Upload multiple .gz resources to an existing B-Fabric workunit.
139
+
140
+ Args:
141
+ token_data (dict): Authentication token data.
142
+ workunit_id (int): ID of the workunit to associate the resources with.
143
+ gz_file_paths (list): List of full paths to .gz files to upload.
144
+
145
+ Returns:
146
+ list: List of successfully uploaded resource IDs.
147
+ """
148
+ if not isinstance(gz_file_paths, list):
149
+ gz_file_paths = [gz_file_paths] # Ensure it's a list
150
+
151
+ resource_ids = [
152
+ create_resource(token_data, workunit_id, gz_file_path)
153
+ for gz_file_path in gz_file_paths
154
+ ]
155
+
156
+ return [res_id for res_id in resource_ids if res_id is not None] # Filter out None values
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bfabric-web-apps
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: A package containing handy boilerplate utilities for developing bfabric web-applications
5
5
  Author: Marc Zuber, Griffin White, GWC GmbH
6
6
  Requires-Python: >=3.8,<4.0
@@ -0,0 +1,16 @@
1
+ bfabric_web_apps/__init__.py,sha256=jU5o22wl7kXHNJVCH6aqW0llZLfxeQssCIeX1OerQfI,1790
2
+ bfabric_web_apps/layouts/layouts.py,sha256=fmv_QTJeAmiOxreAwx14QojzyRV_8RHu1c4sCPN5r5U,13382
3
+ bfabric_web_apps/objects/BfabricInterface.py,sha256=wmcL9JuSC0QEopgImvkZxmtCIS7izt6bwb6y_ch0zus,10178
4
+ bfabric_web_apps/objects/Logger.py,sha256=62LC94xhm7YG5LUw3yH46NqvJQsAX7wnc9D4zbY16rA,5224
5
+ bfabric_web_apps/utils/app_init.py,sha256=RCdpCXp19cF74bouYJLPe-KSETZ0Vwqtd02Ta2VXEF8,428
6
+ bfabric_web_apps/utils/callbacks.py,sha256=XbRMK2sL55twtR6IWGAf5B1m2fnMTOpkhyR55-76nes,8444
7
+ bfabric_web_apps/utils/components.py,sha256=V7ECGmF2XYy5O9ciDJVH1nofJYP2a_ELQF3z3X_ADbo,844
8
+ bfabric_web_apps/utils/create_app_in_bfabric.py,sha256=eVk3cQDXxW-yo9b9n_zzGO6kLg_SLxYbIDECyvEPJXU,2752
9
+ bfabric_web_apps/utils/defaults.py,sha256=B82j3JEbysLEU9JDZgoDBTX7WGvW3Hn5YMZaWAcjZew,278
10
+ bfabric_web_apps/utils/get_logger.py,sha256=0Y3SrXW93--eglS0_ZOc34NOriAt6buFPik5n0ltzRA,434
11
+ bfabric_web_apps/utils/get_power_user_wrapper.py,sha256=T33z64XjmJ0KSlmfEmrEP8eYpbpINCVD6Xld_V7PR2g,1027
12
+ bfabric_web_apps/utils/resource_utilities.py,sha256=q0gC_Lr5GQlMBU0_gLm48zjq3XlXbT4QArqzJcmxrTo,5476
13
+ bfabric_web_apps-0.1.4.dist-info/LICENSE,sha256=k0O_i2k13i9e35aO-j7FerJafAqzzu8x0kkBs0OWF3c,1065
14
+ bfabric_web_apps-0.1.4.dist-info/METADATA,sha256=5TKRMRQB4an34gV7b4_hValbQEvuLvOmJpB9DsKBFR4,480
15
+ bfabric_web_apps-0.1.4.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
16
+ bfabric_web_apps-0.1.4.dist-info/RECORD,,
@@ -1,15 +0,0 @@
1
- bfabric_web_apps/__init__.py,sha256=zZvM0CQPkiu0y9xOZos-d2hK-h6erc8K_nov7vD1bdE,2362
2
- bfabric_web_apps/layouts/layouts.py,sha256=XoSLcQPcgMBhQz2VfxkUzNL23FLBXFRFvbCL2mNLfnk,11636
3
- bfabric_web_apps/objects/BfabricInterface.py,sha256=nRdU_cYLW2_xp2x1cvP9b30gQ5blavbOz66B-Hi6UZQ,8980
4
- bfabric_web_apps/objects/Logger.py,sha256=62LC94xhm7YG5LUw3yH46NqvJQsAX7wnc9D4zbY16rA,5224
5
- bfabric_web_apps/utils/app_init.py,sha256=RCdpCXp19cF74bouYJLPe-KSETZ0Vwqtd02Ta2VXEF8,428
6
- bfabric_web_apps/utils/callbacks.py,sha256=PiP1ZJ-QxdrOAZ-Mt-MN-g9wJLSOoLkWkXwPq_TLqDI,6472
7
- bfabric_web_apps/utils/components.py,sha256=V7ECGmF2XYy5O9ciDJVH1nofJYP2a_ELQF3z3X_ADbo,844
8
- bfabric_web_apps/utils/create_app_in_bfabric.py,sha256=eVk3cQDXxW-yo9b9n_zzGO6kLg_SLxYbIDECyvEPJXU,2752
9
- bfabric_web_apps/utils/defaults.py,sha256=B82j3JEbysLEU9JDZgoDBTX7WGvW3Hn5YMZaWAcjZew,278
10
- bfabric_web_apps/utils/get_logger.py,sha256=0Y3SrXW93--eglS0_ZOc34NOriAt6buFPik5n0ltzRA,434
11
- bfabric_web_apps/utils/get_power_user_wrapper.py,sha256=T33z64XjmJ0KSlmfEmrEP8eYpbpINCVD6Xld_V7PR2g,1027
12
- bfabric_web_apps-0.1.2.dist-info/LICENSE,sha256=k0O_i2k13i9e35aO-j7FerJafAqzzu8x0kkBs0OWF3c,1065
13
- bfabric_web_apps-0.1.2.dist-info/METADATA,sha256=tvvoNH4-tMyPbWJdoe_pVgC_cDzQpDzdGtqd49vt9QI,480
14
- bfabric_web_apps-0.1.2.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
15
- bfabric_web_apps-0.1.2.dist-info/RECORD,,