pipeline-eds 0.2.4__py3-none-any.whl

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 (62) hide show
  1. pipeline/__init__.py +4 -0
  2. pipeline/__main__.py +1 -0
  3. pipeline/api/__init__.py +0 -0
  4. pipeline/api/eds.py +980 -0
  5. pipeline/api/rjn.py +157 -0
  6. pipeline/api/status_api.py +9 -0
  7. pipeline/calls.py +108 -0
  8. pipeline/cli.py +282 -0
  9. pipeline/configrationmanager.py +22 -0
  10. pipeline/decorators.py +13 -0
  11. pipeline/env.py +61 -0
  12. pipeline/environment.py +59 -0
  13. pipeline/gui_fastapi_plotly_live.py +78 -0
  14. pipeline/gui_mpl_live.py +113 -0
  15. pipeline/helpers.py +125 -0
  16. pipeline/logging_setup.py +45 -0
  17. pipeline/pastehelpers.py +10 -0
  18. pipeline/philosophy.py +62 -0
  19. pipeline/plotbuffer.py +21 -0
  20. pipeline/points_loader.py +19 -0
  21. pipeline/queriesmanager.py +122 -0
  22. pipeline/time_manager.py +211 -0
  23. pipeline/workspace_manager.py +253 -0
  24. pipeline_eds-0.2.4.dist-info/LICENSE +14 -0
  25. pipeline_eds-0.2.4.dist-info/METADATA +238 -0
  26. pipeline_eds-0.2.4.dist-info/RECORD +62 -0
  27. pipeline_eds-0.2.4.dist-info/WHEEL +4 -0
  28. pipeline_eds-0.2.4.dist-info/entry_points.txt +6 -0
  29. workspaces/default-workspace.toml +3 -0
  30. workspaces/eds_to_rjn/__init__.py +0 -0
  31. workspaces/eds_to_rjn/code/__init__.py +0 -0
  32. workspaces/eds_to_rjn/code/aggregator.py +84 -0
  33. workspaces/eds_to_rjn/code/collector.py +60 -0
  34. workspaces/eds_to_rjn/code/sanitizer.py +40 -0
  35. workspaces/eds_to_rjn/code/storage.py +16 -0
  36. workspaces/eds_to_rjn/configurations/config_time.toml +11 -0
  37. workspaces/eds_to_rjn/configurations/configuration.toml +2 -0
  38. workspaces/eds_to_rjn/exports/README.md +7 -0
  39. workspaces/eds_to_rjn/exports/aggregate/README.md +7 -0
  40. workspaces/eds_to_rjn/exports/aggregate/live_data - Copy.csv +355 -0
  41. workspaces/eds_to_rjn/exports/aggregate/live_data_EFF.csv +17521 -0
  42. workspaces/eds_to_rjn/exports/aggregate/live_data_INF.csv +17521 -0
  43. workspaces/eds_to_rjn/exports/export_eds_points_neo.txt +11015 -0
  44. workspaces/eds_to_rjn/exports/manual_data_load_to_postman_wetwell.csv +8759 -0
  45. workspaces/eds_to_rjn/exports/manual_data_load_to_postman_wetwell.xlsx +0 -0
  46. workspaces/eds_to_rjn/exports/manual_effluent.csv +8759 -0
  47. workspaces/eds_to_rjn/exports/manual_influent.csv +8759 -0
  48. workspaces/eds_to_rjn/exports/manual_wetwell.csv +8761 -0
  49. workspaces/eds_to_rjn/history/time_sample.txt +0 -0
  50. workspaces/eds_to_rjn/imports/zdMaxson_idcsD321E_sid11003.toml +14 -0
  51. workspaces/eds_to_rjn/imports/zdMaxson_idcsFI8001_sid8528.toml +14 -0
  52. workspaces/eds_to_rjn/imports/zdMaxson_idcsM100FI_sid2308.toml +14 -0
  53. workspaces/eds_to_rjn/imports/zdMaxson_idcsM310LI_sid2382.toml +14 -0
  54. workspaces/eds_to_rjn/queries/default-queries.toml +4 -0
  55. workspaces/eds_to_rjn/queries/points-maxson.csv +4 -0
  56. workspaces/eds_to_rjn/queries/points-stiles.csv +4 -0
  57. workspaces/eds_to_rjn/queries/timestamps_success.json +20 -0
  58. workspaces/eds_to_rjn/scripts/__init__.py +0 -0
  59. workspaces/eds_to_rjn/scripts/daemon_runner.py +212 -0
  60. workspaces/eds_to_rjn/secrets/README.md +24 -0
  61. workspaces/eds_to_rjn/secrets/secrets-example.yaml +15 -0
  62. workspaces/eds_to_termux/..txt +0 -0
@@ -0,0 +1,59 @@
1
+ '''
2
+ Title: environment.py
3
+ Author: Clayton Bennett
4
+ Created: 23 July 2024
5
+ '''
6
+ import platform
7
+ import sys
8
+
9
+ def vercel():
10
+ #return not(windows()) # conflated, when using any linux that is not a webserver
11
+ # the important questions is actually "are we running on a webserver?"
12
+ return False # hard code this
13
+
14
+ def matplotlib_enabled():
15
+ if is_termux():
16
+ return False
17
+ else:
18
+ try:
19
+ import matplotlib
20
+ except ImportError:
21
+ return False
22
+
23
+ def fbx_enabled():
24
+ if is_termux():
25
+ return False
26
+ else:
27
+ return True
28
+
29
+ def is_termux():
30
+ # There might be other android versions that can work with the rise od Debian on android in 2025, but for now, assume all android is termux.
31
+ # I wonder how things would go on pydroid3
32
+ return is_android()
33
+
34
+ def is_android():
35
+ return "android" in platform.platform().lower()
36
+
37
+ def windows():
38
+ if 'win' in platform.platform().lower():
39
+ windows=True
40
+ else:
41
+ windows=False
42
+ return windows
43
+
44
+ def pyinstaller():
45
+ if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
46
+ pyinstaller = True
47
+ else:
48
+ pyinstaller = False
49
+ return pyinstaller
50
+
51
+ def frozen():
52
+ if getattr(sys, 'frozen', True):
53
+ frozen = True
54
+ else:
55
+ frozen = False
56
+ return frozen
57
+
58
+ def operatingsystem():
59
+ return platform.system() #determine OS
@@ -0,0 +1,78 @@
1
+ # src/pipeline/gui_fastapi_plotly_live.py
2
+
3
+ from fastapi import FastAPI
4
+ from fastapi.responses import HTMLResponse, JSONResponse
5
+ from threading import Lock
6
+ import uvicorn
7
+ import time
8
+ import threading
9
+ import webbrowser
10
+
11
+ app = FastAPI()
12
+ plot_buffer = None # Set externally
13
+ buffer_lock = Lock()
14
+
15
+ HTML_TEMPLATE = """
16
+ <!doctype html>
17
+ <html>
18
+ <head>
19
+ <title>Live Plot</title>
20
+ <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
21
+ <meta charset="UTF-8" />
22
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
23
+ </head>
24
+ <body>
25
+ <h2>Live EDS Data Plot</h2>
26
+ <div id="live-plot" style="width:90%;height:80vh;"></div>
27
+ <script>
28
+ async function fetchData() {
29
+ const res = await fetch("/data");
30
+ return await res.json();
31
+ }
32
+
33
+ async function updatePlot() {
34
+ const data = await fetchData();
35
+ const traces = [];
36
+
37
+ for (const [label, series] of Object.entries(data)) {
38
+ traces.push({
39
+ x: series.x,
40
+ y: series.y,
41
+ name: label,
42
+ mode: 'lines+markers',
43
+ type: 'scatter'
44
+ });
45
+ }
46
+ Plotly.newPlot('live-plot', traces, { margin: { t: 30 } });
47
+ }
48
+
49
+ setInterval(updatePlot, 2000); // Refresh every 2 seconds
50
+ updatePlot(); // Initial load
51
+ </script>
52
+ </body>
53
+ </html>
54
+ """
55
+
56
+ @app.get("/", response_class=HTMLResponse)
57
+ async def index():
58
+ return HTML_TEMPLATE
59
+
60
+ @app.get("/data", response_class=JSONResponse)
61
+ async def get_data():
62
+ with buffer_lock:
63
+ data = plot_buffer.get_all() # Should return { label: {"x": [...], "y": [...]}, ... }
64
+ return data
65
+
66
+ def open_browser(port):
67
+ time.sleep(1) # Give server a moment to start
68
+ ## Open in a new Chrome window (if installed)
69
+ ##chrome_path = webbrowser.get(using='windows-default')
70
+ ##chrome_path.open_new(f"http://127.0.0.1:{port}")
71
+
72
+ webbrowser.open(f"http://127.0.0.1:{port}")
73
+
74
+ def run_gui(buffer, port=8000):
75
+ global plot_buffer
76
+ plot_buffer = buffer
77
+ 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)
@@ -0,0 +1,113 @@
1
+ import numpy as np
2
+ import time
3
+ import logging
4
+ import matplotlib.pyplot as plt
5
+ import matplotlib.animation as animation
6
+ from src.pipeline import helpers
7
+ from src.pipeline.plotbuffer import PlotBuffer # Adjust import path as needed
8
+ from src.pipeline.time_manager import TimeManager
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ PADDING_RATIO = 0.25
13
+
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
+ def run_gui(buffer: PlotBuffer, update_interval_ms=1000):
37
+ """
38
+ Runs a matplotlib live updating plot based on the PlotBuffer content.
39
+ `update_interval_ms` controls how often the plot refreshes (default 1000ms = 1s).
40
+ """
41
+ # plt.style.use('seaborn-darkgrid')
42
+ plt.style.use('ggplot') # matplotlib built-in style as a lightweight alternative
43
+
44
+ fig, ax = plt.subplots(figsize=(10, 6))
45
+ ax.set_title("Live Pipeline Data")
46
+ ax.set_xlabel("Time")
47
+ ax.set_ylabel("Value")
48
+
49
+ lines = {}
50
+ legend_labels = []
51
+
52
+ def init():
53
+ ax.clear()
54
+ ax.set_title("Live Pipeline Data")
55
+ ax.set_xlabel("Time")
56
+ ax.set_ylabel("Value")
57
+ return []
58
+
59
+ def update(frame):
60
+ data = buffer.get_all()
61
+ if not data:
62
+ return []
63
+
64
+ # Add/update lines for each series
65
+ for label, series in data.items():
66
+ x_vals = series["x"]
67
+ y_vals = series["y"]
68
+ # Decide how many ticks you want (e.g., max 6)
69
+ num_ticks = min(6, len(x_vals))
70
+
71
+ # Choose evenly spaced indices
72
+ indices = np.linspace(0, len(x_vals) - 1, num_ticks, dtype=int)
73
+
74
+ if label not in lines:
75
+ # Create new line
76
+ line, = ax.plot(x_vals, y_vals, label=label)
77
+ lines[label] = line
78
+ legend_labels.append(label)
79
+ ax.legend()
80
+ else:
81
+ lines[label].set_data(x_vals, y_vals)
82
+
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
+ # Format x-axis ticks as human readable time strings
89
+
90
+ # Tick positions are x values at those indices
91
+ #tick_positions = x_vals[indices]
92
+ tick_positions = np.array(x_vals)[indices]
93
+ tick_labels = [TimeManager(ts).as_formatted_time() for ts in tick_positions]
94
+ # Convert UNIX timestamps to formatted strings on x-axis
95
+ #xticks = ax.get_xticks()
96
+ #xtick_labels = [TimeManager(x).as_formatted_time() for x in xticks]
97
+ ax.set_xticks(tick_positions)
98
+ #ax.set_xticklabels(xtick_labels, rotation=45, ha='right')
99
+ ax.set_xticklabels(tick_labels, rotation=45, ha='right')
100
+
101
+ return list(lines.values())
102
+
103
+ ani = animation.FuncAnimation(
104
+ fig,
105
+ update,
106
+ init_func=init,
107
+ interval=update_interval_ms,
108
+ blit=False # blit=True can be tricky with multiple lines and dynamic axes
109
+ )
110
+
111
+ plt.tight_layout()
112
+ plt.show()
113
+
pipeline/helpers.py ADDED
@@ -0,0 +1,125 @@
1
+ import json
2
+ import toml
3
+ from datetime import datetime
4
+ import inspect
5
+ import types
6
+ import os
7
+ import logging
8
+ import socket
9
+
10
+ from src.pipeline.time_manager import TimeManager
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def load_json(filepath):
16
+ if not os.path.exists(filepath):
17
+ logger.warning(f"[load_json] File not found: {filepath}")
18
+ return {}
19
+
20
+ if os.path.getsize(filepath) == 0:
21
+ logger.warning(f"[load_json] File is empty: {filepath}")
22
+ return {}
23
+
24
+ try:
25
+ with open(filepath, 'r') as file:
26
+ return json.load(file)
27
+ except json.JSONDecodeError as e:
28
+ logger.error(f"[load_json] Failed to decode JSON in {filepath}: {e}")
29
+ return {}
30
+
31
+ def load_toml(filepath):
32
+ # Load TOML data from the file
33
+ with open(filepath, 'r') as f:
34
+ dic_toml = toml.load(f)
35
+ return dic_toml
36
+
37
+ #def round_datetime_to_nearest_past_five_minutes(dt: datetime) -> datetime:
38
+ def round_datetime_to_nearest_past_five_minutes(dt):
39
+ #print(f"dt = {dt}")
40
+ allowed_minutes = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]
41
+ # Find the largest allowed minute <= current minute
42
+ rounded_minute = max(m for m in allowed_minutes if m <= dt.minute)
43
+ return dt.replace(minute=rounded_minute, second=0, microsecond=0)
44
+
45
+ def get_now_time_rounded(workspace_manager):# -> int:
46
+ '''
47
+ workspace_manager is included here so that references can be made to the configured timezone
48
+ '''
49
+ logger.debug(f"helpers.get_now_time_rounded(workspace_manager)")
50
+ nowtime = round_datetime_to_nearest_past_five_minutes(datetime.now())
51
+ logger.debug(f"rounded nowtime = {nowtime}")
52
+ nowtime_local = int(nowtime.timestamp())+300
53
+ nowtime_local = TimeManager(nowtime_local).as_datetime()
54
+ if False:
55
+ try:
56
+ config = load_toml(workspace_manager.get_configuration_file_path())
57
+ timezone_config = config["settings"]["timezone"]
58
+ except:
59
+ timezone_config = "America/Chicago"
60
+ nowtime_utc = TimeManager.from_local(nowtime_local, zone_name = timezone_config).as_unix()
61
+ logger.debug(f"return nowtime_utc")
62
+ return nowtime_utc
63
+ else:
64
+ logger.debug(f"return nowtime_local")
65
+ return TimeManager(nowtime_local).as_unix() # nowtime_utc
66
+
67
+ def function_view(globals_passed=None):
68
+ # Use the calling frame to get info about the *caller* module
69
+ caller_frame = inspect.stack()[1].frame
70
+ if globals_passed is None:
71
+ globals_passed = caller_frame.f_globals
72
+
73
+ # Get filename → basename only (e.g., 'calls.py')
74
+ filename = os.path.basename(caller_frame.f_code.co_filename)
75
+
76
+ print(f"Functions defined in {filename}:")
77
+
78
+ for name, obj in list(globals_passed.items()):
79
+ if isinstance(obj, types.FunctionType):
80
+ if getattr(obj, "__module__", None) == globals_passed.get('__name__', ''):
81
+ print(f" {name}")
82
+ print("\n")
83
+
84
+ #def get_nested_config(dct: dict, keys: list[str]):
85
+ def get_nested_config(dct, keys):
86
+ """Retrieve nested dict value by keys list; raise KeyError with full path if missing."""
87
+ current = dct
88
+ for key in keys:
89
+ try:
90
+ current = current[key]
91
+ except KeyError as e:
92
+ full_path = " -> ".join(keys)
93
+ raise KeyError(f"Missing required configuration at path: {full_path}") from e
94
+ return current
95
+
96
+ def human_readable(ts):
97
+ return datetime.fromtimestamp(ts).strftime("%H:%M:%S")
98
+
99
+ def iso(ts):
100
+ return datetime.fromtimestamp(ts).isoformat()
101
+
102
+ def get_lan_ip_address_of_current_machine():
103
+ """Get the LAN IP address of the current machine."""
104
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
105
+ try:
106
+ # Doesn't need to be reachable; just picks the active interface
107
+ s.connect(("8.8.8.8", 80))
108
+ return s.getsockname()[0]
109
+ finally:
110
+ s.close()
111
+
112
+ def nice_step(delta_sec: int) -> int:
113
+ """
114
+ Return a "nice" step in seconds (1,2,5,10,15,30,60,120,...)
115
+ """
116
+ nice_numbers = [1, 2, 5, 10, 15, 30, 60, 120, 300, 600, 900, 1800, 3600, 7200, 14400, 28800, 86400]
117
+ target_step = delta_sec // 400 # aim for ~400 points
118
+ # find the smallest nice_number >= target_step
119
+ for n in nice_numbers:
120
+ if n >= target_step:
121
+ return n
122
+ return nice_numbers[-1]
123
+
124
+ if __name__ == "__main__":
125
+ function_view()
@@ -0,0 +1,45 @@
1
+ import os
2
+ import json
3
+ import logging.config
4
+
5
+ def setup_logging(config_file="config/logging.json"): # relative to root
6
+ if not os.path.exists("logs"):
7
+ os.makedirs("logs")
8
+ with open(config_file, 'r') as f:
9
+ config = json.load(f)
10
+ logging.config.dictConfig(config)
11
+
12
+ # Dynamically set logging level, per handler, like when using a GUI
13
+ #def set_handler_level(handler_name: str, level_name: str):
14
+ def set_handler_level(handler_name, level_name):
15
+ logger = logging.getLogger() # root logger
16
+ level = getattr(logging, level_name.upper(), None)
17
+ if level is None:
18
+ raise ValueError(f"Invalid log level: {level_name}")
19
+
20
+ for handler in logger.handlers:
21
+ if handler.get_name() == handler_name:
22
+ handler.setLevel(level)
23
+ logging.info(f"Set {handler_name} handler level to {level_name}")
24
+ break
25
+ else:
26
+ raise ValueError(f"Handler '{handler_name}' not found")
27
+
28
+ # Custom JSON formatter
29
+ class JSONFormatter(logging.Formatter):
30
+ def format(self, record):
31
+ log_record = {
32
+ "time": self.formatTime(record, "%Y-%m-%d %H:%M:%S"),
33
+ "level": record.levelname,
34
+ "message": record.getMessage(),
35
+ }
36
+ return json.dumps(log_record, indent=4)
37
+
38
+ class PrettyJSONFormatter(logging.Formatter):
39
+ def format(self, record):
40
+ if isinstance(record.msg, (dict, list)):
41
+ try:
42
+ record.msg = json.dumps(record.msg, indent=2, ensure_ascii=False)
43
+ except Exception:
44
+ pass # fallback to default formatting
45
+ return super().format(record)
@@ -0,0 +1,10 @@
1
+ '''
2
+ title: pastehelpers.py
3
+ author: Clayton Bennett
4
+ created: 30 July 2025
5
+
6
+ why: These functions will not be useful if imported. You'll need to paste the meat.
7
+ '''
8
+ import inspect
9
+ def current_function_name():
10
+ return inspect.currentframe().f_code.co_name
pipeline/philosophy.py ADDED
@@ -0,0 +1,62 @@
1
+ class Philosophy:
2
+ """
3
+ A playful class embodying the philosophy of Python's special (dunder) methods.
4
+ Use this to reflect on Python design or teach how dunders work.
5
+ """
6
+
7
+ def __new__(cls, *args, **kwargs):
8
+ print("Creating a new philosophical instance...")
9
+ return super().__new__(cls)
10
+
11
+ def __init__(self):
12
+ self.truth = 42
13
+ self.meaning = "Pythonic reflection"
14
+
15
+ def __repr__(self):
16
+ return '"__str__ is for users. __repr__ is for developers."'
17
+
18
+ def __str__(self):
19
+ return "Zen of Python: Beautiful is better than ugly."
20
+
21
+ def __bytes__(self):
22
+ return b"In the face of ambiguity, refuse the temptation to guess."
23
+
24
+ def __bool__(self):
25
+ return True # Philosophy is always truthy
26
+
27
+ def __len__(self):
28
+ return len(self.meaning)
29
+
30
+ def __getitem__(self, key):
31
+ return f"Indexing into philosophy returns insight[{key}]"
32
+
33
+ def __setitem__(self, key, value):
34
+ print(f"Attempting to assign {value!r} to insight[{key}]... but wisdom is immutable.")
35
+
36
+ def __call__(self, question):
37
+ return f"You asked: {question!r}. Python answers with clarity."
38
+
39
+ def __eq__(self, other):
40
+ return isinstance(other, Philosophy)
41
+
42
+ def __add__(self, other):
43
+ return "Philosophy enriched with " + str(other)
44
+
45
+ def __contains__(self, item):
46
+ return item.lower() in self.meaning.lower()
47
+
48
+ def __iter__(self):
49
+ yield from self.meaning.split()
50
+
51
+ def __enter__(self):
52
+ print("Entering a context of enlightenment.")
53
+ return self
54
+
55
+ def __exit__(self, exc_type, exc_val, exc_tb):
56
+ print("Exiting the context. Wisdom retained.")
57
+
58
+ def __del__(self):
59
+ print("A philosophical idea vanishes... but not forgotten.")
60
+
61
+ def __hash__(self):
62
+ return hash((self.truth, self.meaning))
pipeline/plotbuffer.py ADDED
@@ -0,0 +1,21 @@
1
+ # src/pipeline/plotbuffer.py
2
+ from collections import defaultdict
3
+
4
+ KEEP_ALL_LIVE_POINTS = True
5
+
6
+ class PlotBuffer:
7
+ def __init__(self, max_points=100):
8
+ self.data = defaultdict(lambda: {"x": [], "y": []})
9
+ self.max_points = max_points
10
+
11
+ def append(self, label, x, y):
12
+ self.data[label]["x"].append(x)
13
+ self.data[label]["y"].append(y)
14
+
15
+ if len(self.data[label]["x"]) > self.max_points:
16
+ if not KEEP_ALL_LIVE_POINTS:
17
+ self.data[label]["x"].pop(0)
18
+ self.data[label]["y"].pop(0)
19
+
20
+ def get_all(self):
21
+ return self.data
@@ -0,0 +1,19 @@
1
+ # src/points_loader.py
2
+
3
+ import csv
4
+
5
+ class PointsCsvLoader:
6
+ """
7
+ loader = PointsCsvLoader(csv_path)
8
+ points_list = loader.load_points()
9
+ """
10
+ def __init__(self, csv_file_path: str):
11
+ self.csv_file_path = csv_file_path
12
+ self.points = []
13
+
14
+ def load_points(self):
15
+ with open(self.csv_file_path, mode='r', newline='') as file:
16
+ reader = csv.DictReader(file)
17
+ for row in reader:
18
+ self.points.append(row)
19
+ return self.points
@@ -0,0 +1,122 @@
1
+ import os
2
+ import toml
3
+ from datetime import datetime
4
+ import json
5
+ import csv
6
+ from collections import defaultdict
7
+ import logging
8
+
9
+ from src.pipeline import helpers
10
+ from src.pipeline.time_manager import TimeManager
11
+
12
+ logger = logging.getLogger(__name__)
13
+ '''
14
+ Goal:
15
+ Set up to use the most recent query:
16
+ use-most-recently-edited-query-file = true # while true, this will ignore the files variable list and instead use a single list of the most recent files
17
+ '''
18
+
19
+ class QueriesManager:
20
+ #def __init__(self, workspace_manager: object):
21
+ def __init__(self, workspace_manager):
22
+ self.workspace_manager = workspace_manager
23
+ logger.info(f"QueriesManager using project: {self.workspace_manager.workspace_name}")
24
+ if not workspace_manager:
25
+ raise ValueError("workspace_manager must be provided and not None.")
26
+ self.workspace_manager = workspace_manager
27
+
28
+
29
+ def load_tracking(self):
30
+ file_path = self.workspace_manager.get_timestamp_success_file_path()
31
+ try:
32
+ #logger.info({"Trying to load tracking file at": file_path})
33
+ logger.debug({
34
+ "event": "Loading tracking file",
35
+ "path": str(file_path)
36
+ })
37
+ data = helpers.load_json(file_path)
38
+ #logger.info({"Tracking data loaded": data})
39
+ logger.debug({
40
+ "event": "Tracking data loaded",
41
+ "data": data
42
+ })
43
+
44
+ return data
45
+ except FileNotFoundError:
46
+ return {}
47
+
48
+ def save_tracking(self,data):
49
+ file_path = self.workspace_manager.get_timestamp_success_file_path()
50
+ with open(file_path, 'w') as f:
51
+ json.dump(data, f, indent=2)
52
+
53
+ def get_most_recent_successful_timestamp(self, api_id):# -> int:
54
+ print("QueriesManager.get_most_recent_successful_timestamp()")
55
+ from src.pipeline.helpers import load_toml
56
+ try:
57
+ config = load_toml(self.workspace_manager.get_configuration_file_path())
58
+ timezone_config = config["settings"]["timezone"]
59
+ except:
60
+ timezone_config = "America/Chicago"
61
+
62
+ data = self.load_tracking()
63
+
64
+ if not data:
65
+ # No stored value found — go back one hour from now, rounded down to nearest 5 minutes
66
+ one_hour_ago_local = TimeManager.now().as_unix() - 3600 # now - 1 hour in unix seconds
67
+ one_hour_ago_local = TimeManager(one_hour_ago_local).as_datetime()
68
+ #one_hour_ago_utc = TimeManager.from_local(one_hour_ago_local, zone_name = timezone_config)
69
+ tm = TimeManager(one_hour_ago_local).round_down_to_nearest_five()
70
+ else:
71
+ # Stored value found — parse ISO timestamp and round down to nearest 5 minutes
72
+ last_success_iso = TimeManager(data[api_id]["timestamps"]["last_success"]).as_datetime()
73
+ #last_success_utc = TimeManager.from_local(last_success_iso, zone_name = timezone_config).as_datetime()
74
+ tm = TimeManager(last_success_iso).round_down_to_nearest_five()
75
+
76
+ return tm
77
+
78
+
79
+ def update_success(self,api_id,success_time=None):
80
+ # This should be called when data is definitely transmitted to the target API.
81
+ # A confirmation algorithm might be in order, like calling back the data and checking it against the original.
82
+ data = self.load_tracking()
83
+ if api_id not in data:
84
+ data[api_id] = {"timestamps": {}}
85
+ #now = success_time or datetime.now().isoformat()
86
+ now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
87
+ data[api_id]["timestamps"]["last_success"] = now
88
+ data[api_id]["timestamps"]["last_attempt"] = now
89
+ self.save_tracking(data)
90
+
91
+ def update_attempt(self, api_id):
92
+ data = self.load_tracking()
93
+ if api_id not in data:
94
+ logger.info(f"Creating new tracking entry for {api_id}")
95
+ data[api_id] = {"timestamps": {}}
96
+ #now = datetime.now().isoformat()
97
+ now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
98
+ data[api_id]["timestamps"]["last_attempt"] = now
99
+ self.save_tracking(data)
100
+ logger.info(f"Updated last_attempt for {api_id}: {now}")
101
+
102
+ def load_query_rows_from_csv_files(csv_paths_list):
103
+ queries_dictlist_unfiltered = []
104
+ for csv_path in csv_paths_list:
105
+ with open(csv_path, newline='') as csvfile:
106
+ reader = csv.DictReader(csvfile)
107
+ for row in reader:
108
+ queries_dictlist_unfiltered.append(row)
109
+ return queries_dictlist_unfiltered
110
+
111
+ def group_queries_by_col(queries_array,grouping_var_str='zd'):
112
+ queries_array_grouped = defaultdict(list)
113
+ for row in queries_array:
114
+ row_filter = row[grouping_var_str]
115
+ queries_array_grouped[row_filter].append(row)
116
+ return queries_array_grouped
117
+
118
+ if __name__ == "__main__":
119
+ pass
120
+
121
+
122
+