labdata 0.1.16__tar.gz → 0.1.17__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.
Files changed (57) hide show
  1. {labdata-0.1.16 → labdata-0.1.17}/PKG-INFO +1 -1
  2. {labdata-0.1.16 → labdata-0.1.17}/labdata/__init__.py +1 -1
  3. {labdata-0.1.16 → labdata-0.1.17}/labdata/dashboard/compute.py +49 -20
  4. {labdata-0.1.16 → labdata-0.1.17}/labdata/dashboard/index.py +81 -36
  5. {labdata-0.1.16 → labdata-0.1.17}/labdata/dashboard/notes.py +21 -11
  6. labdata-0.1.17/labdata/dashboard/procedures.py +818 -0
  7. {labdata-0.1.16 → labdata-0.1.17}/labdata/dashboard/segmentation.py +46 -28
  8. {labdata-0.1.16 → labdata-0.1.17}/labdata/dashboard/sorting.py +123 -60
  9. {labdata-0.1.16 → labdata-0.1.17}/labdata/dashboard/video.py +120 -64
  10. {labdata-0.1.16 → labdata-0.1.17}/labdata/schema/ephys.py +54 -20
  11. {labdata-0.1.16 → labdata-0.1.17}/labdata/schema/procedures.py +5 -3
  12. {labdata-0.1.16 → labdata-0.1.17}/labdata/schema/video.py +46 -13
  13. {labdata-0.1.16 → labdata-0.1.17}/labdata.egg-info/PKG-INFO +1 -1
  14. labdata-0.1.16/labdata/dashboard/procedures.py +0 -318
  15. {labdata-0.1.16 → labdata-0.1.17}/LICENSE +0 -0
  16. {labdata-0.1.16 → labdata-0.1.17}/README.md +0 -0
  17. {labdata-0.1.16 → labdata-0.1.17}/labdata/cli.py +0 -0
  18. {labdata-0.1.16 → labdata-0.1.17}/labdata/compute/__init__.py +0 -0
  19. {labdata-0.1.16 → labdata-0.1.17}/labdata/compute/caiman.py +0 -0
  20. {labdata-0.1.16 → labdata-0.1.17}/labdata/compute/ec2.py +0 -0
  21. {labdata-0.1.16 → labdata-0.1.17}/labdata/compute/ephys.py +0 -0
  22. {labdata-0.1.16 → labdata-0.1.17}/labdata/compute/pose.py +0 -0
  23. {labdata-0.1.16 → labdata-0.1.17}/labdata/compute/schedulers.py +0 -0
  24. {labdata-0.1.16 → labdata-0.1.17}/labdata/compute/singularity.py +0 -0
  25. {labdata-0.1.16 → labdata-0.1.17}/labdata/compute/suite2p.py +0 -0
  26. {labdata-0.1.16 → labdata-0.1.17}/labdata/compute/utils.py +0 -0
  27. {labdata-0.1.16 → labdata-0.1.17}/labdata/copy.py +0 -0
  28. {labdata-0.1.16 → labdata-0.1.17}/labdata/dashboard/__init__.py +0 -0
  29. {labdata-0.1.16 → labdata-0.1.17}/labdata/dashboard/admin.py +0 -0
  30. {labdata-0.1.16 → labdata-0.1.17}/labdata/dashboard/ai_utils.py +0 -0
  31. {labdata-0.1.16 → labdata-0.1.17}/labdata/dashboard/console.py +0 -0
  32. {labdata-0.1.16 → labdata-0.1.17}/labdata/dashboard/decisiontask.py +0 -0
  33. {labdata-0.1.16 → labdata-0.1.17}/labdata/dashboard/docstrings.py +0 -0
  34. {labdata-0.1.16 → labdata-0.1.17}/labdata/dashboard/schema_graph.py +0 -0
  35. {labdata-0.1.16 → labdata-0.1.17}/labdata/export.py +0 -0
  36. {labdata-0.1.16 → labdata-0.1.17}/labdata/rules/__init__.py +0 -0
  37. {labdata-0.1.16 → labdata-0.1.17}/labdata/rules/ephys.py +0 -0
  38. {labdata-0.1.16 → labdata-0.1.17}/labdata/rules/imaging.py +0 -0
  39. {labdata-0.1.16 → labdata-0.1.17}/labdata/rules/utils.py +0 -0
  40. {labdata-0.1.16 → labdata-0.1.17}/labdata/s3.py +0 -0
  41. {labdata-0.1.16 → labdata-0.1.17}/labdata/schema/__init__.py +0 -0
  42. {labdata-0.1.16 → labdata-0.1.17}/labdata/schema/general.py +0 -0
  43. {labdata-0.1.16 → labdata-0.1.17}/labdata/schema/histology.py +0 -0
  44. {labdata-0.1.16 → labdata-0.1.17}/labdata/schema/onephoton.py +0 -0
  45. {labdata-0.1.16 → labdata-0.1.17}/labdata/schema/tasks.py +0 -0
  46. {labdata-0.1.16 → labdata-0.1.17}/labdata/schema/twophoton.py +0 -0
  47. {labdata-0.1.16 → labdata-0.1.17}/labdata/schema/utils.py +0 -0
  48. {labdata-0.1.16 → labdata-0.1.17}/labdata/stacks.py +0 -0
  49. {labdata-0.1.16 → labdata-0.1.17}/labdata/utils.py +0 -0
  50. {labdata-0.1.16 → labdata-0.1.17}/labdata/widgets.py +0 -0
  51. {labdata-0.1.16 → labdata-0.1.17}/labdata.egg-info/SOURCES.txt +0 -0
  52. {labdata-0.1.16 → labdata-0.1.17}/labdata.egg-info/dependency_links.txt +0 -0
  53. {labdata-0.1.16 → labdata-0.1.17}/labdata.egg-info/entry_points.txt +0 -0
  54. {labdata-0.1.16 → labdata-0.1.17}/labdata.egg-info/requires.txt +0 -0
  55. {labdata-0.1.16 → labdata-0.1.17}/labdata.egg-info/top_level.txt +0 -0
  56. {labdata-0.1.16 → labdata-0.1.17}/pyproject.toml +0 -0
  57. {labdata-0.1.16 → labdata-0.1.17}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labdata
3
- Version: 0.1.16
3
+ Version: 0.1.17
4
4
  Summary: Package to manage data in experimental neuroscience labs
5
5
  Author-email: Joao Couto <jpcouto@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/jcouto/labdata
@@ -1,4 +1,4 @@
1
- VERSION = "0.1.16"
1
+ VERSION = "0.1.17"
2
2
 
3
3
  from .utils import *
4
4
  from .copy import copy_to_upload_server
@@ -1,6 +1,7 @@
1
1
  import pandas as pd
2
2
  import numpy as np
3
3
  import streamlit as st
4
+ from writes import write, show_flash
4
5
 
5
6
  STATES = ['COMPLETED', 'WORKING', 'WAITING', 'FAILED', 'CANCELED','ON SERVER']
6
7
  STATE_COLORS = ['#01D186', '#48CAE4', '#F3A21C', '#FF5733', '#F3A21C','#F3A21C']
@@ -50,6 +51,21 @@ def _resubmit_jobs(schema, job_ids, project, target_override=None):
50
51
  return errors
51
52
 
52
53
 
54
+ def _fetch_rows(table):
55
+ '''Fetch all rows of a job table, reconnecting once if the first attempt fails.
56
+
57
+ The dashboard shares one database connection between browser sessions, and
58
+ the client can be left half closed (a stale socket, a packet sequence
59
+ error, or ``'NoneType' object has no attribute 'read'`` from pymysql).
60
+ Such failures clear on a fresh connection, so retry once before giving up.
61
+ '''
62
+ try:
63
+ return table.fetch(as_dict=True)
64
+ except Exception:
65
+ table.connection.connect()
66
+ return table.fetch(as_dict=True)
67
+
68
+
53
69
  def _jobs_panel(table, status_field, target_field, cols, delete_key,
54
70
  resubmit_fn=None, allow_delete_completed=False, on_server_fn=None):
55
71
 
@@ -59,20 +75,13 @@ def _jobs_panel(table, status_field, target_field, cols, delete_key,
59
75
  if st.session_state.get(f'{delete_key}_resubmit_errors'):
60
76
  for e in st.session_state.pop(f'{delete_key}_resubmit_errors'):
61
77
  st.error(e)
78
+ show_flash(f'{delete_key}_msg')
62
79
 
63
80
  try:
64
- rows = table.fetch(as_dict=True)
81
+ rows = _fetch_rows(table)
65
82
  except Exception as e:
66
- if 'Packet sequence' in str(e) or 'sequence number wrong' in str(e):
67
- try:
68
- table.connection.connect()
69
- rows = table.fetch(as_dict=True)
70
- except Exception as e2:
71
- st.error(f'Database connection error: {e2}')
72
- return
73
- else:
74
- st.error(f'Error fetching jobs: {e}')
75
- return
83
+ st.error(f'Could not fetch the jobs: {e}')
84
+ return
76
85
  if not rows:
77
86
  st.info('There are no jobs in the **ComputeTask** table.')
78
87
  return
@@ -95,9 +104,14 @@ def _jobs_panel(table, status_field, target_field, cols, delete_key,
95
104
  st.warning(f'Delete all {n} COMPLETED job(s)? This cannot be undone.')
96
105
  col_yes, col_no, _ = st.columns([1, 1, 4])
97
106
  if col_yes.button('Yes, delete', type='primary', key=f'{delete_key}_del_completed_yes'):
98
- (table & f'{status_field} = "COMPLETED"').delete(safemode=False)
107
+ ok, _ = write(table, 'delete the completed jobs',
108
+ lambda: (table & f'{status_field} = "COMPLETED"').delete(
109
+ safemode=False, transaction=False),
110
+ success=f'Deleted {n} completed job(s).',
111
+ flash_key=f'{delete_key}_msg')
99
112
  st.session_state.pop(f'{delete_key}_del_completed_pending', None)
100
- st.rerun()
113
+ if ok:
114
+ st.rerun()
101
115
  if col_no.button('Cancel', key=f'{delete_key}_del_completed_no'):
102
116
  st.session_state.pop(f'{delete_key}_del_completed_pending', None)
103
117
  st.rerun()
@@ -175,9 +189,14 @@ def _jobs_panel(table, status_field, target_field, cols, delete_key,
175
189
  st.warning(f'Delete all {len(jobs_to_delete)} filtered job(s)? This cannot be undone.')
176
190
  col_yes, col_no, _ = st.columns([1, 1, 4])
177
191
  if col_yes.button('Yes, delete', type='primary', key=f'{delete_key}_del_filtered_yes'):
178
- (table & [dict(job_id=j) for j in jobs_to_delete]).delete(safemode=False)
192
+ ok, _ = write(table, 'delete the filtered jobs',
193
+ lambda: (table & [dict(job_id=j) for j in jobs_to_delete]).delete(
194
+ safemode=False, transaction=False),
195
+ success=f'Deleted {len(jobs_to_delete)} job(s).',
196
+ flash_key=f'{delete_key}_msg')
179
197
  st.session_state.pop(f'{delete_key}_del_filtered_pending', None)
180
- st.rerun()
198
+ if ok:
199
+ st.rerun()
181
200
  if col_no.button('Cancel', key=f'{delete_key}_del_filtered_no'):
182
201
  st.session_state.pop(f'{delete_key}_del_filtered_pending', None)
183
202
  st.rerun()
@@ -221,10 +240,14 @@ def _jobs_panel(table, status_field, target_field, cols, delete_key,
221
240
  st.warning(f'Are you sure you want to delete {len(jobs_to_delete)} job(s)? This cannot be undone and scheduled jobs might fail.')
222
241
  col_yes, col_no, _ = st.columns([1, 1, 4])
223
242
  if col_yes.button('Yes, delete', type='primary', key=f'{delete_key}_yes'):
224
- (table & [dict(job_id=j) for j in jobs_to_delete]).delete(safemode=False)
243
+ ok, _ = write(table, 'delete the selected jobs',
244
+ lambda: (table & [dict(job_id=j) for j in jobs_to_delete]).delete(
245
+ safemode=False, transaction=False),
246
+ success=f'Deleted {len(jobs_to_delete)} job(s).',
247
+ flash_key=f'{delete_key}_msg')
225
248
  st.session_state.pop(f'{delete_key}_pending', None)
226
- st.success(f'Deleted {len(jobs_to_delete)} job(s).')
227
- st.rerun()
249
+ if ok:
250
+ st.rerun()
228
251
  if col_no.button('Cancel', key=f'{delete_key}_no'):
229
252
  st.session_state.pop(f'{delete_key}_pending', None)
230
253
  st.rerun()
@@ -245,6 +268,9 @@ def _jobs_panel(table, status_field, target_field, cols, delete_key,
245
268
  st.session_state.pop(f'{delete_key}_resubmit_pending', None)
246
269
  if errors:
247
270
  st.session_state[f'{delete_key}_resubmit_errors'] = errors
271
+ else:
272
+ st.session_state[f'{delete_key}_msg'] = (
273
+ f'Re-submitted {len(jobs_to_resubmit)} job(s).')
248
274
  st.rerun()
249
275
  if col_no.button('Cancel', key=f'{delete_key}_resubmit_no'):
250
276
  st.session_state.pop(f'{delete_key}_resubmit_pending', None)
@@ -255,9 +281,12 @@ def _jobs_panel(table, status_field, target_field, cols, delete_key,
255
281
  st.warning(f'Reset {len(jobs_to_reset)} job(s) to **ON SERVER** / waiting?')
256
282
  col_yes, col_no, _ = st.columns([1, 1, 4])
257
283
  if col_yes.button('Yes, reset', type='primary', key=f'{delete_key}_on_server_yes'):
258
- on_server_fn(jobs_to_reset)
284
+ ok, _ = write(table, 'reset the jobs', lambda: on_server_fn(jobs_to_reset),
285
+ success=f'Reset {len(jobs_to_reset)} job(s) to ON SERVER.',
286
+ flash_key=f'{delete_key}_msg')
259
287
  st.session_state.pop(f'{delete_key}_on_server_pending', None)
260
- st.rerun()
288
+ if ok:
289
+ st.rerun()
261
290
  if col_no.button('Cancel', key=f'{delete_key}_on_server_no'):
262
291
  st.session_state.pop(f'{delete_key}_on_server_pending', None)
263
292
  st.rerun()
@@ -60,6 +60,8 @@ def intro_tab(user_name=None, schema=None):
60
60
  '''Fetch all strain names.'''
61
61
  return list(schema.Strain().fetch('strain_name'))
62
62
 
63
+ from writes import write, show_flash
64
+ show_flash('subjects_msg')
63
65
  list_tab, add_tab, edit_tab, weighing_tab = st.tabs(['Subjects/Sessions', 'New', 'Update', 'Weighing'])
64
66
 
65
67
  with list_tab:
@@ -164,10 +166,18 @@ def intro_tab(user_name=None, schema=None):
164
166
  insert_dict['subject_sex'] = 'U' if sex == 'Unknown' else sex
165
167
  insert_dict['strain_name'] = st.selectbox('Strain', available_strains)
166
168
  if st.form_submit_button('Add Subject', type='primary'):
167
- schema.Subject().insert1(insert_dict)
168
- get_subjects.clear()
169
- st.success(f'Added subject {insert_dict["subject_name"]}.')
170
- st.rerun()
169
+ if not (insert_dict['subject_name'] or '').strip():
170
+ st.error('Give the subject an id.')
171
+ elif not insert_dict['user_name']:
172
+ st.error('Pick a user name.')
173
+ else:
174
+ ok, _ = write(schema.Subject, f'add subject {insert_dict["subject_name"]}',
175
+ lambda: schema.Subject().insert1(insert_dict),
176
+ success=f'Added subject {insert_dict["subject_name"]}.',
177
+ flash_key='subjects_msg')
178
+ if ok:
179
+ get_subjects.clear()
180
+ st.rerun()
171
181
 
172
182
  with edit_tab:
173
183
  users = get_users()
@@ -186,10 +196,13 @@ def intro_tab(user_name=None, schema=None):
186
196
  edit_dict['strain_name'] = st.selectbox('Strain', available_strains,
187
197
  index=available_strains.index(edit_dict['strain_name']))
188
198
  if st.form_submit_button('Save changes', type='primary'):
189
- schema.Subject().update1(edit_dict)
190
- get_subjects.clear()
191
- st.success(f'Updated {selected_subject}.')
192
- st.rerun()
199
+ ok, _ = write(schema.Subject, f'update {selected_subject}',
200
+ lambda: schema.Subject().update1(edit_dict),
201
+ success=f'Updated {selected_subject}.',
202
+ flash_key='subjects_msg')
203
+ if ok:
204
+ get_subjects.clear()
205
+ st.rerun()
193
206
 
194
207
  with weighing_tab:
195
208
  @st.cache_data
@@ -222,13 +235,14 @@ def intro_tab(user_name=None, schema=None):
222
235
  w_weight = st.number_input('Weight (g)', min_value=0.0, step=0.1, format='%.1f')
223
236
  if st.form_submit_button('Add', type='primary'):
224
237
  w_dt = datetime.datetime.combine(w_date, w_time)
225
- try:
226
- schema.Weighing().insert1(dict(subject_name=w_subject, weighing_datetime=w_dt, weight=w_weight))
238
+ ok, _ = write(schema.Weighing, f'add the weight of {w_subject}',
239
+ lambda: schema.Weighing().insert1(dict(
240
+ subject_name=w_subject, weighing_datetime=w_dt, weight=w_weight)),
241
+ success=f'Added {w_weight:.1f} g for {w_subject} at {w_dt}.',
242
+ flash_key='subjects_msg')
243
+ if ok:
227
244
  get_weighings.clear()
228
- st.success(f'Added {w_weight:.1f} g for {w_subject} at {w_dt}.')
229
245
  st.rerun()
230
- except Exception as e:
231
- st.error(str(e))
232
246
 
233
247
  with plot_col:
234
248
  weights_df = get_weighings(w_subject)
@@ -267,16 +281,17 @@ def intro_tab(user_name=None, schema=None):
267
281
  new_weight = st.number_input('New weight (g)', value=float(sel_row['weight']),
268
282
  min_value=0.0, step=0.1, format='%.1f')
269
283
  if st.form_submit_button('Update', type='primary'):
270
- try:
271
- schema.Weighing().update1(dict(
272
- subject_name=w_subject,
273
- weighing_datetime=sel_dt_exact.to_pydatetime(),
274
- weight=new_weight))
284
+ ok, _ = write(schema.Weighing, f'update the weight of {w_subject}',
285
+ lambda: schema.Weighing().update1(dict(
286
+ subject_name=w_subject,
287
+ weighing_datetime=sel_dt_exact.to_pydatetime(),
288
+ weight=new_weight)),
289
+ success=(f'Updated the weight of {w_subject} at '
290
+ f'{sel_dt_exact:%Y-%m-%d %H:%M} to {new_weight:.1f} g.'),
291
+ flash_key='subjects_msg')
292
+ if ok:
275
293
  get_weighings.clear()
276
- st.success(f'Updated to {new_weight:.1f} g.')
277
294
  st.rerun()
278
- except Exception as e:
279
- st.error(str(e))
280
295
  except Exception:
281
296
  pass
282
297
 
@@ -329,6 +344,36 @@ if project is None:
329
344
  project = k.replace('project=','')
330
345
 
331
346
  import time as _time
347
+
348
+
349
+ def _is_db_client_error(exc):
350
+ '''True for a pymysql error or any exception raised from inside pymysql.
351
+
352
+ The dashboard shares one client connection between browser sessions; a half
353
+ closed client raises AttributeError from its socket reader rather than a
354
+ database error, and a fresh connection clears it.'''
355
+ import traceback
356
+ import pymysql
357
+ if isinstance(exc, pymysql.err.Error) or isinstance(getattr(exc, '__cause__', None), pymysql.err.Error):
358
+ return True
359
+ return any('pymysql' in f.filename for f in traceback.extract_tb(exc.__traceback__))
360
+
361
+
362
+ def _reconnect():
363
+ '''Drop the shared client connection and open a new one.'''
364
+ import datajoint as dj
365
+ try:
366
+ conn = dj.conn()
367
+ if conn._conn is not None:
368
+ try:
369
+ conn._conn.close()
370
+ except Exception:
371
+ pass
372
+ conn.connect()
373
+ except Exception:
374
+ pass
375
+
376
+
332
377
  _schema_err = None
333
378
  for _attempt in range(5):
334
379
  try:
@@ -342,6 +387,13 @@ for _attempt in range(5):
342
387
  _schema_err = _e
343
388
  load_schema.clear()
344
389
  _time.sleep(0.3 * (_attempt + 1))
390
+ except Exception as _e:
391
+ if not _is_db_client_error(_e):
392
+ raise
393
+ _schema_err = _e
394
+ load_schema.clear()
395
+ _reconnect()
396
+ _time.sleep(0.3 * (_attempt + 1))
345
397
  if _schema_err is not None:
346
398
  st.error(f'Database connection error after 5 attempts: {_schema_err}. Please refresh the page.')
347
399
  st.stop()
@@ -433,20 +485,13 @@ tab_name = st.session_state['active_tab']
433
485
  try:
434
486
  page_names_to_funcs[tab_name]()
435
487
  except Exception as _e:
436
- import pymysql, datajoint as dj
437
- def _is_mysql_err(exc):
438
- return isinstance(exc, pymysql.err.Error) or (
439
- hasattr(exc, '__cause__') and isinstance(exc.__cause__, pymysql.err.Error))
440
- if _is_mysql_err(_e):
441
- try:
442
- conn = dj.conn()
443
- if conn._conn is not None:
444
- try:
445
- conn._conn.close()
446
- except Exception:
447
- pass
448
- conn.connect()
449
- except Exception:
450
- pass
488
+ # a stale client connection: reconnect and draw again, at most twice in a row
489
+ _retries = st.session_state.get('_db_reconnects', 0)
490
+ if _is_db_client_error(_e) and _retries < 2:
491
+ st.session_state['_db_reconnects'] = _retries + 1
492
+ _reconnect()
451
493
  st.rerun()
494
+ st.session_state.pop('_db_reconnects', None)
452
495
  raise
496
+ else:
497
+ st.session_state.pop('_db_reconnects', None)
@@ -1,6 +1,7 @@
1
1
  import pandas as pd
2
2
  import streamlit as st
3
3
  from datetime import datetime, date, time as dtime
4
+ from writes import write
4
5
 
5
6
 
6
7
  def notes_tab(schema=None):
@@ -161,11 +162,17 @@ def notes_tab(schema=None):
161
162
  note_dt = datetime.combine(note_date, note_time)
162
163
  dataset_key = dict(subject_name=subject_name, session_name=session_name,
163
164
  dataset_name=dataset_name)
164
- schema.Note.insert1(dict(notetaker=notetaker, note_datetime=note_dt, notes=note_text))
165
- schema.Dataset.update1({**dataset_key, 'notetaker': notetaker, 'note_datetime': note_dt})
166
- get_note_counts.clear()
167
- get_subject_notes.clear()
168
- st.success(f'Note {dataset_key} added successfully.')
165
+
166
+ def _add_note():
167
+ # the note and the link from the dataset land together
168
+ schema.Note.insert1(dict(notetaker=notetaker, note_datetime=note_dt, notes=note_text))
169
+ schema.Dataset.update1({**dataset_key, 'notetaker': notetaker, 'note_datetime': note_dt})
170
+
171
+ ok, _ = write(schema.Note, 'add the note', _add_note,
172
+ success=f'Added the note to {subject_name} / {session_name} / {dataset_name}.')
173
+ if ok:
174
+ get_note_counts.clear()
175
+ get_subject_notes.clear()
169
176
 
170
177
  with update_tab:
171
178
  selected = st.session_state.get('selected_note', {})
@@ -188,9 +195,12 @@ def notes_tab(schema=None):
188
195
  if not new_text.strip():
189
196
  st.error('Note text cannot be empty.')
190
197
  else:
191
- schema.Note.update1(dict(notetaker=notetaker_upd, note_datetime=note_dt_upd,
192
- notes=new_text))
193
- get_note_counts.clear()
194
- get_subject_notes.clear()
195
- st.session_state.pop('selected_note', None)
196
- st.success(f'Note {dict(notetaker=notetaker_upd, note_datetime=note_dt_upd)} updated.')
198
+ ok, _ = write(schema.Note, 'update the note',
199
+ lambda: schema.Note.update1(dict(notetaker=notetaker_upd,
200
+ note_datetime=note_dt_upd,
201
+ notes=new_text)),
202
+ success=f'Updated the note by {notetaker_upd} from {note_dt_upd}.')
203
+ if ok:
204
+ get_note_counts.clear()
205
+ get_subject_notes.clear()
206
+ st.session_state.pop('selected_note', None)