QuLab 2.1.0__tar.gz → 2.1.2__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 (97) hide show
  1. {qulab-2.1.0 → qulab-2.1.2}/PKG-INFO +1 -1
  2. {qulab-2.1.0 → qulab-2.1.2}/QuLab.egg-info/PKG-INFO +1 -1
  3. {qulab-2.1.0 → qulab-2.1.2}/QuLab.egg-info/SOURCES.txt +3 -1
  4. qulab-2.1.2/qulab/__init__.py +3 -0
  5. {qulab-2.1.0 → qulab-2.1.2}/qulab/scan/__init__.py +1 -1
  6. qulab-2.1.0/qulab/scan/query_record.py → qulab-2.1.2/qulab/scan/query.py +16 -12
  7. qulab-2.1.0/qulab/scan/recorder.py → qulab-2.1.2/qulab/scan/record.py +70 -303
  8. qulab-2.1.2/qulab/scan/recorder.py +247 -0
  9. {qulab-2.1.0 → qulab-2.1.2}/qulab/scan/scan.py +121 -147
  10. {qulab-2.1.0 → qulab-2.1.2}/qulab/scan/server.py +4 -6
  11. qulab-2.1.2/qulab/scan/space.py +172 -0
  12. {qulab-2.1.0 → qulab-2.1.2}/qulab/scan/utils.py +48 -0
  13. qulab-2.1.2/qulab/version.py +1 -0
  14. qulab-2.1.0/qulab/__init__.py +0 -1
  15. qulab-2.1.0/qulab/version.py +0 -1
  16. {qulab-2.1.0 → qulab-2.1.2}/LICENSE +0 -0
  17. {qulab-2.1.0 → qulab-2.1.2}/MANIFEST.in +0 -0
  18. {qulab-2.1.0 → qulab-2.1.2}/QuLab.egg-info/dependency_links.txt +0 -0
  19. {qulab-2.1.0 → qulab-2.1.2}/QuLab.egg-info/entry_points.txt +0 -0
  20. {qulab-2.1.0 → qulab-2.1.2}/QuLab.egg-info/requires.txt +0 -0
  21. {qulab-2.1.0 → qulab-2.1.2}/QuLab.egg-info/top_level.txt +0 -0
  22. {qulab-2.1.0 → qulab-2.1.2}/README.md +0 -0
  23. {qulab-2.1.0 → qulab-2.1.2}/pyproject.toml +0 -0
  24. {qulab-2.1.0 → qulab-2.1.2}/qulab/__main__.py +0 -0
  25. {qulab-2.1.0 → qulab-2.1.2}/qulab/monitor/__init__.py +0 -0
  26. {qulab-2.1.0 → qulab-2.1.2}/qulab/monitor/__main__.py +0 -0
  27. {qulab-2.1.0 → qulab-2.1.2}/qulab/monitor/config.py +0 -0
  28. {qulab-2.1.0 → qulab-2.1.2}/qulab/monitor/dataset.py +0 -0
  29. {qulab-2.1.0 → qulab-2.1.2}/qulab/monitor/event_queue.py +0 -0
  30. {qulab-2.1.0 → qulab-2.1.2}/qulab/monitor/mainwindow.py +0 -0
  31. {qulab-2.1.0 → qulab-2.1.2}/qulab/monitor/monitor.py +0 -0
  32. {qulab-2.1.0 → qulab-2.1.2}/qulab/monitor/ploter.py +0 -0
  33. {qulab-2.1.0 → qulab-2.1.2}/qulab/monitor/qt_compat.py +0 -0
  34. {qulab-2.1.0 → qulab-2.1.2}/qulab/monitor/toolbar.py +0 -0
  35. {qulab-2.1.0 → qulab-2.1.2}/qulab/scan/curd.py +0 -0
  36. {qulab-2.1.0 → qulab-2.1.2}/qulab/scan/expression.py +0 -0
  37. {qulab-2.1.0 → qulab-2.1.2}/qulab/scan/models.py +0 -0
  38. {qulab-2.1.0 → qulab-2.1.2}/qulab/scan/optimize.py +0 -0
  39. {qulab-2.1.0 → qulab-2.1.2}/qulab/storage/__init__.py +0 -0
  40. {qulab-2.1.0 → qulab-2.1.2}/qulab/storage/__main__.py +0 -0
  41. {qulab-2.1.0 → qulab-2.1.2}/qulab/storage/backend/__init__.py +0 -0
  42. {qulab-2.1.0 → qulab-2.1.2}/qulab/storage/backend/redis.py +0 -0
  43. {qulab-2.1.0 → qulab-2.1.2}/qulab/storage/base_dataset.py +0 -0
  44. {qulab-2.1.0 → qulab-2.1.2}/qulab/storage/chunk.py +0 -0
  45. {qulab-2.1.0 → qulab-2.1.2}/qulab/storage/dataset.py +0 -0
  46. {qulab-2.1.0 → qulab-2.1.2}/qulab/storage/file.py +0 -0
  47. {qulab-2.1.0 → qulab-2.1.2}/qulab/storage/models/__init__.py +0 -0
  48. {qulab-2.1.0 → qulab-2.1.2}/qulab/storage/models/base.py +0 -0
  49. {qulab-2.1.0 → qulab-2.1.2}/qulab/storage/models/config.py +0 -0
  50. {qulab-2.1.0 → qulab-2.1.2}/qulab/storage/models/file.py +0 -0
  51. {qulab-2.1.0 → qulab-2.1.2}/qulab/storage/models/ipy.py +0 -0
  52. {qulab-2.1.0 → qulab-2.1.2}/qulab/storage/models/models.py +0 -0
  53. {qulab-2.1.0 → qulab-2.1.2}/qulab/storage/models/record.py +0 -0
  54. {qulab-2.1.0 → qulab-2.1.2}/qulab/storage/models/report.py +0 -0
  55. {qulab-2.1.0 → qulab-2.1.2}/qulab/storage/models/tag.py +0 -0
  56. {qulab-2.1.0 → qulab-2.1.2}/qulab/storage/storage.py +0 -0
  57. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/__init__.py +0 -0
  58. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/chat.py +0 -0
  59. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/device/__init__.py +0 -0
  60. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/device/basedevice.py +0 -0
  61. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/device/loader.py +0 -0
  62. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/device/utils.py +0 -0
  63. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/drivers/FakeInstrument.py +0 -0
  64. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/drivers/__init__.py +0 -0
  65. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/ipy_events.py +0 -0
  66. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/net/__init__.py +0 -0
  67. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/net/bencoder.py +0 -0
  68. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/net/cli.py +0 -0
  69. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/net/dhcp.py +0 -0
  70. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/net/dhcpd.py +0 -0
  71. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/net/kad.py +0 -0
  72. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/net/kcp.py +0 -0
  73. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/net/nginx.py +0 -0
  74. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/progress.py +0 -0
  75. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/rpc/__init__.py +0 -0
  76. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/rpc/client.py +0 -0
  77. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/rpc/exceptions.py +0 -0
  78. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/rpc/msgpack.py +0 -0
  79. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/rpc/msgpack.pyi +0 -0
  80. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/rpc/rpc.py +0 -0
  81. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/rpc/serialize.py +0 -0
  82. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/rpc/server.py +0 -0
  83. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/rpc/socket.py +0 -0
  84. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/rpc/utils.py +0 -0
  85. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/rpc/worker.py +0 -0
  86. {qulab-2.1.0 → qulab-2.1.2}/qulab/sys/rpc/zmq_socket.py +0 -0
  87. {qulab-2.1.0 → qulab-2.1.2}/qulab/visualization/__init__.py +0 -0
  88. {qulab-2.1.0 → qulab-2.1.2}/qulab/visualization/__main__.py +0 -0
  89. {qulab-2.1.0 → qulab-2.1.2}/qulab/visualization/_autoplot.py +0 -0
  90. {qulab-2.1.0 → qulab-2.1.2}/qulab/visualization/plot_layout.py +0 -0
  91. {qulab-2.1.0 → qulab-2.1.2}/qulab/visualization/plot_seq.py +0 -0
  92. {qulab-2.1.0 → qulab-2.1.2}/qulab/visualization/qdat.py +0 -0
  93. {qulab-2.1.0 → qulab-2.1.2}/qulab/visualization/widgets.py +0 -0
  94. {qulab-2.1.0 → qulab-2.1.2}/setup.cfg +0 -0
  95. {qulab-2.1.0 → qulab-2.1.2}/setup.py +0 -0
  96. {qulab-2.1.0 → qulab-2.1.2}/src/qulab.h +0 -0
  97. {qulab-2.1.0 → qulab-2.1.2}/tests/test_scan.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: QuLab
3
- Version: 2.1.0
3
+ Version: 2.1.2
4
4
  Summary: contral instruments and manage data
5
5
  Author-email: feihoo87 <feihoo87@gmail.com>
6
6
  Maintainer-email: feihoo87 <feihoo87@gmail.com>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: QuLab
3
- Version: 2.1.0
3
+ Version: 2.1.2
4
4
  Summary: contral instruments and manage data
5
5
  Author-email: feihoo87 <feihoo87@gmail.com>
6
6
  Maintainer-email: feihoo87 <feihoo87@gmail.com>
@@ -27,10 +27,12 @@ qulab/scan/curd.py
27
27
  qulab/scan/expression.py
28
28
  qulab/scan/models.py
29
29
  qulab/scan/optimize.py
30
- qulab/scan/query_record.py
30
+ qulab/scan/query.py
31
+ qulab/scan/record.py
31
32
  qulab/scan/recorder.py
32
33
  qulab/scan/scan.py
33
34
  qulab/scan/server.py
35
+ qulab/scan/space.py
34
36
  qulab/scan/utils.py
35
37
  qulab/storage/__init__.py
36
38
  qulab/storage/__main__.py
@@ -0,0 +1,3 @@
1
+ from .scan import Scan, get_record, load_record, lookup, lookup_list
2
+ from .version import __version__
3
+ from .visualization import autoplot
@@ -1,3 +1,3 @@
1
1
  from .expression import Expression, Symbol
2
- from .query_record import get_record, lookup, lookup_list
2
+ from .query import get_record, load_record, lookup, lookup_list
3
3
  from .scan import Scan
@@ -6,13 +6,17 @@ import dill
6
6
  import ipywidgets as widgets
7
7
  import zmq
8
8
  from IPython.display import display
9
+ from sqlalchemy import create_engine
10
+ from sqlalchemy.orm import sessionmaker
9
11
 
10
12
  from qulab.sys.rpc.zmq_socket import ZMQContextManager
11
13
 
12
- from .recorder import Record
14
+ from .record import Record
15
+ from .recorder import get_local_record
16
+ from .scan import default_server
13
17
 
14
18
 
15
- def get_record(id, database='tcp://127.0.0.1:6789'):
19
+ def get_record(id, database=default_server) -> Record:
16
20
  if isinstance(database, str) and database.startswith('tcp://'):
17
21
  with ZMQContextManager(zmq.DEALER, connect=database) as socket:
18
22
  socket.send_pyobj({
@@ -20,20 +24,20 @@ def get_record(id, database='tcp://127.0.0.1:6789'):
20
24
  'record_id': id
21
25
  })
22
26
  d = dill.loads(socket.recv_pyobj())
23
- return Record(id, database, d)
27
+ d.id = id
28
+ d.database = database
29
+ d._file = None
30
+ return d
24
31
  else:
25
- from .models import Record as RecordInDB
26
- from .models import create_engine, sessionmaker
27
-
28
32
  db_file = Path(database) / 'data.db'
29
33
  engine = create_engine(f'sqlite:///{db_file}')
30
34
  Session = sessionmaker(bind=engine)
31
35
  with Session() as session:
32
- path = Path(database) / 'objects' / session.get(RecordInDB,
33
- id).file
34
- with open(path, 'rb') as f:
35
- record = dill.load(f)
36
- return record
36
+ return get_local_record(session, id, database)
37
+
38
+
39
+ def load_record(file):
40
+ return Record.load(file)
37
41
 
38
42
 
39
43
  def _format_tag(tag):
@@ -292,7 +296,7 @@ def _on_tags_submit(tags, ui_widgets):
292
296
  _update_view(ui_widgets)
293
297
 
294
298
 
295
- def lookup(app=None, limit=10, database='tcp://127.0.0.1:6789'):
299
+ def lookup(app=None, limit=10, database=default_server):
296
300
  after = widgets.DatePicker()
297
301
  before = widgets.DatePicker()
298
302
  app_prefix = widgets.Label('App:')
@@ -1,41 +1,20 @@
1
- import asyncio
2
1
  import itertools
3
- import os
4
- import pickle
5
2
  import sys
6
- import time
7
3
  import uuid
8
- from collections import defaultdict
4
+ import zipfile
9
5
  from pathlib import Path
10
6
  from threading import Lock
11
7
  from types import EllipsisType
12
8
 
13
- import click
14
9
  import dill
15
10
  import numpy as np
16
11
  import zmq
17
- from loguru import logger
18
12
 
19
13
  from qulab.sys.rpc.zmq_socket import ZMQContextManager
20
14
 
21
- from .curd import query_record, remove_tags, tag, update_tags
22
- from .models import Record as RecordInDB
23
- from .models import Session, create_engine, create_tables, sessionmaker, utcnow
15
+ from .space import OptimizeSpace, Space
24
16
 
25
- _notgiven = object()
26
-
27
- try:
28
- default_record_port = int(os.getenv('QULAB_RECORD_PORT', 6789))
29
- except:
30
- default_record_port = 6789
31
-
32
- if os.getenv('QULAB_RECORD_PATH'):
33
- datapath = Path(os.getenv('QULAB_RECORD_PATH'))
34
- else:
35
- datapath = Path.home() / 'qulab' / 'data'
36
- datapath.mkdir(parents=True, exist_ok=True)
37
-
38
- record_cache = {}
17
+ _not_given = object()
39
18
 
40
19
 
41
20
  def random_path(base):
@@ -142,6 +121,18 @@ class BufferList():
142
121
  yield pos, value
143
122
  except EOFError:
144
123
  break
124
+ elif isinstance(
125
+ self.file, tuple) and len(self.file) == 2 and isinstance(
126
+ self.file[0], str) and self.file[0].endswith('.zip'):
127
+ f, name = self.file
128
+ with zipfile.ZipFile(f, 'r') as z:
129
+ with z.open(name, 'r') as f:
130
+ while True:
131
+ try:
132
+ pos, value = dill.load(f)
133
+ yield pos, value
134
+ except EOFError:
135
+ break
145
136
 
146
137
  def iter(self):
147
138
  if self._data_id is None:
@@ -297,51 +288,13 @@ class BufferList():
297
288
  class Record():
298
289
 
299
290
  def __init__(self, id, database, description=None):
300
- from .scan import OptimizeSpace
301
-
302
291
  self.id = id
303
292
  self.database = database
304
293
  self.description = description
305
- self._keys = set()
306
294
  self._items = {}
307
- self._index = []
308
295
  self._pos = []
309
296
  self._last_vars = set()
310
297
  self._file = None
311
- self.independent_variables = {}
312
- self.constants = {}
313
- self.dims = {}
314
-
315
- for name, value in self.description['consts'].items():
316
- if name not in self._items:
317
- self._items[name] = value
318
- self.constants[name] = value
319
- self.dims[name] = ()
320
- for level, range_list in self.description['loops'].items():
321
- for name, iterable in range_list:
322
- if isinstance(iterable, OptimizeSpace):
323
- self.dims[name] = tuple(range(level + 1))
324
- continue
325
- elif isinstance(iterable, (np.ndarray, list, tuple, range)):
326
- self._items[name] = iterable
327
- self.independent_variables[name] = iterable
328
- self.dims[name] = (level, )
329
-
330
- for level, group in self.description['order'].items():
331
- for names in group:
332
- for name in names:
333
- if name not in self.description['dependents']:
334
- if name not in self.dims:
335
- self.dims[name] = (level, )
336
- else:
337
- d = set()
338
- for n in self.description['dependents'][name]:
339
- d.update(self.dims[n])
340
- if name not in self.dims:
341
- self.dims[name] = tuple(sorted(d))
342
- else:
343
- self.dims[name] = tuple(
344
- sorted(set(self.dims[name]) | d))
345
298
 
346
299
  if self.is_local_record():
347
300
  self.database = Path(self.database)
@@ -351,32 +304,23 @@ class Record():
351
304
  def __getstate__(self) -> dict:
352
305
  return {
353
306
  'id': self.id,
354
- 'database': self.database,
355
307
  'description': self.description,
356
- '_keys': self._keys,
357
308
  '_items': self._items,
358
- '_index': self._index,
359
- '_pos': self._pos,
360
- '_last_vars': self._last_vars,
361
- 'independent_variables': self.independent_variables,
362
- 'constants': self.constants,
363
- 'dims': self.dims,
364
309
  }
365
310
 
366
311
  def __setstate__(self, state: dict):
367
312
  self.id = state['id']
368
- self.database = state['database']
369
313
  self.description = state['description']
370
- self._keys = state['_keys']
371
314
  self._items = state['_items']
372
- self._index = state['_index']
373
- self._pos = state['_pos']
374
- self._last_vars = state['_last_vars']
375
- self.independent_variables = state['independent_variables']
376
- self.constants = state['constants']
377
- self.dims = state['dims']
315
+ self._pos = []
316
+ self._last_vars = set()
317
+ self.database = None
378
318
  self._file = None
379
319
 
320
+ @property
321
+ def axis(self):
322
+ return self.description.get('axis', {})
323
+
380
324
  def is_local_record(self):
381
325
  return not self.is_cache_record() and not self.is_remote_record()
382
326
 
@@ -391,9 +335,12 @@ class Record():
391
335
  self.flush()
392
336
 
393
337
  def __getitem__(self, key):
394
- return self.get(key, buffer_to_array=True)
338
+ ret = self.get(key, buffer_to_array=True)
339
+ if isinstance(ret, Space):
340
+ ret = ret.toarray()
341
+ return ret
395
342
 
396
- def get(self, key, default=_notgiven, buffer_to_array=False, slice=None):
343
+ def get(self, key, default=_not_given, buffer_to_array=False, slice=None):
397
344
  if self.is_remote_record():
398
345
  with ZMQContextManager(zmq.DEALER,
399
346
  connect=self.database) as socket:
@@ -421,7 +368,7 @@ class Record():
421
368
  else:
422
369
  return ret
423
370
  else:
424
- if default is _notgiven:
371
+ if default is _not_given:
425
372
  d = self._items.get(key)
426
373
  else:
427
374
  d = self._items.get(key, default)
@@ -446,7 +393,7 @@ class Record():
446
393
  })
447
394
  return socket.recv_pyobj()
448
395
  else:
449
- return list(self._keys)
396
+ return list(self._items.keys())
450
397
 
451
398
  def append(self, level, step, position, variables):
452
399
  if level < 0:
@@ -454,34 +401,29 @@ class Record():
454
401
  return
455
402
 
456
403
  for key in set(variables.keys()) - self._last_vars:
457
- if key not in self.dims:
458
- self.dims[key] = tuple(range(level + 1))
404
+ if key not in self.axis:
405
+ self.axis[key] = tuple(range(level + 1))
459
406
 
460
407
  self._last_vars = set(variables.keys())
461
- self._keys.update(variables.keys())
462
408
 
463
409
  if level >= len(self._pos):
464
410
  l = level + 1 - len(self._pos)
465
- self._index.extend(([0] * (l - 1)) + [step])
466
411
  self._pos.extend(([0] * (l - 1)) + [position])
467
412
  pos = tuple(self._pos)
468
413
  elif level == len(self._pos) - 1:
469
- self._index[-1] = step
470
414
  self._pos[-1] = position
471
415
  pos = tuple(self._pos)
472
416
  else:
473
- self._index = self._index[:level + 1]
474
417
  self._pos = self._pos[:level + 1]
475
- self._index[-1] = step + 1
476
418
  self._pos[-1] = position
477
419
  pos = tuple(self._pos)
478
420
  self._pos[-1] += 1
479
421
 
480
422
  for key, value in variables.items():
481
- if self.dims[key] == ():
423
+ if self.axis[key] == ():
482
424
  if key not in self._items:
483
425
  self._items[key] = value
484
- elif level == self.dims[key][-1]:
426
+ elif level == self.axis[key][-1]:
485
427
  if key not in self._items:
486
428
  if self.is_local_record():
487
429
  bufferlist_file = random_path(self.database /
@@ -493,9 +435,9 @@ class Record():
493
435
  self._items[key] = BufferList()
494
436
  self._items[key].lu = pos
495
437
  self._items[key].rd = tuple([i + 1 for i in pos])
496
- self._items[key].append(pos, value, self.dims[key])
438
+ self._items[key].append(pos, value, self.axis[key])
497
439
  elif isinstance(self._items[key], BufferList):
498
- self._items[key].append(pos, value, self.dims[key])
440
+ self._items[key].append(pos, value, self.axis[key])
499
441
 
500
442
  def flush(self):
501
443
  if self.is_remote_record() or self.is_cache_record():
@@ -508,6 +450,40 @@ class Record():
508
450
  with open(self._file, 'wb') as f:
509
451
  dill.dump(self, f)
510
452
 
453
+ def export(self, file):
454
+ with zipfile.ZipFile(file,
455
+ 'w',
456
+ compression=zipfile.ZIP_DEFLATED,
457
+ compresslevel=9) as z:
458
+ items = {}
459
+ for key in self.keys():
460
+ value = self.get(key)
461
+ if isinstance(value, BufferList):
462
+ v = BufferList()
463
+ v.lu = value.lu
464
+ v.rd = value.rd
465
+ v.inner_shape = value.inner_shape
466
+ items[key] = v
467
+ with z.open(f'{key}.buf', 'w') as f:
468
+ for pos, data in value.iter():
469
+ dill.dump((pos, data), f)
470
+ else:
471
+ items[key] = value
472
+ with z.open('record.pkl', 'w') as f:
473
+ dill.dump((self.description, items), f)
474
+
475
+ @classmethod
476
+ def load(cls, file: str):
477
+ with zipfile.ZipFile(file, 'r') as z:
478
+ with z.open('record.pkl', 'r') as f:
479
+ description, items = dill.load(f)
480
+ record = cls(None, None, description)
481
+ for key, value in items.items():
482
+ if isinstance(value, BufferList):
483
+ value.file = file, f'{key}.buf'
484
+ record._items[key] = value
485
+ return record
486
+
511
487
  def __repr__(self):
512
488
  return f"<Record: id={self.id} app={self.description['app']}, keys={self.keys()}>"
513
489
 
@@ -515,214 +491,5 @@ class Record():
515
491
  # return f"""
516
492
  # <h3>Record: id={self.id}, app={self.description['app']}</h3>
517
493
  # <p>keys={self.keys()}</p>
518
- # <p>dims={self.dims}</p>
494
+ # <p>axis={self.axis}</p>
519
495
  # """
520
-
521
-
522
- class Request():
523
- __slots__ = ['sock', 'identity', 'msg', 'method']
524
-
525
- def __init__(self, sock, identity, msg):
526
- self.sock = sock
527
- self.identity = identity
528
- self.msg = pickle.loads(msg)
529
- self.method = self.msg.get('method', '')
530
-
531
-
532
- async def reply(req, resp):
533
- await req.sock.send_multipart([req.identity, pickle.dumps(resp)])
534
-
535
-
536
- def clear_cache():
537
- if len(record_cache) < 1024:
538
- return
539
-
540
- for k, (t, _) in zip(sorted(record_cache.items(), key=lambda x: x[1][0]),
541
- range(len(record_cache) - 1024)):
542
- del record_cache[k]
543
-
544
-
545
- def flush_cache():
546
- for k, (t, r) in record_cache.items():
547
- r.flush()
548
-
549
-
550
- def get_record(session: Session, id: int, datapath: Path) -> Record:
551
- if id not in record_cache:
552
- record_in_db = session.get(RecordInDB, id)
553
- record_in_db.atime = utcnow()
554
- path = datapath / 'objects' / record_in_db.file
555
- with open(path, 'rb') as f:
556
- record = dill.load(f)
557
- record._file = path
558
- else:
559
- record = record_cache[id][1]
560
- clear_cache()
561
- record_cache[id] = time.time(), record
562
- return record
563
-
564
-
565
- def record_create(session: Session, description: dict, datapath: Path) -> int:
566
- record = Record(None, datapath, description)
567
- record_in_db = RecordInDB()
568
- if 'app' in description:
569
- record_in_db.app = description['app']
570
- if 'tags' in description:
571
- record_in_db.tags = [tag(session, t) for t in description['tags']]
572
- record_in_db.file = '/'.join(record._file.parts[-4:])
573
- session.add(record_in_db)
574
- try:
575
- session.commit()
576
- record.id = record_in_db.id
577
- clear_cache()
578
- record_cache[record.id] = time.time(), record
579
- return record.id
580
- except:
581
- session.rollback()
582
- raise
583
-
584
-
585
- def record_append(session: Session, record_id: int, level: int, step: int,
586
- position: int, variables: dict, datapath: Path):
587
- record = get_record(session, record_id, datapath)
588
- record.append(level, step, position, variables)
589
- try:
590
- record_in_db = session.get(RecordInDB, record_id)
591
- record_in_db.mtime = utcnow()
592
- record_in_db.atime = utcnow()
593
- session.commit()
594
- except:
595
- session.rollback()
596
- raise
597
-
598
-
599
- @logger.catch
600
- async def handle(session: Session, request: Request, datapath: Path):
601
-
602
- msg = request.msg
603
-
604
- match request.method:
605
- case 'ping':
606
- await reply(request, 'pong')
607
- case 'bufferlist_slice':
608
- record = get_record(session, msg['record_id'], datapath)
609
- bufferlist = record.get(msg['key'],
610
- buffer_to_array=False,
611
- slice=msg['slice'])
612
- await reply(request, list(bufferlist.iter()))
613
- case 'record_create':
614
- description = dill.loads(msg['description'])
615
- await reply(request, record_create(session, description, datapath))
616
- case 'record_append':
617
- record_append(session, msg['record_id'], msg['level'], msg['step'],
618
- msg['position'], msg['variables'], datapath)
619
- case 'record_description':
620
- record = get_record(session, msg['record_id'], datapath)
621
- await reply(request, dill.dumps(record.description))
622
- case 'record_getitem':
623
- record = get_record(session, msg['record_id'], datapath)
624
- await reply(request, record.get(msg['key'], buffer_to_array=False))
625
- case 'record_keys':
626
- record = get_record(session, msg['record_id'], datapath)
627
- await reply(request, record.keys())
628
- case 'record_query':
629
- total, apps, table = query_record(session,
630
- offset=msg.get('offset', 0),
631
- limit=msg.get('limit', 10),
632
- app=msg.get('app', None),
633
- tags=msg.get('tags', ()),
634
- before=msg.get('before', None),
635
- after=msg.get('after', None))
636
- await reply(request, (total, apps, table))
637
- case 'record_get_tags':
638
- record_in_db = session.get(RecordInDB, msg['record_id'])
639
- await reply(request, [t.name for t in record_in_db.tags])
640
- case 'record_remove_tags':
641
- remove_tags(session, msg['record_id'], msg['tags'])
642
- case 'record_add_tags':
643
- update_tags(session, msg['record_id'], msg['tags'], True)
644
- case 'record_replace_tags':
645
- update_tags(session, msg['record_id'], msg['tags'], False)
646
- case _:
647
- logger.error(f"Unknown method: {msg['method']}")
648
-
649
-
650
- async def _handle(session: Session, request: Request, datapath: Path):
651
- try:
652
- await handle(session, request, datapath)
653
- except:
654
- await reply(request, 'error')
655
-
656
-
657
- async def serv(port,
658
- datapath,
659
- url=None,
660
- buffer_size=1024 * 1024 * 1024,
661
- interval=60):
662
- logger.info('Server starting.')
663
- async with ZMQContextManager(zmq.ROUTER, bind=f"tcp://*:{port}") as sock:
664
- if url is None:
665
- url = 'sqlite:///' + str(datapath / 'data.db')
666
- engine = create_engine(url)
667
- create_tables(engine)
668
- Session = sessionmaker(engine)
669
- with Session() as session:
670
- logger.info('Server started.')
671
- received = 0
672
- last_flush_time = time.time()
673
- while True:
674
- identity, msg = await sock.recv_multipart()
675
- received += len(msg)
676
- req = Request(sock, identity, msg)
677
- asyncio.create_task(_handle(session, req, datapath))
678
- if received > buffer_size or time.time(
679
- ) - last_flush_time > interval:
680
- flush_cache()
681
- received = 0
682
- last_flush_time = time.time()
683
-
684
-
685
- async def watch(port, datapath, url=None, timeout=1, buffer=1024, interval=60):
686
- with ZMQContextManager(zmq.DEALER,
687
- connect=f"tcp://127.0.0.1:{port}") as sock:
688
- sock.setsockopt(zmq.LINGER, 0)
689
- while True:
690
- try:
691
- sock.send_pyobj({"method": "ping"})
692
- if sock.poll(int(1000 * timeout)):
693
- sock.recv()
694
- else:
695
- raise asyncio.TimeoutError()
696
- except (zmq.error.ZMQError, asyncio.TimeoutError):
697
- return asyncio.create_task(
698
- serv(port, datapath, url, buffer * 1024 * 1024, interval))
699
- await asyncio.sleep(timeout)
700
-
701
-
702
- async def main(port, datapath, url, timeout=1, buffer=1024, interval=60):
703
- task = await watch(port=port,
704
- datapath=datapath,
705
- url=url,
706
- timeout=timeout,
707
- buffer=buffer,
708
- interval=interval)
709
- await task
710
-
711
-
712
- @click.command()
713
- @click.option('--port',
714
- default=os.getenv('QULAB_RECORD_PORT', 6789),
715
- help='Port of the server.')
716
- @click.option('--datapath', default=datapath, help='Path of the data.')
717
- @click.option('--url', default=None, help='URL of the database.')
718
- @click.option('--timeout', default=1, help='Timeout of ping.')
719
- @click.option('--buffer', default=1024, help='Buffer size (MB).')
720
- @click.option('--interval',
721
- default=60,
722
- help='Interval of flush cache, in unit of second.')
723
- def record(port, datapath, url, timeout, buffer, interval):
724
- asyncio.run(main(port, Path(datapath), url, timeout, buffer, interval))
725
-
726
-
727
- if __name__ == "__main__":
728
- record()