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.
- pipeline/__init__.py +4 -0
- pipeline/__main__.py +1 -0
- pipeline/api/__init__.py +0 -0
- pipeline/api/eds.py +980 -0
- pipeline/api/rjn.py +157 -0
- pipeline/api/status_api.py +9 -0
- pipeline/calls.py +108 -0
- pipeline/cli.py +282 -0
- pipeline/configrationmanager.py +22 -0
- pipeline/decorators.py +13 -0
- pipeline/env.py +61 -0
- pipeline/environment.py +59 -0
- pipeline/gui_fastapi_plotly_live.py +78 -0
- pipeline/gui_mpl_live.py +113 -0
- pipeline/helpers.py +125 -0
- pipeline/logging_setup.py +45 -0
- pipeline/pastehelpers.py +10 -0
- pipeline/philosophy.py +62 -0
- pipeline/plotbuffer.py +21 -0
- pipeline/points_loader.py +19 -0
- pipeline/queriesmanager.py +122 -0
- pipeline/time_manager.py +211 -0
- pipeline/workspace_manager.py +253 -0
- pipeline_eds-0.2.4.dist-info/LICENSE +14 -0
- pipeline_eds-0.2.4.dist-info/METADATA +238 -0
- pipeline_eds-0.2.4.dist-info/RECORD +62 -0
- pipeline_eds-0.2.4.dist-info/WHEEL +4 -0
- pipeline_eds-0.2.4.dist-info/entry_points.txt +6 -0
- workspaces/default-workspace.toml +3 -0
- workspaces/eds_to_rjn/__init__.py +0 -0
- workspaces/eds_to_rjn/code/__init__.py +0 -0
- workspaces/eds_to_rjn/code/aggregator.py +84 -0
- workspaces/eds_to_rjn/code/collector.py +60 -0
- workspaces/eds_to_rjn/code/sanitizer.py +40 -0
- workspaces/eds_to_rjn/code/storage.py +16 -0
- workspaces/eds_to_rjn/configurations/config_time.toml +11 -0
- workspaces/eds_to_rjn/configurations/configuration.toml +2 -0
- workspaces/eds_to_rjn/exports/README.md +7 -0
- workspaces/eds_to_rjn/exports/aggregate/README.md +7 -0
- workspaces/eds_to_rjn/exports/aggregate/live_data - Copy.csv +355 -0
- workspaces/eds_to_rjn/exports/aggregate/live_data_EFF.csv +17521 -0
- workspaces/eds_to_rjn/exports/aggregate/live_data_INF.csv +17521 -0
- workspaces/eds_to_rjn/exports/export_eds_points_neo.txt +11015 -0
- workspaces/eds_to_rjn/exports/manual_data_load_to_postman_wetwell.csv +8759 -0
- workspaces/eds_to_rjn/exports/manual_data_load_to_postman_wetwell.xlsx +0 -0
- workspaces/eds_to_rjn/exports/manual_effluent.csv +8759 -0
- workspaces/eds_to_rjn/exports/manual_influent.csv +8759 -0
- workspaces/eds_to_rjn/exports/manual_wetwell.csv +8761 -0
- workspaces/eds_to_rjn/history/time_sample.txt +0 -0
- workspaces/eds_to_rjn/imports/zdMaxson_idcsD321E_sid11003.toml +14 -0
- workspaces/eds_to_rjn/imports/zdMaxson_idcsFI8001_sid8528.toml +14 -0
- workspaces/eds_to_rjn/imports/zdMaxson_idcsM100FI_sid2308.toml +14 -0
- workspaces/eds_to_rjn/imports/zdMaxson_idcsM310LI_sid2382.toml +14 -0
- workspaces/eds_to_rjn/queries/default-queries.toml +4 -0
- workspaces/eds_to_rjn/queries/points-maxson.csv +4 -0
- workspaces/eds_to_rjn/queries/points-stiles.csv +4 -0
- workspaces/eds_to_rjn/queries/timestamps_success.json +20 -0
- workspaces/eds_to_rjn/scripts/__init__.py +0 -0
- workspaces/eds_to_rjn/scripts/daemon_runner.py +212 -0
- workspaces/eds_to_rjn/secrets/README.md +24 -0
- workspaces/eds_to_rjn/secrets/secrets-example.yaml +15 -0
- workspaces/eds_to_termux/..txt +0 -0
pipeline/environment.py
ADDED
@@ -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)
|
pipeline/gui_mpl_live.py
ADDED
@@ -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)
|
pipeline/pastehelpers.py
ADDED
@@ -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
|
+
|