pipeline-eds 0.2.14__tar.gz → 0.2.16__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.14 → pipeline_eds-0.2.16}/PKG-INFO +3 -2
  2. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/pyproject.toml +3 -2
  3. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/api/eds.py +52 -6
  4. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/cli.py +36 -17
  5. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/env.py +2 -2
  6. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/environment.py +2 -0
  7. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/gui_fastapi_plotly_live.py +31 -5
  8. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/gui_mpl_live.py +41 -27
  9. pipeline_eds-0.2.16/src/pipeline/gui_plotly_static.py +43 -0
  10. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/workspace_manager.py +78 -18
  11. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/LICENSE +0 -0
  12. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/README.md +0 -0
  13. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/__init__.py +0 -0
  14. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/__main__.py +0 -0
  15. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/api/__init__.py +0 -0
  16. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/api/rjn.py +0 -0
  17. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/api/status_api.py +0 -0
  18. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/calls.py +0 -0
  19. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/configrationmanager.py +0 -0
  20. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/decorators.py +0 -0
  21. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/helpers.py +0 -0
  22. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/install_appdata.py +0 -0
  23. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/logging_setup.py +0 -0
  24. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/pastehelpers.py +0 -0
  25. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/philosophy.py +0 -0
  26. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/plotbuffer.py +0 -0
  27. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/points_loader.py +0 -0
  28. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/queriesmanager.py +0 -0
  29. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/src/pipeline/time_manager.py +0 -0
  30. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/default-workspace.toml +0 -0
  31. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/__init__.py +0 -0
  32. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/code/__init__.py +0 -0
  33. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/code/aggregator.py +0 -0
  34. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/code/collector.py +0 -0
  35. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/code/sanitizer.py +0 -0
  36. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/code/storage.py +0 -0
  37. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/configurations/config_time.toml +0 -0
  38. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/configurations/configuration.toml +0 -0
  39. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/exports/README.md +0 -0
  40. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/exports/aggregate/README.md +0 -0
  41. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/exports/aggregate/live_data - Copy.csv +0 -0
  42. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/exports/aggregate/live_data_EFF.csv +0 -0
  43. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/exports/aggregate/live_data_INF.csv +0 -0
  44. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/exports/export_eds_points_neo.txt +0 -0
  45. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/exports/manual_data_load_to_postman_wetwell.csv +0 -0
  46. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/exports/manual_data_load_to_postman_wetwell.xlsx +0 -0
  47. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/exports/manual_effluent.csv +0 -0
  48. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/exports/manual_influent.csv +0 -0
  49. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/exports/manual_wetwell.csv +0 -0
  50. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/history/time_sample.txt +0 -0
  51. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/imports/zdMaxson_idcsD321E_sid11003.toml +0 -0
  52. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/imports/zdMaxson_idcsFI8001_sid8528.toml +0 -0
  53. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/imports/zdMaxson_idcsM100FI_sid2308.toml +0 -0
  54. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/imports/zdMaxson_idcsM310LI_sid2382.toml +0 -0
  55. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/queries/default-queries.toml +0 -0
  56. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/queries/points-maxson.csv +0 -0
  57. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/queries/points-stiles.csv +0 -0
  58. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/queries/timestamps_success.json +0 -0
  59. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/scripts/__init__.py +0 -0
  60. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/scripts/daemon_runner.py +0 -0
  61. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/secrets/README.md +0 -0
  62. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/workspaces/eds_to_rjn/secrets/secrets-example.yaml +0 -0
  63. {pipeline_eds-0.2.14 → pipeline_eds-0.2.16}/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.14
3
+ Version: 0.2.16
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.14"
3
+ version = "0.2.16"
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:
@@ -17,7 +17,7 @@ from importlib.metadata import version, PackageNotFoundError
17
17
 
18
18
  from pipeline.env import SecretConfig
19
19
  #from pipeline.helpers import setup_logging
20
- from pipeline.workspace_manager import WorkspaceManager
20
+ #from pipeline.workspace_manager import WorkspaceManager
21
21
 
22
22
  ### Versioning
23
23
  CLI_APP_NAME = "pipeline"
@@ -30,13 +30,13 @@ def print_version(value: bool):
30
30
  raise typer.Exit()
31
31
  try:
32
32
  PIPELINE_VERSION = version(CLI_APP_NAME)
33
- __version__ = version({CLI_APP_NAME})
33
+ __version__ = version(CLI_APP_NAME)
34
34
  except PackageNotFoundError:
35
35
  PIPELINE_VERSION = "unknown"
36
36
 
37
37
  try:
38
38
  from importlib.metadata import version
39
- __version__ = version({CLI_APP_NAME})
39
+ __version__ = version(CLI_APP_NAME)
40
40
  except PackageNotFoundError:
41
41
  # fallback if running from source
42
42
  try:
@@ -71,6 +71,7 @@ def run(
71
71
  Import and run a workspace's main() function.
72
72
  """
73
73
  # Determine workspace name
74
+ from pipeline.workspace_manager import WorkspaceManager
74
75
  if workspace is None:
75
76
  workspace = WorkspaceManager.identify_default_workspace_name()
76
77
  wm = WorkspaceManager(workspace)
@@ -103,7 +104,9 @@ def trend(
103
104
  starttime: str = typer.Option(None, "--start", "-s", help="Index from 'mulch order' to choose scaffold source."),
104
105
  endtime: str = typer.Option(None, "--end", "-end", help="Reference a known template for workspace organization."),
105
106
  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)."),
106
- 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. ")
107
+ 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. "),
108
+ print_csv: bool = typer.Option(False,"--print-csv","-p",help = "Print the CSV style for pasting into Excel."),
109
+ 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.")
107
110
  ):
108
111
  """
109
112
  Show a curve for a sensor over time.
@@ -113,16 +116,16 @@ def trend(
113
116
  from pipeline.api.eds import EdsClient, load_historic_data
114
117
  from pipeline import helpers
115
118
  from pipeline.plotbuffer import PlotBuffer
116
- from pipeline import gui_fastapi_plotly_live
117
119
  from pipeline import environment
118
120
  from pipeline.workspace_manager import WorkspaceManager
119
- ws_dir = WorkspaceManager.ensure_workspace()
121
+ workspaces_dir = WorkspaceManager.ensure_appdata_workspaces_dir()
120
122
 
121
123
  # must set up %appdata for pip/x installation. Use mulch or yeoman for this. And have a secrets filler.
122
- if workspace is None:
123
- WorkspaceManager.identify_default_workspace_name()
124
- wm = WorkspaceManager(workspace)
125
- secrets_dict = SecretConfig.load_config(secrets_file_path = wm.get_secrets_file_path())
124
+ if workspacename is None:
125
+ workspacename = WorkspaceManager.identify_default_workspace_name()
126
+ wm = WorkspaceManager(workspacename)
127
+ secrets_file_path = wm.get_secrets_file_path()
128
+ secrets_dict = SecretConfig.load_config(secrets_file_path)
126
129
 
127
130
  if zd.lower() == "stiles":
128
131
  zd = "WWTF"
@@ -157,22 +160,38 @@ def trend(
157
160
  dt_finish = pendulum.parse(endtime, strict=False)
158
161
 
159
162
  # Should automatically choose time step granularity based on time length; map
160
-
161
- results = load_historic_data(session, iess_list, dt_start, dt_finish)
162
-
163
+ if step_seconds is None:
164
+ step_seconds = helpers.nice_step(TimeManager(dt_finish).as_unix()-TimeManager(dt_start.as_unix())) # TimeManager(starttime).as_unix()
165
+ results = load_historic_data(session, iess_list, dt_start, dt_finish, step_seconds)
166
+ if not results:
167
+ return
168
+
163
169
  data_buffer = PlotBuffer()
164
170
  for idx, rows in enumerate(results):
165
171
  for row in rows:
166
- label = f"{row.get('rjn_entityid')} ({row.get('units')})"
172
+ #label = f"({row.get('units')})"
173
+ label = iess_list[0]
167
174
  ts = helpers.iso(row.get("ts"))
168
175
  av = row.get("value")
176
+ #print(f"{round(av,2)}")
169
177
  data_buffer.append(label, ts, av) # needs to be adapted for multiple iess sensor results
170
-
178
+ #print(f"data_buffer = {data_buffer}")
179
+ #print(f"data_buffer.get_all() = {data_buffer.get_all()}")
171
180
  if not environment.matplotlib_enabled():
172
- gui_fastapi_plotly_live.run_gui(data_buffer)
181
+ from pipeline import gui_plotly_static
182
+ #gui_fastapi_plotly_live.run_gui(data_buffer)
183
+ gui_plotly_static.show_static(data_buffer)
173
184
  else:
174
185
  from pipeline import gui_mpl_live
175
- gui_mpl_live.run_gui(data_buffer)
186
+ #gui_mpl_live.run_gui(data_buffer)
187
+ gui_mpl_live.show_static(data_buffer)
188
+
189
+ if print_csv:
190
+ print(f"Time,\\{iess_list[0]}\\,")
191
+ for idx, rows in enumerate(results):
192
+ for row in rows:
193
+ print(f"{helpers.iso(row.get('ts'))},{row.get('value')},")
194
+
176
195
 
177
196
  @app.command()
178
197
  def list_workspaces():
@@ -1,8 +1,6 @@
1
1
  #env.__main__.py
2
2
 
3
3
  import yaml
4
- from pipeline.workspace_manager import WorkspaceManager
5
-
6
4
  '''
7
5
  migrate this to ConfigurationManager
8
6
  '''
@@ -49,6 +47,8 @@ def demo_secrets():
49
47
  caed defaut_workspace.toml - Clayton Bennett 26 April 2025.
50
48
  However this call can also be made if another project is made the active project.
51
49
  """
50
+ from pipeline.workspace_manager import WorkspaceManager
51
+
52
52
  workspace_name = WorkspaceManager.identify_default_workspace_name()
53
53
  workspace_manager = WorkspaceManager(workspace_name)
54
54
  config = SecretConfig.load_config(secrets_file_path = workspace_manager.get_secrets_file_path())
@@ -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,8 @@ import os
2
2
  import toml
3
3
  import logging
4
4
  from pathlib import Path
5
+ import sys
6
+ import mulch
5
7
 
6
8
  '''
7
9
  Goal:
@@ -29,7 +31,14 @@ class WorkspaceManager:
29
31
  APP_NAME = "pipeline"
30
32
 
31
33
  TIMESTAMPS_JSON_FILE_NAME = 'timestamps_success.json'
32
- ROOT_DIR = Path(__file__).resolve().parents[2] # root directory
34
+
35
+ # Detect if running in a dev repo vs installed package
36
+ if getattr(sys, "frozen", False):
37
+ # Running from a pipx/executable environment
38
+ ROOT_DIR = None
39
+ else:
40
+ # Running from a cloned repo
41
+ ROOT_DIR = Path(__file__).resolve().parents[2] # root directory
33
42
 
34
43
 
35
44
  # This climbs out of /src/pipeline/ to find the root.
@@ -62,11 +71,38 @@ class WorkspaceManager:
62
71
  self.logs_dir,
63
72
  self.aggregate_dir])
64
73
 
65
- def get_workspaces_dir(self):
66
- return self.ROOT_DIR / self.WORKSPACES_DIR_NAME
74
+
75
+ @classmethod
76
+ def get_workspaces_dir(cls):
77
+ """
78
+ Return workspaces directory depending on environment:
79
+ - If ROOT_DIR is defined (repo clone), use that
80
+ - Else use AppData/local platform-specific location
81
+ """
82
+ if cls.ROOT_DIR and (cls.ROOT_DIR / cls.WORKSPACES_DIR_NAME).exists():
83
+ workspaces_dir = cls.ROOT_DIR / cls.WORKSPACES_DIR_NAME
84
+ else:
85
+ workspaces_dir = cls.get_appdata_dir() / cls.WORKSPACES_DIR_NAME
86
+ workspaces_dir.mkdir(parents=True, exist_ok=True)
87
+ default_file = workspaces_dir / cls.DEFAULT_WORKSPACE_TOML_FILE_NAME
88
+ if not default_file.exists():
89
+ # auto-populate default TOML with most recent workspace
90
+ recent_ws = cls.most_recent_workspace_name() or "default"
91
+ default_file.write_text(f"[default-workspace]\nworkspace = '{recent_ws}'\n")
92
+ return workspaces_dir
93
+
94
+ @classmethod
95
+ def most_recent_workspace_name(cls):
96
+ workspaces_dir = cls.get_workspaces_dir()
97
+ all_dirs = [p for p in workspaces_dir.iterdir() if p.is_dir() and not p.name.startswith('.')]
98
+ if not all_dirs:
99
+ return None
100
+ latest = max(all_dirs, key=lambda p: p.stat().st_mtime)
101
+ return latest.name
67
102
 
68
103
  def get_workspace_dir(self):
69
- return self.get_workspaces_dir() / self.workspace_name
104
+ # workspace_name is established at instantiation. You want a new name? Initialize a new WorkspaceManager(). It manages one workpspace.
105
+ return self.get_workspaces_dir() / self.workspace_name
70
106
 
71
107
  def get_exports_dir(self):
72
108
  return self.workspace_dir / self.EXPORTS_DIR_NAME
@@ -185,24 +221,45 @@ class WorkspaceManager:
185
221
  """
186
222
  Class method that reads default-workspace.toml to identify the default-workspace path.
187
223
  """
188
- workspace_name = cls.identify_default_workspace_name()
224
+
189
225
  workspaces_dir = cls.ROOT_DIR / cls.WORKSPACES_DIR_NAME
190
- default_workspace_path = workspaces_dir / workspace_name
191
- if not default_workspace_path.exists():
192
- raise FileNotFoundError(f"Default workspace directory not found: {default_workspace_path}")
193
- return default_workspace_path
226
+ workspace_name = cls.identify_default_workspace_name()
227
+ if workspace_name is None:
228
+ workspace_name = cls.most_recent_workspace_name() # if
229
+ if workspace_name is None:
230
+ mulch.workspace(target_dir = workspaces_dir, scaffold = workspaces_dir / '.mulch' / 'mulch.toml', workspace_name = 'eds') # allow date based default if no workspace_name is provided
231
+ workspace_name = 'eds'
232
+ workspace_path = workspaces_dir / workspace_name
233
+ if not workspace_path.exists():
234
+ #raise FileNotFoundError(f"Workspace directory not found: {default_workspace_path}")
235
+ print("No default_workspace.toml file to identify a default workspace folder, so the most recently edited folder will be used.")
236
+
237
+ return workspace_path
238
+
239
+ '''
240
+ @classmethod
241
+ def identify_default_workspace_name(cls, workspaces_dir = None):
242
+ if workspaces_dir is None:
243
+ workspaces_dir = cls.get_workspaces_dir()
244
+ default_file = workspaces_dir / cls.DEFAULT_WORKSPACE_TOML_FILE_NAME
245
+ if not default_file.exists():
246
+ default_file.write_text("# Default workspace\n")
247
+ return str(default_file)
248
+ '''
249
+
194
250
  @classmethod
195
- def identify_default_workspace_name(cls):
251
+ def identify_default_workspace_name(cls, workspaces_dir = None):
196
252
  """
197
253
  Class method that reads default-workspace.toml to identify the default-workspace.
198
254
  """
199
-
200
- workspaces_dir = cls.ROOT_DIR / cls.WORKSPACES_DIR_NAME
255
+ if workspaces_dir is None:
256
+ workspaces_dir = cls.get_workspaces_dir()
201
257
  logging.info(f"workspaces_dir = {workspaces_dir}\n")
202
258
  default_toml_path = workspaces_dir / cls.DEFAULT_WORKSPACE_TOML_FILE_NAME
203
259
 
204
260
  if not default_toml_path.exists():
205
- raise FileNotFoundError(f"Missing {cls.DEFAULT_WORKSPACE_TOML_FILE_NAME} in {workspaces_dir}")
261
+ return None
262
+ #raise FileNotFoundError(f"Missing {cls.DEFAULT_WORKSPACE_TOML_FILE_NAME} in {workspaces_dir}")
206
263
 
207
264
  with open(default_toml_path, 'r') as f:
208
265
  data = toml.load(f)
@@ -244,16 +301,19 @@ class WorkspaceManager:
244
301
  return base / cls.APP_NAME
245
302
 
246
303
  @classmethod
247
- def ensure_workspace(cls) -> Path:
304
+ def ensure_appdata_workspaces_dir(cls) -> Path:
248
305
  """Create workspace folder and default toml if missing."""
249
- workspaces_dir = cls.get_appdata_dir() / "workspaces"
306
+ workspaces_dir = cls.get_appdata_dir() / cls.WORKSPACES_DIR_NAME
250
307
  workspaces_dir.mkdir(parents=True, exist_ok=True)
251
-
308
+ cls.workspaces_dir = workspaces_dir
252
309
  default_file = workspaces_dir / cls.DEFAULT_WORKSPACE_TOML_FILE_NAME
253
310
  if not default_file.exists():
254
- default_file.write_text("# Default workspace config\n")
311
+ pass
312
+ #default_file.write_text("# Default workspace config\n")
313
+ mulch_scaffold_toml = []
314
+ mulch.seed(target_dir = workspaces_dir,scaffold = mulch_scaffold_toml)
315
+ mulch.workspace(base_dir = workspaces_dir.parent, scaffold_file = workspaces_dir / '.mulch' / 'mulch.toml', workspace_name = 'eds') # allow date based default if no workspace_name is provided
255
316
  return workspaces_dir
256
-
257
317
 
258
318
  def establish_default_workspace():
259
319
  workspace_name = WorkspaceManager.identify_default_workspace_name()
File without changes
File without changes