PyELSSA 0.1.0__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.
- PyELSSA/__init__.py +28 -0
- PyELSSA/configuration.py +325 -0
- PyELSSA/data/TestFile.txt +0 -0
- PyELSSA/data/TestFile1.csv +0 -0
- PyELSSA/data/TestFile2.csv +0 -0
- PyELSSA/gui/gui_base_frame.py +221 -0
- PyELSSA/gui/gui_main_page.py +142 -0
- PyELSSA/gui/gui_start.py +424 -0
- PyELSSA/gui/gui_sub_frame.py +414 -0
- PyELSSA/licensing.py +224 -0
- PyELSSA/master.py +87 -0
- PyELSSA/release_update.py +59 -0
- PyELSSA/shared_contents.py +491 -0
- PyELSSA/shared_msazure_api.py +469 -0
- pyelssa-0.1.0.dist-info/METADATA +44 -0
- pyelssa-0.1.0.dist-info/RECORD +18 -0
- pyelssa-0.1.0.dist-info/WHEEL +4 -0
- pyelssa-0.1.0.dist-info/licenses/LICENSE +88 -0
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
#=================================================================
|
|
2
|
+
# Created by: Jieming Ye
|
|
3
|
+
# Created on: June 2026
|
|
4
|
+
# Last Modified: June 2026
|
|
5
|
+
#=================================================================
|
|
6
|
+
# Copyright (c) 2026 [Jieming Ye]
|
|
7
|
+
#
|
|
8
|
+
# This Python source code is licensed under the
|
|
9
|
+
# Open Source Non-Commercial License (OSNCL) v1.0
|
|
10
|
+
# See LICENSE for details.
|
|
11
|
+
#=================================================================
|
|
12
|
+
"""
|
|
13
|
+
Microsoft Graph API related functions
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
#=================================================================
|
|
17
|
+
# VERSION CONTROL
|
|
18
|
+
# V1.0 (Jieming Ye) - Initial Version
|
|
19
|
+
#=================================================================
|
|
20
|
+
# Set Information Variable
|
|
21
|
+
# N/A
|
|
22
|
+
#=================================================================
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import shutil
|
|
26
|
+
import subprocess
|
|
27
|
+
import msal
|
|
28
|
+
import requests
|
|
29
|
+
import urllib.parse
|
|
30
|
+
import tempfile
|
|
31
|
+
|
|
32
|
+
from PyELSSA.shared_contents import SharedVariables, SharedMethods
|
|
33
|
+
|
|
34
|
+
class AzureCloudID:
|
|
35
|
+
TENANT_ID = "c22cc3e1-5d7f-4f4d-be03-d5a158cc9409"
|
|
36
|
+
CLIENT_ID = "cd44fee0-76ce-422f-af29-386036f0eac5"
|
|
37
|
+
GRAPH_BASE = "https://graph.microsoft.com/v1.0"
|
|
38
|
+
AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
|
|
39
|
+
|
|
40
|
+
# Delegated scopes for interactive/public-client login
|
|
41
|
+
SCOPES_RW = ["https://graph.microsoft.com/Sites.ReadWrite.All"]
|
|
42
|
+
# App-only scopes for confidential client login
|
|
43
|
+
SCOPES_APP = ["https://graph.microsoft.com/.default"]
|
|
44
|
+
|
|
45
|
+
PUBLIC_APP = None
|
|
46
|
+
CONFIDENTIAL_APP = None
|
|
47
|
+
ACCESS_TOKEN_CACHE = None
|
|
48
|
+
|
|
49
|
+
SECRET_KEY = "GraphAPISecret.key"
|
|
50
|
+
|
|
51
|
+
# ---- SharePoint details ----
|
|
52
|
+
SHAREPOINT_HOST = "networkrail.sharepoint.com" # Network Rail Host Base
|
|
53
|
+
SITE_PATH = "/sites/NRDDTDNS" # e.g. /sites/Engineering
|
|
54
|
+
|
|
55
|
+
DOC_LIBRARY = "Documents"
|
|
56
|
+
MANUAL_PATH = "05 - Traction Power Modelling/04 - Vision Oslo/04 - VISION OSLO Extension Tool/VISION-OSLO Extension User Guide A09.pdf"
|
|
57
|
+
BHTPBANK_PATH = "05 - Traction Power Modelling/02 - Asset Data/07 - Rolling Stock/01 - Master BHTPBANK Library"
|
|
58
|
+
ELSSA_CORE_PATH = "00 - E and P Systems Team/zz_DO_NOT_CHANGE - CONFIDENTIAL_ACCESS/PyELSSA_DO_NOT_CHANGE/Latest_Core"
|
|
59
|
+
|
|
60
|
+
APP = None # internal ms api holding within the python process
|
|
61
|
+
|
|
62
|
+
class AzureWorkflow:
|
|
63
|
+
# open document file from sharepoint
|
|
64
|
+
@staticmethod
|
|
65
|
+
def open_file_from_sharepoint(filename):
|
|
66
|
+
# create a temp directory and download the file there
|
|
67
|
+
temp_dir = tempfile.gettempdir()
|
|
68
|
+
temp_file = os.path.join(temp_dir, filename)
|
|
69
|
+
|
|
70
|
+
online_library = AzureCloudID.DOC_LIBRARY
|
|
71
|
+
online_file = AzureCloudID.MANUAL_PATH
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
AzureWorkflow.download_tdns_sharepoint_file(online_library,online_file,temp_file)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
SharedMethods.print_message(f"ERROR: Error acuqiring the manual: {e}","31")
|
|
77
|
+
SharedMethods.print_message(f"ERROR: Ensure all errors are cleared in sequence...","31")
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
subprocess.Popen(['start', '', temp_file], shell=True,close_fds=True)
|
|
82
|
+
except Exception as e:
|
|
83
|
+
SharedMethods.print_message(f"ERROR: Error opening file with default app: {e}","31")
|
|
84
|
+
return
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def _read_secret():
|
|
89
|
+
secret_file = os.path.join(SharedVariables.configuration_path, AzureCloudID.SECRET_KEY)
|
|
90
|
+
if not os.path.isfile(secret_file):
|
|
91
|
+
SharedMethods.print_message(f"ATTENTION: ESDD advanced user could request secret token via email to 'traction.power@networkrail.co.uk'.", "33")
|
|
92
|
+
return None
|
|
93
|
+
try:
|
|
94
|
+
with open(secret_file, 'r') as file:
|
|
95
|
+
secret = file.read().strip()
|
|
96
|
+
return secret if secret else None
|
|
97
|
+
except Exception as e:
|
|
98
|
+
SharedMethods.print_message(f"WARNING: Failed to read secret file. {e}", "33")
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def _get_confidential_app():
|
|
103
|
+
secret = AzureWorkflow._read_secret()
|
|
104
|
+
if not secret:
|
|
105
|
+
return None
|
|
106
|
+
try:
|
|
107
|
+
if AzureCloudID.CONFIDENTIAL_APP is None:
|
|
108
|
+
AzureCloudID.CONFIDENTIAL_APP = msal.ConfidentialClientApplication(
|
|
109
|
+
AzureCloudID.CLIENT_ID,
|
|
110
|
+
authority=AzureCloudID.AUTHORITY,
|
|
111
|
+
client_credential=secret
|
|
112
|
+
)
|
|
113
|
+
return AzureCloudID.CONFIDENTIAL_APP
|
|
114
|
+
except Exception as e:
|
|
115
|
+
SharedMethods.print_message(f"WARNING: Failed to initialise confidential client. {e}", "33")
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
@staticmethod
|
|
119
|
+
def _acquire_confidential_token():
|
|
120
|
+
app = AzureWorkflow._get_confidential_app()
|
|
121
|
+
if app is None:
|
|
122
|
+
return None
|
|
123
|
+
try:
|
|
124
|
+
result = app.acquire_token_for_client(scopes=AzureCloudID.SCOPES_APP)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
SharedMethods.print_message(f"WARNING: Confidential login failed. {e}", "33")
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
if result and "access_token" in result:
|
|
130
|
+
SharedMethods.print_message("INFO: Access token acquired using confidential client.", "32")
|
|
131
|
+
return result["access_token"]
|
|
132
|
+
|
|
133
|
+
SharedMethods.print_message(f"WARNING: Confidential token error: {result.get('error')} | {result.get('error_description')}.","33")
|
|
134
|
+
SharedMethods.print_message(f"\nWARNING: Please report screenshot back via 'Help - Email to Support' action...\n","33")
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _get_public_app():
|
|
139
|
+
if AzureCloudID.PUBLIC_APP is None:
|
|
140
|
+
AzureCloudID.PUBLIC_APP = msal.PublicClientApplication(
|
|
141
|
+
AzureCloudID.CLIENT_ID,
|
|
142
|
+
authority=AzureCloudID.AUTHORITY
|
|
143
|
+
)
|
|
144
|
+
return AzureCloudID.PUBLIC_APP
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def _acquire_public_token():
|
|
148
|
+
app = AzureWorkflow._get_public_app()
|
|
149
|
+
|
|
150
|
+
# Try silent token first
|
|
151
|
+
try:
|
|
152
|
+
accounts = app.get_accounts()
|
|
153
|
+
if accounts:
|
|
154
|
+
result = app.acquire_token_silent(
|
|
155
|
+
AzureCloudID.SCOPES_RW,
|
|
156
|
+
account=accounts[0]
|
|
157
|
+
)
|
|
158
|
+
if result and "access_token" in result:
|
|
159
|
+
SharedMethods.print_message("INFO: Access token acquired silently using public client.", "32")
|
|
160
|
+
return result["access_token"]
|
|
161
|
+
except Exception as e:
|
|
162
|
+
SharedMethods.print_message(f"WARNING: Silent token acquisition failed. {e}", "33")
|
|
163
|
+
|
|
164
|
+
# Interactive fallback
|
|
165
|
+
try:
|
|
166
|
+
SharedMethods.print_message(f"ATTENTION: Please sign in via system brower in two minutes. (Application awaiting for two minutes)...", "33")
|
|
167
|
+
result = app.acquire_token_interactive(
|
|
168
|
+
scopes=AzureCloudID.SCOPES_RW,
|
|
169
|
+
timeout=120
|
|
170
|
+
)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
SharedMethods.print_message(f"ERROR: Interactive login failed. {e}", "31")
|
|
173
|
+
return None
|
|
174
|
+
|
|
175
|
+
if result and "access_token" in result:
|
|
176
|
+
SharedMethods.print_message("INFO: Access token acquired interactively using public client.", "32")
|
|
177
|
+
return result["access_token"]
|
|
178
|
+
|
|
179
|
+
SharedMethods.print_message(f"ERROR: Public token error: {result.get('error')} | {result.get('error_description')}","31")
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def get_access_token():
|
|
184
|
+
# 1) Try confidential client first
|
|
185
|
+
token = AzureWorkflow._acquire_confidential_token()
|
|
186
|
+
if token:
|
|
187
|
+
AzureCloudID.ACCESS_TOKEN_CACHE = token
|
|
188
|
+
return token
|
|
189
|
+
|
|
190
|
+
# 2) Fall back to public client flow
|
|
191
|
+
token = AzureWorkflow._acquire_public_token()
|
|
192
|
+
if token:
|
|
193
|
+
AzureCloudID.ACCESS_TOKEN_CACHE = token
|
|
194
|
+
return token
|
|
195
|
+
|
|
196
|
+
return False
|
|
197
|
+
|
|
198
|
+
def graph_get(url, token, **kwargs):
|
|
199
|
+
headers = kwargs.pop("headers", {})
|
|
200
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
201
|
+
response = requests.get(url, headers=headers, **kwargs)
|
|
202
|
+
response.raise_for_status()
|
|
203
|
+
return response
|
|
204
|
+
|
|
205
|
+
def download_tdns_sharepoint_file(online_library,online_file,output_file):
|
|
206
|
+
token = AzureWorkflow.get_access_token()
|
|
207
|
+
if not token:
|
|
208
|
+
SharedMethods.print_message(f"ERROR: Authorisation Fail...","31")
|
|
209
|
+
return False
|
|
210
|
+
# 1) Resolve site ID
|
|
211
|
+
# GET /sites/{hostname}:/{server-relative-path}
|
|
212
|
+
site_url = f"{AzureCloudID.GRAPH_BASE}/sites/{AzureCloudID.SHAREPOINT_HOST}:{AzureCloudID.SITE_PATH}"
|
|
213
|
+
site_resp = AzureWorkflow.graph_get(site_url, token)
|
|
214
|
+
site = site_resp.json()
|
|
215
|
+
site_id = site["id"]
|
|
216
|
+
print(f"Site: {AzureCloudID.SITE_PATH} -- ID: {site_id}")
|
|
217
|
+
|
|
218
|
+
# 2) Get all drives (document libraries) in the site
|
|
219
|
+
# GET /sites/{site-id}/drives
|
|
220
|
+
drives_url = f"{AzureCloudID.GRAPH_BASE}/sites/{site_id}/drives"
|
|
221
|
+
drives_resp = AzureWorkflow.graph_get(drives_url, token)
|
|
222
|
+
drives = drives_resp.json()["value"]
|
|
223
|
+
|
|
224
|
+
drive = next((d for d in drives if d["name"] == online_library), None)
|
|
225
|
+
if not drive:
|
|
226
|
+
SharedMethods.print_message(f"Library '{online_library}' not found. Available: {[d['name'] for d in drives]}","31")
|
|
227
|
+
drive_id = drive["id"]
|
|
228
|
+
print(f"Drive: {online_library} -- ID: {drive_id}")
|
|
229
|
+
|
|
230
|
+
# 3) Resolve the file by path relative to the library root
|
|
231
|
+
# GET /drives/{drive-id}/root:/{item-path}
|
|
232
|
+
encoded_path = urllib.parse.quote(online_file)
|
|
233
|
+
item_url = f"{AzureCloudID.GRAPH_BASE}/drives/{drive_id}/root:/{encoded_path}"
|
|
234
|
+
item_resp = AzureWorkflow.graph_get(item_url, token)
|
|
235
|
+
item = item_resp.json()
|
|
236
|
+
item_id = item["id"]
|
|
237
|
+
print(f"Item: {online_file} -- ID: {item_id}")
|
|
238
|
+
|
|
239
|
+
# 4) Download file content
|
|
240
|
+
# GET /drives/{drive-id}/items/{item-id}/content
|
|
241
|
+
download_url = f"{AzureCloudID.GRAPH_BASE}/drives/{drive_id}/items/{item_id}/content"
|
|
242
|
+
print("Downloading in process...")
|
|
243
|
+
with requests.get(
|
|
244
|
+
download_url,
|
|
245
|
+
headers={"Authorization": f"Bearer {token}"},
|
|
246
|
+
stream=True,
|
|
247
|
+
allow_redirects=True
|
|
248
|
+
) as r:
|
|
249
|
+
r.raise_for_status()
|
|
250
|
+
with open(output_file, "wb") as f:
|
|
251
|
+
for chunk in r.iter_content(chunk_size=8192):
|
|
252
|
+
if chunk:
|
|
253
|
+
f.write(chunk)
|
|
254
|
+
|
|
255
|
+
print(f"Downloaded to: {output_file}")
|
|
256
|
+
|
|
257
|
+
return True
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def download_tdns_sharepoint_folder(online_library, online_folder, local_folder, delete_extra_local=False):
|
|
261
|
+
"""
|
|
262
|
+
Download all files from a SharePoint folder (and subfolders) into a local folder.
|
|
263
|
+
|
|
264
|
+
Parameters
|
|
265
|
+
----------
|
|
266
|
+
online_library : str
|
|
267
|
+
SharePoint document library name, e.g. "Documents"
|
|
268
|
+
online_folder : str
|
|
269
|
+
Folder path relative to the library root, e.g. "FolderA/SubFolderB"
|
|
270
|
+
Use "" or "/" for the library root.
|
|
271
|
+
local_folder : str
|
|
272
|
+
Local destination folder.
|
|
273
|
+
delete_extra_local : bool
|
|
274
|
+
If True, remove local files/folders that do not exist in SharePoint,
|
|
275
|
+
making the local folder a mirror of the online folder.
|
|
276
|
+
|
|
277
|
+
Returns
|
|
278
|
+
-------
|
|
279
|
+
bool
|
|
280
|
+
True if successful, False otherwise.
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
token = AzureWorkflow.get_access_token()
|
|
284
|
+
if not token:
|
|
285
|
+
SharedMethods.print_message("ERROR: Authorisation Fail...", "31")
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
session = requests.Session()
|
|
289
|
+
session.headers.update({"Authorization": f"Bearer {token}"})
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
# 1) Resolve site ID
|
|
293
|
+
# GET /sites/{hostname}:/{server-relative-path}
|
|
294
|
+
site_url = f"{AzureCloudID.GRAPH_BASE}/sites/{AzureCloudID.SHAREPOINT_HOST}:{AzureCloudID.SITE_PATH}"
|
|
295
|
+
site_resp = AzureWorkflow.graph_get(site_url, token)
|
|
296
|
+
site_resp.raise_for_status()
|
|
297
|
+
site = site_resp.json()
|
|
298
|
+
site_id = site["id"]
|
|
299
|
+
print(f"Site {AzureCloudID.SITE_PATH} -- ID: {site_id}")
|
|
300
|
+
|
|
301
|
+
# 2) Get all drives (document libraries) in the site
|
|
302
|
+
# GET /sites/{site-id}/drives
|
|
303
|
+
drives_url = f"{AzureCloudID.GRAPH_BASE}/sites/{site_id}/drives"
|
|
304
|
+
drives_resp = AzureWorkflow.graph_get(drives_url, token)
|
|
305
|
+
drives_resp.raise_for_status()
|
|
306
|
+
drives = drives_resp.json().get("value", [])
|
|
307
|
+
|
|
308
|
+
drive = next((d for d in drives if d["name"] == online_library), None)
|
|
309
|
+
if not drive:
|
|
310
|
+
SharedMethods.print_message(
|
|
311
|
+
f"Library '{online_library}' not found. Available: {[d['name'] for d in drives]}",
|
|
312
|
+
"31"
|
|
313
|
+
)
|
|
314
|
+
return False
|
|
315
|
+
|
|
316
|
+
drive_id = drive["id"]
|
|
317
|
+
print(f"Drive {online_library} -- ID: {drive_id}")
|
|
318
|
+
|
|
319
|
+
# Normalise online folder
|
|
320
|
+
online_folder = (online_folder or "").strip("/").strip()
|
|
321
|
+
|
|
322
|
+
# 3) Resolve folder item ID
|
|
323
|
+
if online_folder == "":
|
|
324
|
+
# Library root
|
|
325
|
+
folder_item_id = "root"
|
|
326
|
+
print(f"Using library root folder")
|
|
327
|
+
else:
|
|
328
|
+
# IMPORTANT: preserve "/" so the path stays hierarchical
|
|
329
|
+
encoded_folder = urllib.parse.quote(online_folder, safe="/")
|
|
330
|
+
folder_url = f"{AzureCloudID.GRAPH_BASE}/drives/{drive_id}/root:/{encoded_folder}"
|
|
331
|
+
folder_resp = AzureWorkflow.graph_get(folder_url, token)
|
|
332
|
+
folder_resp.raise_for_status()
|
|
333
|
+
folder_item = folder_resp.json()
|
|
334
|
+
|
|
335
|
+
if "folder" not in folder_item:
|
|
336
|
+
SharedMethods.print_message(f"Path '{online_folder}' exists but is not a folder.","31")
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
folder_item_id = folder_item["id"]
|
|
340
|
+
print(f"Folder {online_folder} -- ID: {folder_item_id}")
|
|
341
|
+
|
|
342
|
+
# Ensure local root exists
|
|
343
|
+
os.makedirs(local_folder, exist_ok=True)
|
|
344
|
+
|
|
345
|
+
# Keep track of downloaded relative paths so we can optionally delete extra local files
|
|
346
|
+
remote_relative_paths = set()
|
|
347
|
+
|
|
348
|
+
def list_children(drive_id, item_id):
|
|
349
|
+
"""
|
|
350
|
+
Get all children for a folder, including pagination.
|
|
351
|
+
"""
|
|
352
|
+
if item_id == "root":
|
|
353
|
+
url = f"{AzureCloudID.GRAPH_BASE}/drives/{drive_id}/root/children"
|
|
354
|
+
else:
|
|
355
|
+
url = f"{AzureCloudID.GRAPH_BASE}/drives/{drive_id}/items/{item_id}/children"
|
|
356
|
+
|
|
357
|
+
items = []
|
|
358
|
+
while url:
|
|
359
|
+
resp = session.get(url)
|
|
360
|
+
resp.raise_for_status()
|
|
361
|
+
data = resp.json()
|
|
362
|
+
items.extend(data.get("value", []))
|
|
363
|
+
url = data.get("@odata.nextLink")
|
|
364
|
+
|
|
365
|
+
return items
|
|
366
|
+
|
|
367
|
+
def download_file_by_item_id(drive_id, item_id, local_path):
|
|
368
|
+
"""
|
|
369
|
+
Download one file and overwrite if it already exists.
|
|
370
|
+
"""
|
|
371
|
+
download_url = f"{AzureCloudID.GRAPH_BASE}/drives/{drive_id}/items/{item_id}/content"
|
|
372
|
+
print(f"Downloading: {local_path}")
|
|
373
|
+
|
|
374
|
+
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
|
375
|
+
|
|
376
|
+
with session.get(download_url, stream=True, allow_redirects=True) as r:
|
|
377
|
+
r.raise_for_status()
|
|
378
|
+
with open(local_path, "wb") as f:
|
|
379
|
+
for chunk in r.iter_content(chunk_size=8192):
|
|
380
|
+
if chunk:
|
|
381
|
+
f.write(chunk)
|
|
382
|
+
|
|
383
|
+
def walk_and_download(current_item_id, current_local_folder, current_relative_folder=""):
|
|
384
|
+
"""
|
|
385
|
+
Recursively walk SharePoint folder and download all files/subfolders.
|
|
386
|
+
"""
|
|
387
|
+
children = list_children(drive_id, current_item_id)
|
|
388
|
+
|
|
389
|
+
for item in children:
|
|
390
|
+
item_name = item["name"]
|
|
391
|
+
item_id = item["id"]
|
|
392
|
+
|
|
393
|
+
# Build relative path for tracking/mirroring
|
|
394
|
+
if current_relative_folder:
|
|
395
|
+
relative_path = f"{current_relative_folder}/{item_name}"
|
|
396
|
+
else:
|
|
397
|
+
relative_path = item_name
|
|
398
|
+
|
|
399
|
+
relative_path_norm = relative_path.replace("\\", "/")
|
|
400
|
+
|
|
401
|
+
if "folder" in item:
|
|
402
|
+
# Subfolder
|
|
403
|
+
next_local_folder = os.path.join(current_local_folder, item_name)
|
|
404
|
+
os.makedirs(next_local_folder, exist_ok=True)
|
|
405
|
+
|
|
406
|
+
remote_relative_paths.add(relative_path_norm + "/")
|
|
407
|
+
walk_and_download(item_id, next_local_folder, relative_path_norm)
|
|
408
|
+
|
|
409
|
+
elif "file" in item:
|
|
410
|
+
# File
|
|
411
|
+
local_file_path = os.path.join(current_local_folder, item_name)
|
|
412
|
+
download_file_by_item_id(drive_id, item_id, local_file_path)
|
|
413
|
+
remote_relative_paths.add(relative_path_norm)
|
|
414
|
+
|
|
415
|
+
else:
|
|
416
|
+
print(f"Skipping unknown item type: {relative_path_norm}")
|
|
417
|
+
|
|
418
|
+
print("Downloading folder contents...")
|
|
419
|
+
walk_and_download(folder_item_id, local_folder)
|
|
420
|
+
|
|
421
|
+
if delete_extra_local:
|
|
422
|
+
print("Deleting extra local files/folders not found online...")
|
|
423
|
+
|
|
424
|
+
# Walk local tree bottom-up so folders can be removed after files
|
|
425
|
+
for root, dirs, files in os.walk(local_folder, topdown=False):
|
|
426
|
+
for file_name in files:
|
|
427
|
+
full_path = os.path.join(root, file_name)
|
|
428
|
+
rel_path = os.path.relpath(full_path, local_folder).replace("\\", "/")
|
|
429
|
+
if rel_path not in remote_relative_paths:
|
|
430
|
+
print(f"Removing local extra file: {full_path}")
|
|
431
|
+
os.remove(full_path)
|
|
432
|
+
|
|
433
|
+
for dir_name in dirs:
|
|
434
|
+
full_dir = os.path.join(root, dir_name)
|
|
435
|
+
rel_dir = os.path.relpath(full_dir, local_folder).replace("\\", "/") + "/"
|
|
436
|
+
if rel_dir not in remote_relative_paths:
|
|
437
|
+
print(f"Removing local extra folder: {full_dir}")
|
|
438
|
+
shutil.rmtree(full_dir, ignore_errors=True)
|
|
439
|
+
|
|
440
|
+
print(f"Download complete. Local folder: {local_folder}")
|
|
441
|
+
return True
|
|
442
|
+
|
|
443
|
+
except requests.HTTPError as e:
|
|
444
|
+
SharedMethods.print_message(f"ERROR: HTTP ERROR: {str(e)}", "31")
|
|
445
|
+
try:
|
|
446
|
+
resp_text = e.response.text
|
|
447
|
+
print(resp_text)
|
|
448
|
+
except Exception:
|
|
449
|
+
pass
|
|
450
|
+
return False
|
|
451
|
+
|
|
452
|
+
except Exception as e:
|
|
453
|
+
SharedMethods.print_message(f"ERROR: {str(e)}", "31")
|
|
454
|
+
return False
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
if __name__ == "__main__":
|
|
458
|
+
print("TBC")
|
|
459
|
+
LIBRARY_NAME = "Documents" # e.g. Documents or Shared Documents
|
|
460
|
+
FILE_PATH_IN_LIBRARY = "05 - Traction Power Modelling/04 - Vision Oslo/04 - VISION OSLO Extension Tool/VISION-OSLO Extension User Guide A09.pdf"
|
|
461
|
+
LOCAL_OUTPUT = "Manual.pdf"
|
|
462
|
+
|
|
463
|
+
AzureWorkflow.download_tdns_sharepoint_file(LIBRARY_NAME,FILE_PATH_IN_LIBRARY,LOCAL_OUTPUT)
|
|
464
|
+
|
|
465
|
+
# AzureWorkflow.download_tdns_sharepoint_folder(
|
|
466
|
+
# online_library="Documents",
|
|
467
|
+
# online_folder="05 - Traction Power Modelling/02 - Asset Data/07 - Rolling Stock/01 - Master BHTPBANK Library",
|
|
468
|
+
# local_folder=r"C:\Users\JYe1\Downloads\tractiontest"
|
|
469
|
+
# )
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PyELSSA
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: This is 'ELSSA in Python' package owned by Network Rail Infrastructure Limited.
|
|
5
|
+
License: Open Source Non-Commercial License (OSNCL) v2.0
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Jieming Ye
|
|
8
|
+
Author-email: Jieming.Ye@networkrail.co.uk
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Classifier: License :: Other/Proprietary License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Requires-Dist: matplotlib
|
|
19
|
+
Requires-Dist: numpy
|
|
20
|
+
Requires-Dist: pandas
|
|
21
|
+
Requires-Dist: psutil
|
|
22
|
+
Requires-Dist: scipy
|
|
23
|
+
Requires-Dist: tk
|
|
24
|
+
Project-URL: Homepage, https://pypi.org/project/PyELSSA/
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# ELSSA_python
|
|
28
|
+
ELSSA (Electrification System Simulation Analysis) was written in Matlab. The last supporting Matlab version is 2014a which is out of official support for a fairly long time.
|
|
29
|
+
Some of the source file is p coded (encrypted) which makes the support extremly difficult.
|
|
30
|
+
|
|
31
|
+
This repository looks to mordenlize the software suite to a new era, giving the technology involvement in the last decade.
|
|
32
|
+
|
|
33
|
+
The code will be rewritten from scratch in Python aiming for better user interface and program maintenance.
|
|
34
|
+
|
|
35
|
+
## ELSSA Python Migration Plan
|
|
36
|
+
- Phase 1: Reverse engineering the p coded section.
|
|
37
|
+
- Phase 2: Replicate the ELSSA Matlab functionality.
|
|
38
|
+
- Phase 3: Enhance the tool package and user interface.
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
## ELLSA Matlab
|
|
42
|
+
Please refer to: https://github.com/NR-ESTractionPower/ELSSA_matlab
|
|
43
|
+
|
|
44
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
PyELSSA/__init__.py,sha256=7_2h1M-IR8oVQlfeJ9byqKqDcmtCYDWQzZRUoJ-QWmY,956
|
|
2
|
+
PyELSSA/configuration.py,sha256=cvtpvz-0lerODKOFjkH-y8VjmG5xJWH0z42taq7eeJ8,13726
|
|
3
|
+
PyELSSA/data/TestFile.txt,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
PyELSSA/data/TestFile1.csv,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
PyELSSA/data/TestFile2.csv,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
PyELSSA/gui/gui_base_frame.py,sha256=RMhsrJ2S6ZDmc88DdKknX0-0fOJA_3ve6CDFfGpvvds,9185
|
|
7
|
+
PyELSSA/gui/gui_main_page.py,sha256=aitWGd1BdNpErlFvN335vADOxHSi7gHNCmduFkkTRRs,7510
|
|
8
|
+
PyELSSA/gui/gui_start.py,sha256=UiJ1e3OQHkuzVZiyvujm9vlDu5kyYlO6dOdjJtx0DBk,20734
|
|
9
|
+
PyELSSA/gui/gui_sub_frame.py,sha256=VRC_cVnIAM1-HofmD4Fdo65jultjzpl_1ETfGjfzDtM,22579
|
|
10
|
+
PyELSSA/licensing.py,sha256=6s-VnUqg7pQZ1QMcKxpVySrkZrXP8RWuZL1Sd1e_WXM,9088
|
|
11
|
+
PyELSSA/master.py,sha256=4Xlq1JmMtHeDdbmyDKuTG7pf8Z76RdBBaZhOawk6ZVo,2756
|
|
12
|
+
PyELSSA/release_update.py,sha256=iEk-L7LMwoYD7_Ia-vOrY9G_w-B7RpuuYB5Ya-Frek8,2162
|
|
13
|
+
PyELSSA/shared_contents.py,sha256=GDlQuypmtdP7SaVhfus-xZpfTLPmmIOuxxKVT-sJ9Rw,21630
|
|
14
|
+
PyELSSA/shared_msazure_api.py,sha256=34V2ka68ASvO93BTdQH_Zpi57P3k124b6-BPavagUKI,20025
|
|
15
|
+
pyelssa-0.1.0.dist-info/licenses/LICENSE,sha256=WRXXPQ74IB1kCBAbJBgciruEL10LYdL7z29TUatP1Lc,4580
|
|
16
|
+
pyelssa-0.1.0.dist-info/METADATA,sha256=1DY7Qmwd2hUew8K1RxCH4AAHVYF_2BPyECzCzJ3y11Q,1728
|
|
17
|
+
pyelssa-0.1.0.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
|
|
18
|
+
pyelssa-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Copyright (c) 2026 [Jieming Ye, Engineering Services Design]
|
|
2
|
+
|
|
3
|
+
Open Source Non-Commercial License (OSNCL) v2.0
|
|
4
|
+
|
|
5
|
+
Preamble
|
|
6
|
+
The purpose of this license is to ensure that the software remains free for non-commercial use, while also ensuring
|
|
7
|
+
that any modifications to the software are made publicly available. Commercial use of this software is not allowed
|
|
8
|
+
without prior written permission from the original author(s).
|
|
9
|
+
|
|
10
|
+
The precise terms and conditions for copying, distribution, and modification follow.
|
|
11
|
+
|
|
12
|
+
TERMS AND CONDITIONS
|
|
13
|
+
Section 0: Definitions
|
|
14
|
+
|
|
15
|
+
“This License” refers to version 2.0 of the Open Source Non-Commercial License.
|
|
16
|
+
“The Software” refers to the Python package and all associated files, including but not limited to source code,
|
|
17
|
+
documentation, and configuration files, distributed under this License.
|
|
18
|
+
“Modification” means any work derived from or based on the Software, such as bug fixes, enhancements, or other
|
|
19
|
+
alterations to the original work.
|
|
20
|
+
"E&P Team" refers to the E&P team under Engineering Services, Route Services, Network Rail Infrastructure Ltd,
|
|
21
|
+
structured at September 2024.
|
|
22
|
+
“You” refers to the individual or entity using, modifying, or distributing the Software.
|
|
23
|
+
|
|
24
|
+
“Public Distribution” refers to the act of making any modifications of the Software available to the general
|
|
25
|
+
public.
|
|
26
|
+
|
|
27
|
+
Section 1: Source Code and Modifications
|
|
28
|
+
|
|
29
|
+
1.1 You may modify the Software and distribute your modified version, provided that:
|
|
30
|
+
You make the source code of the modified version available to the public under the same terms as this License.
|
|
31
|
+
The modified version must be clearly marked to indicate that changes have been made, including the date of changes.
|
|
32
|
+
|
|
33
|
+
1.2 All modifications must be publicly available, in a manner that makes them easily accessible, such as posting
|
|
34
|
+
on a public repository (e.g., GitHub, GitLab).
|
|
35
|
+
|
|
36
|
+
Section 2: Non-Commercial Use
|
|
37
|
+
|
|
38
|
+
2.1 You may use, distribute, and modify the Software for non-commercial purposes only, including:
|
|
39
|
+
Personal projects.
|
|
40
|
+
Educational purposes.
|
|
41
|
+
Research purposes.
|
|
42
|
+
|
|
43
|
+
2.2 Commercial use of the Software is prohibited unless you have received explicit, prior written permission
|
|
44
|
+
from the original author(s).
|
|
45
|
+
|
|
46
|
+
2.3 If you wish to make commercial use of the Software, including any modifications, you must contact the original
|
|
47
|
+
author(s) to seek permission and negotiate terms for such use.
|
|
48
|
+
|
|
49
|
+
Section 3: Conveying Modified Source Code
|
|
50
|
+
|
|
51
|
+
3.1 If you modify the Software, you must:
|
|
52
|
+
Ensure that the modifications are licensed under this License.
|
|
53
|
+
Make the modified source code publicly available.
|
|
54
|
+
Provide information in any modified version about the changes made, including a notice stating the date of the
|
|
55
|
+
modification and who made the changes.
|
|
56
|
+
|
|
57
|
+
Section 4: Conveying Non-Source Forms
|
|
58
|
+
|
|
59
|
+
4.1 You may convey non-source forms of the Software, such as compiled binaries, provided that:
|
|
60
|
+
You also provide access to the corresponding source code.
|
|
61
|
+
The source code is made available under the same terms as this License.
|
|
62
|
+
|
|
63
|
+
Section 5: Commercial Use Exception
|
|
64
|
+
|
|
65
|
+
5.1 Commercial use of the Software or any modified versions is strictly prohibited unless permission is granted
|
|
66
|
+
in writing by the original author(s). For commercial inquiries, please contact [Traction.Power@networkrail.co.uk].
|
|
67
|
+
|
|
68
|
+
5.2 Commercial use of the Software or any modified versions is allowed by [E&P Team] without any restriction.
|
|
69
|
+
|
|
70
|
+
Section 6: No Warranty
|
|
71
|
+
|
|
72
|
+
6.1 There is no warranty for the Software, to the extent permitted by applicable law. The Software is provided
|
|
73
|
+
“as is” without warranty of any kind, either expressed or implied, including but not limited to the implied
|
|
74
|
+
warranties of merchantability and fitness for a particular purpose. The entire risk as to the quality and
|
|
75
|
+
performance of the Software is with you.
|
|
76
|
+
|
|
77
|
+
6.2 In no event, unless required by law or agreed to in writing, shall the author(s) be liable for any damages,
|
|
78
|
+
including any general, special, incidental, or consequential damages arising from the use or inability to use the
|
|
79
|
+
Software.
|
|
80
|
+
|
|
81
|
+
Section 7: Termination
|
|
82
|
+
|
|
83
|
+
7.1 You may not propagate or modify the Software except as expressly provided under this License. Any attempt
|
|
84
|
+
otherwise to propagate or modify the Software is void and will automatically terminate your rights under this
|
|
85
|
+
License.
|
|
86
|
+
|
|
87
|
+
7.2 However, parties who have received copies, or rights, from you under this License will not have their
|
|
88
|
+
licenses terminated so long as they remain in full compliance.
|