cicwave 0.7.0__tar.gz → 0.7.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 (23) hide show
  1. {cicwave-0.7.0/src/cicwave.egg-info → cicwave-0.7.2}/PKG-INFO +1 -1
  2. {cicwave-0.7.0 → cicwave-0.7.2}/pyproject.toml +1 -1
  3. {cicwave-0.7.0 → cicwave-0.7.2}/src/cicwave/cli.py +27 -5
  4. {cicwave-0.7.0 → cicwave-0.7.2}/src/cicwave/wave_pg.py +60 -10
  5. {cicwave-0.7.0 → cicwave-0.7.2}/src/cicwave/wavefiles.py +12 -0
  6. {cicwave-0.7.0 → cicwave-0.7.2/src/cicwave.egg-info}/PKG-INFO +1 -1
  7. {cicwave-0.7.0 → cicwave-0.7.2}/LICENSE +0 -0
  8. {cicwave-0.7.0 → cicwave-0.7.2}/README.md +0 -0
  9. {cicwave-0.7.0 → cicwave-0.7.2}/setup.cfg +0 -0
  10. {cicwave-0.7.0 → cicwave-0.7.2}/src/cicwave/__init__.py +0 -0
  11. {cicwave-0.7.0 → cicwave-0.7.2}/src/cicwave/analysis.py +0 -0
  12. {cicwave-0.7.0 → cicwave-0.7.2}/src/cicwave/apisource.py +0 -0
  13. {cicwave-0.7.0 → cicwave-0.7.2}/src/cicwave/command.py +0 -0
  14. {cicwave-0.7.0 → cicwave-0.7.2}/src/cicwave/mcp_server.py +0 -0
  15. {cicwave-0.7.0 → cicwave-0.7.2}/src/cicwave/ngraw.py +0 -0
  16. {cicwave-0.7.0 → cicwave-0.7.2}/src/cicwave/pivot.py +0 -0
  17. {cicwave-0.7.0 → cicwave-0.7.2}/src/cicwave/stdf.py +0 -0
  18. {cicwave-0.7.0 → cicwave-0.7.2}/src/cicwave/theme.py +0 -0
  19. {cicwave-0.7.0 → cicwave-0.7.2}/src/cicwave.egg-info/SOURCES.txt +0 -0
  20. {cicwave-0.7.0 → cicwave-0.7.2}/src/cicwave.egg-info/dependency_links.txt +0 -0
  21. {cicwave-0.7.0 → cicwave-0.7.2}/src/cicwave.egg-info/entry_points.txt +0 -0
  22. {cicwave-0.7.0 → cicwave-0.7.2}/src/cicwave.egg-info/requires.txt +0 -0
  23. {cicwave-0.7.0 → cicwave-0.7.2}/src/cicwave.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cicwave
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: Advanced waveform viewer with PyQtGraph backend
5
5
  Author-email: Carsten Wulff <carsten@wulff.no>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "cicwave"
7
- version = "0.7.0"
7
+ version = "0.7.2"
8
8
  authors = [
9
9
  { name="Carsten Wulff", email="carsten@wulff.no" },
10
10
  ]
@@ -30,6 +30,7 @@ import os
30
30
  import re
31
31
  import sys
32
32
  import glob as _glob
33
+ import urllib.parse
33
34
  import click
34
35
  from .command import setup_logging
35
36
 
@@ -111,6 +112,23 @@ def _expand_glob_patterns(files, patterns):
111
112
  return tuple(out)
112
113
 
113
114
 
115
+ def _spec_path_of(path):
116
+ """Lower-cased path part of *path*, with any URL query string cut off."""
117
+ if _URL_RE.match(path):
118
+ return urllib.parse.urlsplit(path).path.lower()
119
+ return path.lower()
120
+
121
+
122
+ def _spec_ref(path):
123
+ """How a spec is recorded for a session: absolute path, or URL as-is.
124
+
125
+ ``os.path.abspath`` on an ``http://`` URL yields a local path that
126
+ names nothing, so a session written from a URL spec could not be
127
+ read back. Mirrors what ``PgWaveWindow.openSourceSpec`` does.
128
+ """
129
+ return path if _URL_RE.match(path) else os.path.abspath(path)
130
+
131
+
114
132
  def _is_source_spec_file(path):
115
133
  """True for a YAML/JSON pivot spec that fetches its own data.
116
134
 
@@ -126,8 +144,11 @@ def _is_source_spec_file(path):
126
144
  """
127
145
  #- Extension check first, so the pandas/Qt import chain behind
128
146
  #- apisource is paid for only when a YAML argument is actually given,
129
- #- and so a plain data URL is never fetched twice.
130
- if not path.lower().endswith((".yaml", ".yml")):
147
+ #- and so a plain data URL is never fetched twice. Test it against
148
+ #- the URL's path, not the whole URL: a generated spec is served at
149
+ #- something like `/spec.yaml?dut=X`, and the rest of cicwave already
150
+ #- ignores the query string when resolving a format.
151
+ if not _spec_path_of(path).endswith((".yaml", ".yml")):
131
152
  return False
132
153
  from .apisource import is_source_spec_file
133
154
  return is_source_spec_file(path)
@@ -199,7 +220,7 @@ def _run_catalog_spec(pivot_spec, spec, x, pivot_info_flag,
199
220
  c.win.applySession(session_path)
200
221
  c.win.browser.openLazyFrame(
201
222
  os.path.basename(pivot_spec), groups, catalog.load_group,
202
- pivot_spec_path=os.path.abspath(pivot_spec))
223
+ pivot_spec_path=_spec_ref(pivot_spec))
203
224
  c.run()
204
225
 
205
226
 
@@ -309,7 +330,7 @@ def _run_wave_pg(files, x, sheet, pivot_spec=None,
309
330
  name = "pivot(%s)" % os.path.basename(f)
310
331
  c.openDataFrame(
311
332
  pivoted, name,
312
- pivot_spec_path=os.path.abspath(pivot_spec),
333
+ pivot_spec_path=_spec_ref(pivot_spec),
313
334
  original_path=(
314
335
  f if _URL_RE.match(f) else os.path.abspath(f)),
315
336
  source_spec=from_source)
@@ -382,7 +403,8 @@ def main(files, globs, x, sheet, fmt, pivot, pivot_info, session, export,
382
403
  advanced visualization of simulation data.
383
404
 
384
405
  Supports: .raw, .csv, .tsv, .xlsx, .json, .parquet, .feather, .npz,
385
- .h5, .pkl, .vcd (digital), .iqvsa (LitePoint), .stdf (ATE), and more.
406
+ .h5, .pkl, .vcd (digital), .iqvsa (LitePoint), .stdf (ATE),
407
+ .u32 (raw counter captures), and more.
386
408
 
387
409
  \b
388
410
  URL sources:
@@ -10,6 +10,7 @@ import math
10
10
  import os
11
11
  import sys
12
12
  import re
13
+ from urllib.parse import urlsplit
13
14
  import numpy as np
14
15
  import pandas as pd
15
16
  from importlib.metadata import version as _pkg_version
@@ -3460,10 +3461,13 @@ class PgWaveWindow(QMainWindow):
3460
3461
  """
3461
3462
  from .apisource import is_source_spec_file
3462
3463
 
3463
- if path.lower().endswith('.cicwave.yaml'):
3464
+ #- Match on the path part: a spec served by an API can carry a
3465
+ #- query string, which the rest of cicwave already ignores when
3466
+ #- resolving a format.
3467
+ probe = (urlsplit(path).path if _is_url(path) else path).lower()
3468
+ if probe.endswith('.cicwave.yaml'):
3464
3469
  self.applySession(path)
3465
- elif (path.lower().endswith(('.yaml', '.yml'))
3466
- and is_source_spec_file(path)):
3470
+ elif probe.endswith(('.yaml', '.yml')) and is_source_spec_file(path):
3467
3471
  self.openSourceSpec(path)
3468
3472
  else:
3469
3473
  self.browser.openFile(path)
@@ -4017,6 +4021,14 @@ class PgWaveWindow(QMainWindow):
4017
4021
  if getattr(wave, 'show_as_digital', False):
4018
4022
  wave_dict['digital'] = True
4019
4023
  wave_dict['digital_format'] = wave.digital_format
4024
+ #- A catalog's waves exist only once their group has been
4025
+ #- fetched, so the session has to say which one to ask
4026
+ #- for; without it the wave is simply not found on load.
4027
+ group = None
4028
+ if hasattr(wave.wfile, 'groupOf'):
4029
+ group = wave.wfile.groupOf(wave.key)
4030
+ if group is not None:
4031
+ wave_dict['group'] = group
4020
4032
  waves.append(wave_dict)
4021
4033
  plot_dict = {'name': tab_name, 'waves': waves}
4022
4034
  if widget.custom_xlabel:
@@ -4063,11 +4075,18 @@ class PgWaveWindow(QMainWindow):
4063
4075
  return
4064
4076
  session = self._build_session()
4065
4077
  session_dir = os.path.dirname(os.path.abspath(fname))
4078
+
4079
+ def _portable(ref):
4080
+ #- A URL is already portable; only a local path is worth
4081
+ #- rewriting relative to where the session is being saved.
4082
+ return ref if _is_url(ref) else os.path.relpath(ref, session_dir)
4083
+
4066
4084
  for fe in session.get('files', []):
4067
- if not _is_url(fe['path']):
4068
- fe['path'] = os.path.relpath(fe['path'], session_dir)
4069
- if 'pivot' in fe:
4070
- fe['pivot'] = os.path.relpath(fe['pivot'], session_dir)
4085
+ #- A spec that fetches its own data is stored as 'source'
4086
+ #- and has no 'path' at all, so key by what is there.
4087
+ for key in ('path', 'source', 'pivot'):
4088
+ if key in fe:
4089
+ fe[key] = _portable(fe[key])
4071
4090
 
4072
4091
  with open(fname, 'w') as fh:
4073
4092
  yaml.dump(session, fh, default_flow_style=False, sort_keys=False)
@@ -4097,15 +4116,29 @@ class PgWaveWindow(QMainWindow):
4097
4116
  fpath = os.path.normpath(os.path.join(session_dir, fpath))
4098
4117
  fmt = fe.get('format')
4099
4118
  pivot_path = fpath if from_source else fe.get('pivot')
4100
- if pivot_path and not os.path.isabs(pivot_path):
4119
+ if (pivot_path and not _is_url(pivot_path)
4120
+ and not os.path.isabs(pivot_path)):
4101
4121
  pivot_path = os.path.normpath(
4102
4122
  os.path.join(session_dir, pivot_path))
4103
4123
 
4104
4124
  try:
4105
4125
  if pivot_path:
4106
- from .apisource import load_flat_frame
4126
+ from .apisource import (
4127
+ LazyCatalog, has_catalog, load_flat_frame)
4107
4128
  from .pivot import load_spec, apply_pivot
4108
4129
  spec = load_spec(pivot_path)
4130
+ spec_ref = (pivot_path if _is_url(pivot_path)
4131
+ else os.path.abspath(pivot_path))
4132
+ if from_source and has_catalog(spec):
4133
+ #- Same as opening the spec directly: the groups
4134
+ #- are named now and each is fetched when it is
4135
+ #- plotted. Fetching the lot is what a catalog
4136
+ #- exists to avoid, session or not.
4137
+ catalog = LazyCatalog(spec)
4138
+ self.browser.openLazyFrame(
4139
+ os.path.basename(fpath), catalog.load(),
4140
+ catalog.load_group, pivot_spec_path=spec_ref)
4141
+ continue
4109
4142
  xaxis = self.browser.xaxis or spec.get('columns', '')
4110
4143
  if not self.browser.xaxis and spec.get('columns'):
4111
4144
  self.browser.xaxis = spec['columns']
@@ -4116,7 +4149,7 @@ class PgWaveWindow(QMainWindow):
4116
4149
  name = "pivot(%s)" % os.path.basename(fpath)
4117
4150
  self.browser.openDataFrame(
4118
4151
  pivoted, name,
4119
- pivot_spec_path=os.path.abspath(pivot_path),
4152
+ pivot_spec_path=spec_ref,
4120
4153
  original_path=(fpath if _is_url(fpath)
4121
4154
  else os.path.abspath(fpath)),
4122
4155
  source_spec=from_source)
@@ -4141,6 +4174,10 @@ class PgWaveWindow(QMainWindow):
4141
4174
  for wd in pd.get('waves', []):
4142
4175
  wave_name = wd.get('name')
4143
4176
  style = wd.get('style', 'Lines')
4177
+ #- A wave from a catalog group is not there until that
4178
+ #- group is fetched; one request, not the whole catalog.
4179
+ if wd.get('group'):
4180
+ self._fetch_session_group(wd['group'])
4144
4181
  wave = self._find_wave(wave_name)
4145
4182
  if wave:
4146
4183
  bits = wd.get('twos_complement_bits')
@@ -4189,6 +4226,19 @@ class PgWaveWindow(QMainWindow):
4189
4226
  xv = math.log10(xv)
4190
4227
  p._set_cursor(which[-1], xv)
4191
4228
 
4229
+ def _fetch_session_group(self, group):
4230
+ """Fetch a catalog group a session's plot needs, if still pending."""
4231
+ for wf in self.browser.files.values():
4232
+ if hasattr(wf, 'isPendingGroup') and wf.isPendingGroup(group):
4233
+ try:
4234
+ wf.loadGroup(group)
4235
+ except Exception as e:
4236
+ QMessageBox.critical(
4237
+ self, "Fetch",
4238
+ "Failed to fetch %s:\n%s" % (group, e))
4239
+ self.browser._fill_waves()
4240
+ return
4241
+
4192
4242
  def _find_wave(self, name):
4193
4243
  """Look up a PgWave by column name across all loaded files."""
4194
4244
  for fkey, wf in self.browser.files.items():
@@ -470,6 +470,9 @@ class WaveFile():
470
470
  self._group_loader = group_loader
471
471
  self._pending_groups = list(groups)
472
472
  self._loaded_groups = set()
473
+ #- Which group produced each wave, so a session can name the one
474
+ #- group it has to fetch to get a plotted wave back.
475
+ self._group_of = dict()
473
476
  self._remote = (not self._virtual) and _is_url(fname)
474
477
  self._remote_bytes = None
475
478
  self._remote_content_type = None
@@ -1056,10 +1059,19 @@ class WaveFile():
1056
1059
  if c not in x_columns
1057
1060
  and (self._df is None or c not in self._df.columns)]
1058
1061
  self._merge_group_frame(frame)
1062
+ #- Every column the fetch brought in, not just the plottable
1063
+ #- ones: a session may name the group's x column too, and it is
1064
+ #- just as absent until the group is fetched.
1065
+ for c in frame.columns:
1066
+ self._group_of.setdefault(c, name)
1059
1067
  for wave in list(self.waves.values()):
1060
1068
  wave.reload()
1061
1069
  return added
1062
1070
 
1071
+ def groupOf(self, name):
1072
+ """The catalog group *name* came from, or None if it stands alone."""
1073
+ return self._group_of.get(name)
1074
+
1063
1075
  def _merge_group_frame(self, frame):
1064
1076
  """Append a fetched group's rows, widening the frame as needed.
1065
1077
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cicwave
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: Advanced waveform viewer with PyQtGraph backend
5
5
  Author-email: Carsten Wulff <carsten@wulff.no>
6
6
  License: MIT
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes