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/time_manager.py
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
from datetime import datetime, timezone
|
2
|
+
from typing import Union
|
3
|
+
import click
|
4
|
+
try:
|
5
|
+
from zoneinfo import ZoneInfo
|
6
|
+
except ImportError:
|
7
|
+
from backports.zoneinfo import ZoneInfo
|
8
|
+
|
9
|
+
|
10
|
+
class TimeManager:
|
11
|
+
"""
|
12
|
+
TimeManager is a flexible utility for handling various datetime representations.
|
13
|
+
|
14
|
+
Supports initialization from:
|
15
|
+
- ISO 8601 string (e.g., "2025-07-19T15:00:00Z")
|
16
|
+
- Formatted datetime string (e.g., "2025-07-19 15:00:00")
|
17
|
+
- Unix timestamp as int or float
|
18
|
+
- datetime.datetime object
|
19
|
+
|
20
|
+
Example usage:
|
21
|
+
tm1 = TimeManager("2025-07-19T15:00:00Z") # ISO 8601
|
22
|
+
tm2 = TimeManager("2025-07-19 15:00:00") # formatted string
|
23
|
+
tm3 = TimeManager(1752946800) # unix int
|
24
|
+
tm4 = TimeManager(1752946800.0) # unix float
|
25
|
+
tm5 = TimeManager(datetime(2025, 7, 19, 15, 0)) # datetime object
|
26
|
+
|
27
|
+
print(tm1.as_formatted_date_time()) # → '2025-07-19 15:00:00'
|
28
|
+
print(tm1.as_formatted_time()) # → '15:00:00'
|
29
|
+
print(tm1.as_isoz()) # → '2025-07-19T15:00:00Z'
|
30
|
+
print(tm1.as_unix()) # → 1752946800
|
31
|
+
print(tm1.as_datetime()) # → datetime.datetime(2025, 7, 19, 15, 0)
|
32
|
+
|
33
|
+
rounded_tm = tm1.round_down_to_nearest_five()
|
34
|
+
print(rounded_tm.as_formatted_date_time())
|
35
|
+
|
36
|
+
now_tm = TimeManager.now()
|
37
|
+
now_rounded_tm = TimeManager.now_rounded_to_five()
|
38
|
+
"""
|
39
|
+
|
40
|
+
HOW_TO_UTCZ_DOC = """
|
41
|
+
# HOW TO CONVERT TIME BEFORE USING TIMEMANAGER
|
42
|
+
## 1. Create a datetime in Central Time
|
43
|
+
central_time = datetime(2025, 7, 19, 10, 0, tzinfo=ZoneInfo("America/Chicago"))
|
44
|
+
|
45
|
+
## 2. Convert to UTC
|
46
|
+
utc_time = central_time.astimezone(ZoneInfo("UTC"))
|
47
|
+
|
48
|
+
## 3. Use the TimeManager class to ensure ISO format (with Z).
|
49
|
+
utc_time_z = TimeManager(utc_time).as_isoz()
|
50
|
+
|
51
|
+
print("Central:", central_time)
|
52
|
+
print("UTC: ", utc_time)
|
53
|
+
|
54
|
+
# ALTERNATIVE METHODS
|
55
|
+
## - Prepare single timestamp (top of the hour UTC)
|
56
|
+
```
|
57
|
+
import datetime
|
58
|
+
timestamp = datetime.datetime.now(datetime.timezone.utc).replace(minute=0, second=0, microsecond=0)
|
59
|
+
timestamp_str = timestamp.strftime('%Y-%m-%d %H:%M:%S')
|
60
|
+
```
|
61
|
+
|
62
|
+
"""
|
63
|
+
|
64
|
+
def __init__(self, timestamp: Union[str, int, float, datetime]):
|
65
|
+
if isinstance(timestamp, datetime):
|
66
|
+
self._dt = timestamp.replace(tzinfo=timezone.utc)
|
67
|
+
elif isinstance(timestamp, (int, float)):
|
68
|
+
self._dt = datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
69
|
+
'''
|
70
|
+
elif isinstance(timestamp, str):
|
71
|
+
try:
|
72
|
+
# Use fromisoformat (Python 3.7+)
|
73
|
+
self._dt = datetime.fromisoformat(timestamp).replace(tzinfo=timezone.utc)
|
74
|
+
except ValueError:
|
75
|
+
self._dt = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
|
76
|
+
'''
|
77
|
+
elif isinstance(timestamp, str):
|
78
|
+
try:
|
79
|
+
if timestamp.endswith("Z"):
|
80
|
+
# Strip 'Z' and parse as UTC
|
81
|
+
self._dt = datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
|
82
|
+
else:
|
83
|
+
# Try ISO 8601 string with offset (e.g., +00:00)
|
84
|
+
self._dt = datetime.fromisoformat(timestamp).astimezone(timezone.utc)
|
85
|
+
except ValueError:
|
86
|
+
# Fallback to "YYYY-MM-DD HH:MM:SS"
|
87
|
+
self._dt = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
|
88
|
+
|
89
|
+
'''
|
90
|
+
elif isinstance(timestamp, str):
|
91
|
+
try:
|
92
|
+
# Try ISO 8601 with 'Z'
|
93
|
+
self._dt = datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
|
94
|
+
except ValueError:
|
95
|
+
# Try formatted string without timezone
|
96
|
+
self._dt = datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
|
97
|
+
'''
|
98
|
+
else:
|
99
|
+
raise TypeError(f"Unsupported timestamp type: {type(timestamp)}")
|
100
|
+
|
101
|
+
def as_datetime(self) -> datetime:
|
102
|
+
"""Return the internal datetime object (UTC)."""
|
103
|
+
return self._dt
|
104
|
+
|
105
|
+
def as_safe_isoformat_for_filename(self) -> str:
|
106
|
+
"""
|
107
|
+
Returns an ISO 8601 formatted UTC time string safe for use in filenames.
|
108
|
+
Example: '2025-07-19T23-35-00Z'
|
109
|
+
"""
|
110
|
+
return self.as_datetime().isoformat().replace(":", "-") + "Z"
|
111
|
+
|
112
|
+
def as_unix(self):# -> int:
|
113
|
+
"""Return the Unix timestamp as an integer."""
|
114
|
+
return int(self._dt.timestamp())
|
115
|
+
|
116
|
+
def as_isoz(self):# -> str:
|
117
|
+
"""Return ISO 8601 string (UTC) with 'Z' suffix."""
|
118
|
+
return self._dt.strftime("%Y-%m-%dT%H:%M:%SZ")
|
119
|
+
|
120
|
+
def as_iso(self):# -> str:
|
121
|
+
"""Return ISO 8601, like datetime.fromtimestamp(ts).isoformat()."""
|
122
|
+
return self._dt.isoformat()
|
123
|
+
|
124
|
+
def as_formatted_date_time(self):# -> str:
|
125
|
+
"""Return formatted string 'YYYY-MM-DD HH:MM:SS'."""
|
126
|
+
return self._dt.strftime("%Y-%m-%d %H:%M:%S")
|
127
|
+
|
128
|
+
def as_formatted_time(self):# -> str:
|
129
|
+
"""Return formatted string 'HH:MM:SS'."""
|
130
|
+
return self._dt.strftime("%H:%M:%S")
|
131
|
+
|
132
|
+
def as_excel(self):# -> float:
|
133
|
+
"""Returns Excel serial number for Windows (based on 1899-12-30 epoch)."""
|
134
|
+
unix_ts = self.as_unix()
|
135
|
+
return unix_ts / 86400 + 25569 # 86400 seconds in a day
|
136
|
+
|
137
|
+
def round_down_to_nearest_five(self):# -> "TimeManager":
|
138
|
+
"""Return new TimeManager rounded down to nearest 5-minute mark."""
|
139
|
+
minute = (self._dt.minute // 5) * 5
|
140
|
+
rounded_dt = self._dt.replace(minute=minute, second=0, microsecond=0)
|
141
|
+
return TimeManager(rounded_dt).as_unix()
|
142
|
+
|
143
|
+
@staticmethod
|
144
|
+
def now():# -> "TimeManager":
|
145
|
+
"""Return current UTC time as a TimeManager."""
|
146
|
+
return TimeManager(datetime.now(timezone.utc)).as_unix()
|
147
|
+
|
148
|
+
|
149
|
+
@staticmethod
|
150
|
+
#def from_local(dt: datetime, zone_name: str) -> "TimeManager":
|
151
|
+
def from_local(dt, zone_name):
|
152
|
+
"""
|
153
|
+
Convert a local datetime in the given time zone to UTC and return a TimeManager instance.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
dt (datetime): The local datetime (can be naive or aware).
|
157
|
+
zone_name (str): A valid IANA time zone string, e.g. 'America/Chicago'.
|
158
|
+
|
159
|
+
Returns:
|
160
|
+
TimeManager: A new instance based on the UTC version of the datetime.
|
161
|
+
"""
|
162
|
+
if dt.tzinfo is None:
|
163
|
+
local_dt = dt.replace(tzinfo=ZoneInfo(zone_name))
|
164
|
+
else:
|
165
|
+
local_dt = dt.astimezone(ZoneInfo(zone_name))
|
166
|
+
utc_dt = local_dt.astimezone(timezone.utc)
|
167
|
+
return TimeManager(utc_dt)
|
168
|
+
|
169
|
+
|
170
|
+
@staticmethod
|
171
|
+
def now_rounded_to_five():# -> "TimeManager":
|
172
|
+
"""Return current UTC time rounded down to nearest 5 minutes."""
|
173
|
+
now = datetime.now(timezone.utc)
|
174
|
+
minute = (now.minute // 5) * 5
|
175
|
+
rounded = now.replace(minute=minute, second=0, microsecond=0)
|
176
|
+
return TimeManager(rounded).as_unix()
|
177
|
+
|
178
|
+
def __repr__(self):
|
179
|
+
return f"TimeManager({self.as_isoz()})"
|
180
|
+
|
181
|
+
def __str__(self):
|
182
|
+
return self.as_formatted_date_time()
|
183
|
+
|
184
|
+
@click.command()
|
185
|
+
def main():
|
186
|
+
click.echo("WELCOME TO THE `TimeManager` CLASS")
|
187
|
+
click.echo("pipx install mulch")
|
188
|
+
click.echo("from mulch.time_manager import TimeManager")
|
189
|
+
click.echo("")
|
190
|
+
click.echo("test>> click.MultiCommand(True)")
|
191
|
+
click.MultiCommand(True)
|
192
|
+
click.echo("test>> click.MultiCommand()")
|
193
|
+
click.MultiCommand()
|
194
|
+
|
195
|
+
msg0 = ''' python time_manager.py # runs 'main' by default if you set it as default command (else error)
|
196
|
+
python time_manager.py main # run main
|
197
|
+
python time_manager.py easteregg # run easteregg
|
198
|
+
python time_manager.py howto-utcz # show how-to UTCZ doc
|
199
|
+
python time_manager.py license # show license
|
200
|
+
'''
|
201
|
+
click.echo(msg0)
|
202
|
+
def easteregg():
|
203
|
+
click.echo("from mulch.philosophy import Philosophy")
|
204
|
+
click.echo("PS C:Users/user/dev/pipeline >> poetry run python -m mulch.philosphy --help # click ")
|
205
|
+
|
206
|
+
def howto_utcz():
|
207
|
+
click.echo(TimeManager.HOW_TO_UTCZ_DOC)
|
208
|
+
def license():
|
209
|
+
pass
|
210
|
+
if __name__ == "__main__":
|
211
|
+
main()
|
@@ -0,0 +1,253 @@
|
|
1
|
+
import os
|
2
|
+
import toml
|
3
|
+
import logging
|
4
|
+
from pathlib import Path
|
5
|
+
|
6
|
+
'''
|
7
|
+
Goal:
|
8
|
+
Implement default-workspace.toml variable: use-most-recently-edited-workspace-directory
|
9
|
+
'''
|
10
|
+
|
11
|
+
# Configure logging (adjust level as needed)
|
12
|
+
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
13
|
+
|
14
|
+
class WorkspaceManager:
|
15
|
+
# It has been chosen to not make the WorkspaceManager a singleton if there is to be batch processing.
|
16
|
+
|
17
|
+
WORKSPACES_DIR_NAME = 'workspaces'
|
18
|
+
QUERIES_DIR_NAME = 'queries'
|
19
|
+
IMPORTS_DIR_NAME = 'imports'
|
20
|
+
EXPORTS_DIR_NAME = 'exports'
|
21
|
+
SCRIPTS_DIR_NAME = 'scripts'
|
22
|
+
CONFIGURATIONS_DIR_NAME = 'configurations'
|
23
|
+
SECRETS_DIR_NAME ='secrets'
|
24
|
+
LOGS_DIR_NAME = 'logs'
|
25
|
+
CONFIGURATION_FILE_NAME = 'configuration.toml'
|
26
|
+
SECRETS_YAML_FILE_NAME ='secrets.yaml'
|
27
|
+
SECRETS_EXAMPLE_YAML_FILE_NAME ='secrets-example.yaml'
|
28
|
+
DEFAULT_WORKSPACE_TOML_FILE_NAME = 'default-workspace.toml'
|
29
|
+
TIMESTAMPS_JSON_FILE_NAME = 'timestamps_success.json'
|
30
|
+
ROOT_DIR = Path(__file__).resolve().parents[2] # root directory
|
31
|
+
|
32
|
+
|
33
|
+
# This climbs out of /src/pipeline/ to find the root.
|
34
|
+
# parents[0] → The directory that contains the (this) Python file.
|
35
|
+
# parents[1] → The parent of that directory.
|
36
|
+
# parents[2] → The grandparent directory (which should be the root), if root_pipeline\src\pipeline\
|
37
|
+
# This organization anticipates PyPi packaging.
|
38
|
+
|
39
|
+
|
40
|
+
def __init__(self, workspace_name):
|
41
|
+
self.workspace_name = workspace_name
|
42
|
+
self.workspaces_dir = self.get_workspaces_dir()
|
43
|
+
self.workspace_dir = self.get_workspace_dir()
|
44
|
+
self.configurations_dir = self.get_configurations_dir()
|
45
|
+
self.exports_dir = self.get_exports_dir()
|
46
|
+
self.imports_dir = self.get_imports_dir()
|
47
|
+
self.queries_dir = self.get_queries_dir()
|
48
|
+
self.secrets_dir = self.get_secrets_dir()
|
49
|
+
self.scripts_dir = self.get_scripts_dir()
|
50
|
+
self.logs_dir = self.get_logs_dir()
|
51
|
+
self.aggregate_dir = self.get_aggregate_dir()
|
52
|
+
|
53
|
+
|
54
|
+
self.check_and_create_dirs(list_dirs =
|
55
|
+
[self.workspace_dir,
|
56
|
+
self.exports_dir,
|
57
|
+
self.imports_dir,
|
58
|
+
self.secrets_dir,
|
59
|
+
self.scripts_dir,
|
60
|
+
self.logs_dir,
|
61
|
+
self.aggregate_dir])
|
62
|
+
|
63
|
+
def get_workspaces_dir(self):
|
64
|
+
return self.ROOT_DIR / self.WORKSPACES_DIR_NAME
|
65
|
+
|
66
|
+
def get_workspace_dir(self):
|
67
|
+
return self.get_workspaces_dir() / self.workspace_name
|
68
|
+
|
69
|
+
def get_exports_dir(self):
|
70
|
+
return self.workspace_dir / self.EXPORTS_DIR_NAME
|
71
|
+
|
72
|
+
def get_exports_file_path(self, filename):
|
73
|
+
# Return the full path to the export file
|
74
|
+
return self.exports_dir / filename
|
75
|
+
|
76
|
+
def get_aggregate_dir(self):
|
77
|
+
# This is for five-minute aggregation data to be stored between hourly bulk passes
|
78
|
+
# This should become defunct once the tabular trend data request is functional
|
79
|
+
return self.exports_dir / 'aggregate'
|
80
|
+
|
81
|
+
def get_configurations_dir(self):
|
82
|
+
return self.workspace_dir / self.CONFIGURATIONS_DIR_NAME
|
83
|
+
|
84
|
+
def get_configuration_file_path(self):
|
85
|
+
# Return the full path to the config file or create it from the fallback copy if it exists
|
86
|
+
file_path = self.get_configurations_dir() / self.CONFIGURATION_FILE_NAME
|
87
|
+
return file_path
|
88
|
+
|
89
|
+
def get_logs_dir(self):
|
90
|
+
return self.workspace_dir / self.LOGS_DIR_NAME
|
91
|
+
|
92
|
+
def get_imports_dir(self):
|
93
|
+
return self.workspace_dir / self.IMPORTS_DIR_NAME
|
94
|
+
|
95
|
+
def get_imports_file_path(self, filename):
|
96
|
+
# Return the full path to the export file
|
97
|
+
return self.imports_dir / filename
|
98
|
+
|
99
|
+
def get_secrets_dir(self):
|
100
|
+
return self.workspace_dir / self.SECRETS_DIR_NAME
|
101
|
+
|
102
|
+
def get_secrets_file_path(self):
|
103
|
+
# Return the full path to the config file
|
104
|
+
file_path = self.secrets_dir / self.SECRETS_YAML_FILE_NAME
|
105
|
+
if not file_path.exists():
|
106
|
+
logging.warning(f"Secrets sonfiguration file {self.SECRETS_YAML_FILE_NAME} not found in:\n{self.secrets_dir}.\nHint: Copy and edit the {self.SECRETS_YAML_FILE_NAME}.")
|
107
|
+
print("\n")
|
108
|
+
choice = str(input(f"Auto-copy {self.SECRETS_EXAMPLE_YAML_FILE_NAME} [Y] or sys.exit() [n] ? "))
|
109
|
+
if choice.lower().startswith("y"):
|
110
|
+
file_path = self.get_secrets_file_path_or_copy()
|
111
|
+
else:
|
112
|
+
# edge case, expected once per machine, or less, if the user knows to set up a secrets.yaml file.
|
113
|
+
import sys
|
114
|
+
sys.exit()
|
115
|
+
return file_path
|
116
|
+
|
117
|
+
def get_secrets_file_path_or_copy(self):
|
118
|
+
# Return the full path to the config file or create it from the fallback copy if it exists
|
119
|
+
file_path = self.secrets_dir / self.SECRETS_YAML_FILE_NAME
|
120
|
+
fallback_file_path = self.secrets_dir / self.SECRETS_EXAMPLE_YAML_FILE_NAME
|
121
|
+
if not file_path.exists() and fallback_file_path.exists():
|
122
|
+
import shutil
|
123
|
+
shutil.copy(fallback_file_path, file_path)
|
124
|
+
print(f"{self.SECRETS_YAML_FILE_NAME} not found, copied from {self.SECRETS_YAML_FILE_NAME}")
|
125
|
+
elif not file_path.exists() and not fallback_file_path.exists():
|
126
|
+
raise FileNotFoundError(f"Configuration file {self.SECRETS_YAML_FILE_NAME} nor {self.SECRETS_EXAMPLE_YAML_FILE_NAME} not found in directory '{self.secrets_dir}'.")
|
127
|
+
return file_path
|
128
|
+
|
129
|
+
def get_scripts_dir(self):
|
130
|
+
return self.workspace_dir / self.SCRIPTS_DIR_NAME
|
131
|
+
|
132
|
+
def get_scripts_file_path(self, filename):
|
133
|
+
# Return the full path to the config file
|
134
|
+
return self.get_scripts_dir() / filename
|
135
|
+
|
136
|
+
def get_queries_dir(self):
|
137
|
+
return self.workspace_dir / self.QUERIES_DIR_NAME
|
138
|
+
|
139
|
+
def get_queries_file_path(self,filename): #
|
140
|
+
# Return the full path to the config file
|
141
|
+
filepath = self.get_queries_dir() / filename
|
142
|
+
if not filepath.exists():
|
143
|
+
raise FileNotFoundError(f"Query filepath={filepath} not found. \nPossible reason: You are in the wrong project directory.")
|
144
|
+
return filepath
|
145
|
+
|
146
|
+
def get_timestamp_success_file_path(self):
|
147
|
+
# Return the full path to the timestamp file
|
148
|
+
filepath = self.get_queries_dir() / self.TIMESTAMPS_JSON_FILE_NAME
|
149
|
+
logging.info(f"WorkspaceManager.get_timestamp_success_file_path() = {filepath}")
|
150
|
+
return filepath
|
151
|
+
|
152
|
+
def check_and_create_dirs(self, list_dirs):
|
153
|
+
for dir_path in list_dirs:
|
154
|
+
if not dir_path.exists():
|
155
|
+
dir_path.mkdir(parents=True, exist_ok=True)
|
156
|
+
|
157
|
+
@classmethod
|
158
|
+
def get_cwd(cls) -> Path:
|
159
|
+
"""Return current workspace directory, not the source code root, as a Path instance."""
|
160
|
+
# Quick and dirty, not representative of the complex truth or opportunity.
|
161
|
+
#cls.ROOT_DIR / 'workspaces' / cls.identify_default_workspace_name()
|
162
|
+
|
163
|
+
# Pre-Exisiting function, generated some time before July 24, 2025. May as well use that instead. It is good to use your own library. Benefit from having built it.
|
164
|
+
return cls.identify_default_workspace_name()
|
165
|
+
|
166
|
+
@classmethod
|
167
|
+
def get_all_workspaces_names(cls):
|
168
|
+
"""
|
169
|
+
Return a list of all workspace names found in the workspaces directory.
|
170
|
+
"""
|
171
|
+
workspaces_dir = cls.ROOT_DIR / cls.WORKSPACES_DIR_NAME
|
172
|
+
if not workspaces_dir.exists():
|
173
|
+
raise FileNotFoundError(f"Workspaces directory not found at: {workspaces_dir}")
|
174
|
+
|
175
|
+
workspace_dirs = [
|
176
|
+
p.name for p in workspaces_dir.iterdir()
|
177
|
+
if p.is_dir() and not p.name.startswith('.') # skip hidden/system folders
|
178
|
+
]
|
179
|
+
return workspace_dirs
|
180
|
+
|
181
|
+
@classmethod
|
182
|
+
def identify_default_workspace_path(cls):
|
183
|
+
"""
|
184
|
+
Class method that reads default-workspace.toml to identify the default-workspace path.
|
185
|
+
"""
|
186
|
+
workspace_name = cls.identify_default_workspace_name()
|
187
|
+
workspaces_dir = cls.ROOT_DIR / cls.WORKSPACES_DIR_NAME
|
188
|
+
default_workspace_path = workspaces_dir / workspace_name
|
189
|
+
if not default_workspace_path.exists():
|
190
|
+
raise FileNotFoundError(f"Default workspace directory not found: {default_workspace_path}")
|
191
|
+
return default_workspace_path
|
192
|
+
@classmethod
|
193
|
+
def identify_default_workspace_name(cls):
|
194
|
+
"""
|
195
|
+
Class method that reads default-workspace.toml to identify the default-workspace.
|
196
|
+
"""
|
197
|
+
|
198
|
+
workspaces_dir = cls.ROOT_DIR / cls.WORKSPACES_DIR_NAME
|
199
|
+
logging.info(f"workspaces_dir = {workspaces_dir}\n")
|
200
|
+
default_toml_path = workspaces_dir / cls.DEFAULT_WORKSPACE_TOML_FILE_NAME
|
201
|
+
|
202
|
+
if not default_toml_path.exists():
|
203
|
+
raise FileNotFoundError(f"Missing {cls.DEFAULT_WORKSPACE_TOML_FILE_NAME} in {workspaces_dir}")
|
204
|
+
|
205
|
+
with open(default_toml_path, 'r') as f:
|
206
|
+
data = toml.load(f)
|
207
|
+
logging.debug(f"data = {data}")
|
208
|
+
try:
|
209
|
+
return data['default-workspace']['workspace'] # This dictates the proper formatting of the TOML file.
|
210
|
+
except KeyError as e:
|
211
|
+
raise KeyError(f"Missing key in {cls.DEFAULT_WORKSPACE_TOML_FILE_NAME}: {e}")
|
212
|
+
|
213
|
+
def get_default_query_file_paths_list(self):
|
214
|
+
|
215
|
+
default_query_path = self.get_queries_dir()/ 'default-queries.toml'
|
216
|
+
|
217
|
+
with open(default_query_path, 'r') as f:
|
218
|
+
query_config = toml.load(f)
|
219
|
+
filenames = query_config['default-query']['files']
|
220
|
+
if not isinstance(filenames, list):
|
221
|
+
raise ValueError("Expected a list under ['default-query']['files'] in default-queries.toml")
|
222
|
+
paths = [self.get_queries_file_path(fname) for fname in filenames]
|
223
|
+
|
224
|
+
for path in paths:
|
225
|
+
if not os.path.exists(path):
|
226
|
+
raise FileNotFoundError(f"Query file not found: {path}")
|
227
|
+
return paths
|
228
|
+
|
229
|
+
@property
|
230
|
+
def name(self):
|
231
|
+
return self.workspace_name
|
232
|
+
|
233
|
+
def establish_default_workspace():
|
234
|
+
workspace_name = WorkspaceManager.identify_default_workspace_name()
|
235
|
+
logging.info(f"workspace_name = {workspace_name}")
|
236
|
+
workspace_manager = WorkspaceManager(workspace_name)
|
237
|
+
logging.info(f"WorkspaceManager.get_workspace_dir() = {WorkspaceManager.get_workspace_dir()}")
|
238
|
+
return
|
239
|
+
|
240
|
+
def demo_establish_default_workspace():
|
241
|
+
establish_default_workspace()
|
242
|
+
|
243
|
+
if __name__ == "__main__":
|
244
|
+
import sys
|
245
|
+
cmd = sys.argv[1] if len(sys.argv) > 1 else "default"
|
246
|
+
|
247
|
+
if cmd == "demo-default":
|
248
|
+
demo_establish_default_workspace()
|
249
|
+
else:
|
250
|
+
print("Usage options: \n"
|
251
|
+
"poetry run python -m pipeline.api.eds demo-default \n")
|
252
|
+
|
253
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
BSD-3 License
|
2
|
+
|
3
|
+
Copyright (c) 2025 George Clayton Bennett
|
4
|
+
|
5
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
6
|
+
|
7
|
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
8
|
+
|
9
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
10
|
+
|
11
|
+
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
12
|
+
|
13
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
14
|
+
|