pipeline-eds 0.2.13__tar.gz → 0.2.15__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 (63) hide show
  1. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/PKG-INFO +3 -2
  2. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/pyproject.toml +3 -2
  3. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/api/eds.py +52 -6
  4. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/cli.py +38 -19
  5. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/environment.py +2 -0
  6. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/gui_fastapi_plotly_live.py +31 -5
  7. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/gui_mpl_live.py +41 -27
  8. pipeline_eds-0.2.15/src/pipeline/gui_plotly_static.py +43 -0
  9. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/workspace_manager.py +57 -11
  10. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/LICENSE +0 -0
  11. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/README.md +0 -0
  12. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/__init__.py +0 -0
  13. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/__main__.py +0 -0
  14. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/api/__init__.py +0 -0
  15. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/api/rjn.py +0 -0
  16. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/api/status_api.py +0 -0
  17. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/calls.py +0 -0
  18. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/configrationmanager.py +0 -0
  19. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/decorators.py +0 -0
  20. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/env.py +0 -0
  21. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/helpers.py +0 -0
  22. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/install_appdata.py +0 -0
  23. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/logging_setup.py +0 -0
  24. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/pastehelpers.py +0 -0
  25. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/philosophy.py +0 -0
  26. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/plotbuffer.py +0 -0
  27. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/points_loader.py +0 -0
  28. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/queriesmanager.py +0 -0
  29. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/src/pipeline/time_manager.py +0 -0
  30. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/default-workspace.toml +0 -0
  31. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/__init__.py +0 -0
  32. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/code/__init__.py +0 -0
  33. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/code/aggregator.py +0 -0
  34. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/code/collector.py +0 -0
  35. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/code/sanitizer.py +0 -0
  36. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/code/storage.py +0 -0
  37. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/configurations/config_time.toml +0 -0
  38. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/configurations/configuration.toml +0 -0
  39. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/exports/README.md +0 -0
  40. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/exports/aggregate/README.md +0 -0
  41. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/exports/aggregate/live_data - Copy.csv +0 -0
  42. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/exports/aggregate/live_data_EFF.csv +0 -0
  43. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/exports/aggregate/live_data_INF.csv +0 -0
  44. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/exports/export_eds_points_neo.txt +0 -0
  45. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/exports/manual_data_load_to_postman_wetwell.csv +0 -0
  46. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/exports/manual_data_load_to_postman_wetwell.xlsx +0 -0
  47. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/exports/manual_effluent.csv +0 -0
  48. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/exports/manual_influent.csv +0 -0
  49. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/exports/manual_wetwell.csv +0 -0
  50. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/history/time_sample.txt +0 -0
  51. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/imports/zdMaxson_idcsD321E_sid11003.toml +0 -0
  52. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/imports/zdMaxson_idcsFI8001_sid8528.toml +0 -0
  53. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/imports/zdMaxson_idcsM100FI_sid2308.toml +0 -0
  54. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/imports/zdMaxson_idcsM310LI_sid2382.toml +0 -0
  55. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/queries/default-queries.toml +0 -0
  56. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/queries/points-maxson.csv +0 -0
  57. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/queries/points-stiles.csv +0 -0
  58. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/queries/timestamps_success.json +0 -0
  59. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/scripts/__init__.py +0 -0
  60. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/scripts/daemon_runner.py +0 -0
  61. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/secrets/README.md +0 -0
  62. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_rjn/secrets/secrets-example.yaml +0 -0
  63. {pipeline_eds-0.2.13 → pipeline_eds-0.2.15}/workspaces/eds_to_termux/..txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pipeline-eds
3
- Version: 0.2.13
3
+ Version: 0.2.15
4
4
  Summary: The official API pipeline library for mulch-based projects. Key target: Emerson Ovation EDS REST API.
5
5
  License: BSD-3
6
6
  Author: George Clayton Bennett
@@ -13,7 +13,8 @@ Classifier: Programming Language :: Python :: 3.12
13
13
  Classifier: Programming Language :: Python :: 3.13
14
14
  Provides-Extra: windb
15
15
  Requires-Dist: certifi (>=2025.1.31,<2026.0.0)
16
- Requires-Dist: fastapi (==0.103.0)
16
+ Requires-Dist: fastapi (>=0.116.1,<0.117.0)
17
+ Requires-Dist: matplotlib (>=3.10.6,<4.0.0)
17
18
  Requires-Dist: mulch (>=0.2.8,<0.3.0)
18
19
  Requires-Dist: mysql-connector-python (>=9.3.0,<10.0.0)
19
20
  Requires-Dist: pendulum (>=3.1.0,<4.0.0)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "pipeline-eds"
3
- version = "0.2.13"
3
+ version = "0.2.15"
4
4
  description = "The official API pipeline library for mulch-based projects. Key target: Emerson Ovation EDS REST API."
5
5
  authors = ["George Clayton Bennett <george.bennett@memphistn.gov>"]
6
6
  license = "BSD-3"
@@ -39,7 +39,6 @@ pyyaml = ">=6.0.2,<7.0.0"
39
39
  certifi = ">=2025.1.31,<2026.0.0"
40
40
  schedule = ">=1.2.2,<2.0.0"
41
41
  #fastapi = ">=0.115.12,<0.116.0"
42
- fastapi = "0.103.0"
43
42
  pydantic = "1.10.13"
44
43
  urllib3 = "^2.4.0"
45
44
  plotly = "^6.2.0"
@@ -48,6 +47,8 @@ mulch = "^0.2.8"
48
47
  mysql-connector-python = "^9.3.0"
49
48
  tzdata = "^2025.2"
50
49
  pendulum = "^3.1.0"
50
+ fastapi = "^0.116.1"
51
+ matplotlib = "^3.10.6"
51
52
 
52
53
  [tool.poetry.extras]
53
54
  windb = ["pyodbc"]
@@ -165,7 +165,7 @@ class EdsClient:
165
165
 
166
166
  @staticmethod
167
167
  #def create_tabular_request(session: object, api_url: str, starttime: int, endtime: int, points: list):
168
- def create_tabular_request(session, api_url, starttime, endtime, points, step_seconds = 300):
168
+ def create_tabular_request_(session, api_url, starttime, endtime, points, step_seconds = 300):
169
169
 
170
170
  data = {
171
171
  'period': {
@@ -189,6 +189,51 @@ class EdsClient:
189
189
  response = session.post(f"{api_url}/trend/tabular", json=data, verify=False)
190
190
  #print(f"response = {response}")
191
191
 
192
+ @staticmethod
193
+ def create_tabular_request(session, api_url, starttime, endtime, points, step_seconds=300):
194
+ """
195
+ Submit a tabular trend request. Returns request id on success, or None if failed.
196
+ """
197
+
198
+ data = {
199
+ "period": {
200
+ "from": starttime,
201
+ "till": endtime,
202
+ },
203
+ "step": step_seconds,
204
+ "items": [
205
+ {
206
+ "pointId": {"iess": p},
207
+ "shadePriority": "DEFAULT",
208
+ "function": "AVG",
209
+ }
210
+ for p in points
211
+ ],
212
+ }
213
+
214
+ try:
215
+ res = session.post(f"{api_url}/trend/tabular", json=data, verify=False)
216
+ except Exception as e:
217
+ logger.error(f"Request failed to {api_url}/trend/tabular: {e}")
218
+ return None
219
+
220
+ if res.status_code != 200:
221
+ logger.error(f"Bad status {res.status_code} from server: {res.text}")
222
+ return None
223
+
224
+ try:
225
+ payload = res.json()
226
+ except Exception:
227
+ logger.error(f"Non-JSON response: {res.text}")
228
+ return None
229
+
230
+ req_id = payload.get("id")
231
+ if not req_id:
232
+ logger.error(f"No request id in response: {payload}")
233
+ return None
234
+
235
+ return req_id
236
+
192
237
  @staticmethod
193
238
  def wait_for_request_execution_session(session, api_url, req_id):
194
239
  st = time.time()
@@ -726,7 +771,7 @@ def demo_eds_webplot_point_live():
726
771
  gui_fastapi_plotly_live.run_gui(data_buffer)
727
772
 
728
773
  @log_function_call(level=logging.DEBUG)
729
- def load_historic_data(session, iess_list, starttime, endtime):
774
+ def load_historic_data(session, iess_list, starttime, endtime, step_seconds):
730
775
 
731
776
 
732
777
  starttime = TimeManager(starttime).as_unix()
@@ -734,11 +779,13 @@ def load_historic_data(session, iess_list, starttime, endtime):
734
779
  logger.info(f"starttime = {starttime}")
735
780
  logger.info(f"endtime = {endtime}")
736
781
 
737
- step_seconds = helpers.nice_step(endtime-starttime)
738
782
 
739
783
  point_list = iess_list
740
784
  api_url = str(session.base_url)
741
785
  request_id = EdsClient.create_tabular_request(session, api_url, starttime, endtime, points=point_list, step_seconds=step_seconds)
786
+ if not request_id:
787
+ logger.warning(f"Could not create tabular request for points: {point_list}")
788
+ return [] # or None, depending on how you want the CLI to behave
742
789
  EdsClient.wait_for_request_execution_session(session, api_url, request_id)
743
790
  results = EdsClient.get_tabular_trend(session, request_id, point_list)
744
791
  logger.debug(f"len(results) = {len(results)}")
@@ -951,10 +998,9 @@ if __name__ == "__main__":
951
998
  demo_eds_save_graphics_export()
952
999
  elif cmd == "license":
953
1000
  demo_eds_print_license()
954
- elif cmd == "access-workspace""":
1001
+ elif cmd == "access-workspace":
955
1002
  if platform.system().lower() == "windows":
956
- # run the Open-FileBrowser command, registered with: git clone https://github.com/city-of-memphis-wastewater/powershell-tools.git ## run `notepad $profile` #noobs
957
- #command = ["Open-FileBrowser", WorkspaceManager.get_cwd()]
1003
+ # at this level it is correct but the get_cwd() command only knows the default workspace.
958
1004
  command = ["explorer", str(WorkspaceManager.get_cwd())]
959
1005
  subprocess.call(command)
960
1006
  else:
@@ -20,22 +20,23 @@ from pipeline.env import SecretConfig
20
20
  from pipeline.workspace_manager import WorkspaceManager
21
21
 
22
22
  ### Versioning
23
+ CLI_APP_NAME = "pipeline"
23
24
  def print_version(value: bool):
24
25
  if value:
25
26
  try:
26
- typer.secho(f"mulch {MULCH_VERSION}",fg=typer.colors.GREEN, bold=True)
27
+ typer.secho(f"{CLI_APP_NAME} {PIPELINE_VERSION}",fg=typer.colors.GREEN, bold=True)
27
28
  except PackageNotFoundError:
28
29
  typer.echo("Version info not found")
29
30
  raise typer.Exit()
30
31
  try:
31
- MULCH_VERSION = version("mulch")
32
- __version__ = version("mulch")
32
+ PIPELINE_VERSION = version(CLI_APP_NAME)
33
+ __version__ = version(CLI_APP_NAME)
33
34
  except PackageNotFoundError:
34
- MULCH_VERSION = "unknown"
35
+ PIPELINE_VERSION = "unknown"
35
36
 
36
37
  try:
37
38
  from importlib.metadata import version
38
- __version__ = version("mulch")
39
+ __version__ = version(CLI_APP_NAME)
39
40
  except PackageNotFoundError:
40
41
  # fallback if running from source
41
42
  try:
@@ -102,7 +103,9 @@ def trend(
102
103
  starttime: str = typer.Option(None, "--start", "-s", help="Index from 'mulch order' to choose scaffold source."),
103
104
  endtime: str = typer.Option(None, "--end", "-end", help="Reference a known template for workspace organization."),
104
105
  zd: str = typer.Option('Maxson', "--zd", "-z", help = "Define the EDS ZD from your secrets file. This must correlate with your idcs point selection(s)."),
105
- workspace: str = typer.Option(None,"--workspace","-w", help = "Provide the name of the workspace you want to use, for the secrets.yaml credentials and for the timezone config. If a start time is not provided, the workspace queries can checked for the most recent successful timestamp. ")
106
+ workspacename: str = typer.Option(None,"--workspace","-w", help = "Provide the name of the workspace you want to use, for the secrets.yaml credentials and for the timezone config. If a start time is not provided, the workspace queries can checked for the most recent successful timestamp. "),
107
+ print_csv: bool = typer.Option(False,"--print-csv","-p",help = "Print the CSV style for pasting into Excel."),
108
+ step_seconds: int = typer.Option(None, "--step-seconds", help="You can explicitly provide the delta between datapoints. If not, ~400 data points will be used, based on the nice_step() function.")
106
109
  ):
107
110
  """
108
111
  Show a curve for a sensor over time.
@@ -112,16 +115,16 @@ def trend(
112
115
  from pipeline.api.eds import EdsClient, load_historic_data
113
116
  from pipeline import helpers
114
117
  from pipeline.plotbuffer import PlotBuffer
115
- from pipeline import gui_fastapi_plotly_live
116
118
  from pipeline import environment
117
119
  from pipeline.workspace_manager import WorkspaceManager
118
- ws_dir = WorkspaceManager.ensure_workspace()
120
+ workspaces_dir = WorkspaceManager.ensure_appdata_workspaces_dir()
119
121
 
120
122
  # must set up %appdata for pip/x installation. Use mulch or yeoman for this. And have a secrets filler.
121
- if workspace is None:
122
- WorkspaceManager.identify_default_workspace_name()
123
- wm = WorkspaceManager(workspace)
124
- secrets_dict = SecretConfig.load_config(secrets_file_path = wm.get_secrets_file_path())
123
+ if workspacename is None:
124
+ workspacename = WorkspaceManager.identify_default_workspace_name()
125
+ wm = WorkspaceManager(workspacename)
126
+ secrets_file_path = wm.get_secrets_file_path()
127
+ secrets_dict = SecretConfig.load_config(secrets_file_path)
125
128
 
126
129
  if zd.lower() == "stiles":
127
130
  zd = "WWTF"
@@ -156,22 +159,38 @@ def trend(
156
159
  dt_finish = pendulum.parse(endtime, strict=False)
157
160
 
158
161
  # Should automatically choose time step granularity based on time length; map
159
-
160
- results = load_historic_data(session, iess_list, dt_start, dt_finish)
161
-
162
+ if step_seconds is None:
163
+ step_seconds = helpers.nice_step(endtime-starttime)
164
+ results = load_historic_data(session, iess_list, dt_start, dt_finish, step_seconds)
165
+ if not results:
166
+ return
167
+
162
168
  data_buffer = PlotBuffer()
163
169
  for idx, rows in enumerate(results):
164
170
  for row in rows:
165
- label = f"{row.get('rjn_entityid')} ({row.get('units')})"
171
+ #label = f"({row.get('units')})"
172
+ label = iess_list[0]
166
173
  ts = helpers.iso(row.get("ts"))
167
174
  av = row.get("value")
175
+ #print(f"{round(av,2)}")
168
176
  data_buffer.append(label, ts, av) # needs to be adapted for multiple iess sensor results
169
-
177
+ #print(f"data_buffer = {data_buffer}")
178
+ #print(f"data_buffer.get_all() = {data_buffer.get_all()}")
170
179
  if not environment.matplotlib_enabled():
171
- gui_fastapi_plotly_live.run_gui(data_buffer)
180
+ from pipeline import gui_plotly_static
181
+ #gui_fastapi_plotly_live.run_gui(data_buffer)
182
+ gui_plotly_static.show_static(data_buffer)
172
183
  else:
173
184
  from pipeline import gui_mpl_live
174
- gui_mpl_live.run_gui(data_buffer)
185
+ #gui_mpl_live.run_gui(data_buffer)
186
+ gui_mpl_live.show_static(data_buffer)
187
+
188
+ if print_csv:
189
+ print(f"Time,\\{iess_list[0]}\\,")
190
+ for idx, rows in enumerate(results):
191
+ for row in rows:
192
+ print(f"{helpers.iso(row.get('ts'))},{row.get('value')},")
193
+
175
194
 
176
195
  @app.command()
177
196
  def list_workspaces():
@@ -12,11 +12,13 @@ def vercel():
12
12
  return False # hard code this
13
13
 
14
14
  def matplotlib_enabled():
15
+ #print(f"is_termux() = {is_termux()}")
15
16
  if is_termux():
16
17
  return False
17
18
  else:
18
19
  try:
19
20
  import matplotlib
21
+ return True
20
22
  except ImportError:
21
23
  return False
22
24
 
@@ -57,12 +57,32 @@ HTML_TEMPLATE = """
57
57
  async def index():
58
58
  return HTML_TEMPLATE
59
59
 
60
+
60
61
  @app.get("/data", response_class=JSONResponse)
61
62
  async def get_data():
63
+ if plot_buffer is None:
64
+ print("plot_buffer is None")
65
+ return {}
62
66
  with buffer_lock:
63
- data = plot_buffer.get_all() # Should return { label: {"x": [...], "y": [...]}, ... }
64
- return data
65
-
67
+ data = plot_buffer.get_all()
68
+ print("Data in buffer:", data) # <-- DEBUG
69
+ fixed_data = {}
70
+ for label, series in data.items():
71
+ fixed_data[label] = {
72
+ "x": [ts + "Z" if not ts.endswith("Z") else ts for ts in series["x"]],
73
+ "y": series["y"]
74
+ }
75
+ return fixed_data
76
+ """
77
+ @app.get("/data", response_class=JSONResponse)
78
+ async def get_data():
79
+ return {
80
+ "Test Series": {
81
+ "x": ["2025-09-05T15:00:00Z", "2025-09-05T15:05:00Z", "2025-09-05T15:10:00Z"],
82
+ "y": [1, 3, 2]
83
+ }
84
+ }
85
+ """
66
86
  def open_browser(port):
67
87
  time.sleep(1) # Give server a moment to start
68
88
  ## Open in a new Chrome window (if installed)
@@ -71,8 +91,14 @@ def open_browser(port):
71
91
 
72
92
  webbrowser.open(f"http://127.0.0.1:{port}")
73
93
 
94
+ #def run_gui(buffer, port=8000):
95
+ # global plot_buffer
96
+ # plot_buffer = buffer
97
+ # threading.Thread(target=open_browser, args=(port,), daemon=True).start()
98
+ # uvicorn.run("src.pipeline.gui_fastapi_plotly_live:app", host="127.0.0.1", port=port, log_level="info", reload=False)
99
+
74
100
  def run_gui(buffer, port=8000):
75
101
  global plot_buffer
76
- plot_buffer = buffer
102
+ plot_buffer = buffer # set the buffer in this process
77
103
  threading.Thread(target=open_browser, args=(port,), daemon=True).start()
78
- uvicorn.run("src.pipeline.gui_fastapi_plotly_live:app", host="127.0.0.1", port=port, log_level="info", reload=False)
104
+ uvicorn.run(app, host="127.0.0.1", port=port, log_level="info", reload=False) # <- reload=False
@@ -3,6 +3,8 @@ import time
3
3
  import logging
4
4
  import matplotlib.pyplot as plt
5
5
  import matplotlib.animation as animation
6
+ import matplotlib.dates as mdates
7
+ from datetime import datetime, timedelta
6
8
  from pipeline import helpers
7
9
  from pipeline.plotbuffer import PlotBuffer # Adjust import path as needed
8
10
  from pipeline.time_manager import TimeManager
@@ -11,28 +13,6 @@ logger = logging.getLogger(__name__)
11
13
 
12
14
  PADDING_RATIO = 0.25
13
15
 
14
- def compute_padded_bounds(data):
15
- all_x_vals = []
16
- all_y_vals = []
17
-
18
- for series in data.values():
19
- all_x_vals.extend(series["x"])
20
- all_y_vals.extend(series["y"])
21
-
22
- if not all_x_vals or not all_y_vals:
23
- return (0, 1), (0, 1)
24
-
25
- x_min, x_max = min(all_x_vals), max(all_x_vals)
26
- y_min, y_max = min(all_y_vals), max(all_y_vals)
27
-
28
- x_pad = max((x_max - x_min) * PADDING_RATIO, 1.0)
29
- y_pad = max((y_max - y_min) * PADDING_RATIO, 1.0)
30
-
31
- padded_x = (x_min - x_pad, x_max + x_pad)
32
- padded_y = (y_min - y_pad, y_max + y_pad)
33
-
34
- return padded_x, padded_y
35
-
36
16
  def run_gui(buffer: PlotBuffer, update_interval_ms=1000):
37
17
  """
38
18
  Runs a matplotlib live updating plot based on the PlotBuffer content.
@@ -45,6 +25,11 @@ def run_gui(buffer: PlotBuffer, update_interval_ms=1000):
45
25
  ax.set_title("Live Pipeline Data")
46
26
  ax.set_xlabel("Time")
47
27
  ax.set_ylabel("Value")
28
+ # Auto-locate ticks and auto-format dates
29
+ locator = mdates.AutoDateLocator()
30
+ formatter = mdates.AutoDateFormatter(locator)
31
+ ax.xaxis.set_major_locator(locator)
32
+ ax.xaxis.set_major_formatter(formatter)
48
33
 
49
34
  lines = {}
50
35
  legend_labels = []
@@ -80,11 +65,6 @@ def run_gui(buffer: PlotBuffer, update_interval_ms=1000):
80
65
  else:
81
66
  lines[label].set_data(x_vals, y_vals)
82
67
 
83
- # Adjust axes limits with padding
84
- padded_x, padded_y = compute_padded_bounds(data)
85
- ax.set_xlim(padded_x)
86
- ax.set_ylim(padded_y)
87
-
88
68
  # Format x-axis ticks as human readable time strings
89
69
 
90
70
  # Tick positions are x values at those indices
@@ -111,3 +91,37 @@ def run_gui(buffer: PlotBuffer, update_interval_ms=1000):
111
91
  plt.tight_layout()
112
92
  plt.show()
113
93
 
94
+ def show_static(buffer: PlotBuffer):
95
+ """
96
+ Show a static matplotlib plot of the current PlotBuffer contents,
97
+ with automatic date formatting based on time span.
98
+ """
99
+ plt.style.use('ggplot')
100
+ fig, ax = plt.subplots(figsize=(10, 6))
101
+ ax.set_title("Static Pipeline Data")
102
+ ax.set_xlabel("Time")
103
+ ax.set_ylabel("Value")
104
+
105
+ data = buffer.get_all()
106
+ if not data:
107
+ ax.text(0.5, 0.5, "No data to display", ha='center', va='center')
108
+ plt.show()
109
+ return
110
+
111
+ for label, series in data.items():
112
+ # Convert strings to datetime objects for better handling
113
+ x_vals = [TimeManager(ts).as_datetime() for ts in series["x"]]
114
+ y_vals = series["y"]
115
+
116
+ ax.plot(x_vals, y_vals, marker='o', linestyle='-', label=label)
117
+
118
+ # Let matplotlib auto-locate ticks and auto-format
119
+ locator = mdates.AutoDateLocator()
120
+ formatter = mdates.AutoDateFormatter(locator)
121
+ ax.xaxis.set_major_locator(locator)
122
+ ax.xaxis.set_major_formatter(formatter)
123
+
124
+ plt.setp(ax.get_xticklabels(), rotation=45, ha='right')
125
+ ax.legend()
126
+ plt.tight_layout()
127
+ plt.show()
@@ -0,0 +1,43 @@
1
+ # src/pipeline/gui_plotly_static.py
2
+
3
+ import plotly.graph_objs as go
4
+ import plotly.offline as pyo
5
+ import webbrowser
6
+ import tempfile
7
+ from threading import Lock
8
+
9
+ buffer_lock = Lock() # Optional, if you want thread safety
10
+
11
+ def show_static(plot_buffer):
12
+ """
13
+ Renders the current contents of plot_buffer as a static HTML plot.
14
+ Does not listen for updates.
15
+ """
16
+ if plot_buffer is None:
17
+ print("plot_buffer is None")
18
+ return
19
+
20
+ with buffer_lock:
21
+ data = plot_buffer.get_all()
22
+
23
+ traces = []
24
+ for label, series in data.items():
25
+ traces.append(go.Scatter(
26
+ x=series["x"],
27
+ y=series["y"],
28
+ mode="lines+markers",
29
+ name=label
30
+ ))
31
+
32
+ layout = go.Layout(
33
+ title="EDS Data Plot (Static)",
34
+ margin=dict(t=40)
35
+ )
36
+
37
+ fig = go.Figure(data=traces, layout=layout)
38
+
39
+ # Write to a temporary HTML file
40
+ tmp_file = tempfile.NamedTemporaryFile(suffix=".html", delete=False)
41
+ pyo.plot(fig, filename=tmp_file.name, auto_open=False)
42
+
43
+ webbrowser.open(f"file://{tmp_file.name}")
@@ -2,6 +2,7 @@ import os
2
2
  import toml
3
3
  import logging
4
4
  from pathlib import Path
5
+ import sys
5
6
 
6
7
  '''
7
8
  Goal:
@@ -29,7 +30,14 @@ class WorkspaceManager:
29
30
  APP_NAME = "pipeline"
30
31
 
31
32
  TIMESTAMPS_JSON_FILE_NAME = 'timestamps_success.json'
32
- ROOT_DIR = Path(__file__).resolve().parents[2] # root directory
33
+
34
+ # Detect if running in a dev repo vs installed package
35
+ if getattr(sys, "frozen", False):
36
+ # Running from a pipx/executable environment
37
+ ROOT_DIR = None
38
+ else:
39
+ # Running from a cloned repo
40
+ ROOT_DIR = Path(__file__).resolve().parents[2] # root directory
33
41
 
34
42
 
35
43
  # This climbs out of /src/pipeline/ to find the root.
@@ -62,11 +70,38 @@ class WorkspaceManager:
62
70
  self.logs_dir,
63
71
  self.aggregate_dir])
64
72
 
65
- def get_workspaces_dir(self):
66
- return self.ROOT_DIR / self.WORKSPACES_DIR_NAME
73
+
74
+ @classmethod
75
+ def get_workspaces_dir(cls):
76
+ """
77
+ Return workspaces directory depending on environment:
78
+ - If ROOT_DIR is defined (repo clone), use that
79
+ - Else use AppData/local platform-specific location
80
+ """
81
+ if cls.ROOT_DIR and (cls.ROOT_DIR / cls.WORKSPACES_DIR_NAME).exists():
82
+ workspaces_dir = cls.ROOT_DIR / cls.WORKSPACES_DIR_NAME
83
+ else:
84
+ workspaces_dir = cls.get_appdata_dir() / cls.WORKSPACES_DIR_NAME
85
+ workspaces_dir.mkdir(parents=True, exist_ok=True)
86
+ default_file = workspaces_dir / cls.DEFAULT_WORKSPACE_TOML_FILE_NAME
87
+ if not default_file.exists():
88
+ # auto-populate default TOML with most recent workspace
89
+ recent_ws = cls.most_recent_workspace_name() or "default"
90
+ default_file.write_text(f"[default-workspace]\nworkspace = '{recent_ws}'\n")
91
+ return workspaces_dir
92
+
93
+ @classmethod
94
+ def most_recent_workspace_name(cls):
95
+ workspaces_dir = cls.get_workspaces_dir()
96
+ all_dirs = [p for p in workspaces_dir.iterdir() if p.is_dir() and not p.name.startswith('.')]
97
+ if not all_dirs:
98
+ return None
99
+ latest = max(all_dirs, key=lambda p: p.stat().st_mtime)
100
+ return latest.name
67
101
 
68
102
  def get_workspace_dir(self):
69
- return self.get_workspaces_dir() / self.workspace_name
103
+ # workspace_name is established at instantiation. You want a new name? Initialize a new WorkspaceManager(). It manages one workpspace.
104
+ return self.get_workspaces_dir() / self.workspace_name
70
105
 
71
106
  def get_exports_dir(self):
72
107
  return self.workspace_dir / self.EXPORTS_DIR_NAME
@@ -191,13 +226,25 @@ class WorkspaceManager:
191
226
  if not default_workspace_path.exists():
192
227
  raise FileNotFoundError(f"Default workspace directory not found: {default_workspace_path}")
193
228
  return default_workspace_path
229
+
230
+ '''
231
+ @classmethod
232
+ def identify_default_workspace_name(cls, workspaces_dir = None):
233
+ if workspaces_dir is None:
234
+ workspaces_dir = cls.get_workspaces_dir()
235
+ default_file = workspaces_dir / cls.DEFAULT_WORKSPACE_TOML_FILE_NAME
236
+ if not default_file.exists():
237
+ default_file.write_text("# Default workspace\n")
238
+ return str(default_file)
239
+ '''
240
+
194
241
  @classmethod
195
- def identify_default_workspace_name(cls):
242
+ def identify_default_workspace_name(cls, workspaces_dir = None):
196
243
  """
197
244
  Class method that reads default-workspace.toml to identify the default-workspace.
198
245
  """
199
-
200
- workspaces_dir = cls.ROOT_DIR / cls.WORKSPACES_DIR_NAME
246
+ if workspaces_dir is None:
247
+ workspaces_dir = cls.get_workspaces_dir()
201
248
  logging.info(f"workspaces_dir = {workspaces_dir}\n")
202
249
  default_toml_path = workspaces_dir / cls.DEFAULT_WORKSPACE_TOML_FILE_NAME
203
250
 
@@ -244,16 +291,15 @@ class WorkspaceManager:
244
291
  return base / cls.APP_NAME
245
292
 
246
293
  @classmethod
247
- def ensure_workspace(cls) -> Path:
294
+ def ensure_appdata_workspaces_dir(cls) -> Path:
248
295
  """Create workspace folder and default toml if missing."""
249
- workspaces_dir = cls.get_appdata_dir() / "workspaces"
296
+ workspaces_dir = cls.get_appdata_dir() / cls.WORKSPACES_DIR_NAME
250
297
  workspaces_dir.mkdir(parents=True, exist_ok=True)
251
-
298
+ cls.workspaces_dir = workspaces_dir
252
299
  default_file = workspaces_dir / cls.DEFAULT_WORKSPACE_TOML_FILE_NAME
253
300
  if not default_file.exists():
254
301
  default_file.write_text("# Default workspace config\n")
255
302
  return workspaces_dir
256
-
257
303
 
258
304
  def establish_default_workspace():
259
305
  workspace_name = WorkspaceManager.identify_default_workspace_name()
File without changes
File without changes