easy-utils-dev 2.140__py3-none-any.whl → 2.142__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.

Potentially problematic release.


This version of easy-utils-dev might be problematic. Click here for more details.

@@ -1,18 +1,23 @@
1
+ import copy
2
+ import gc
3
+ import time
1
4
  from werkzeug.serving import ThreadedWSGIServer
2
5
  from easy_utils_dev.utils import getRandomKey , generateToken , getTimestamp
3
6
  from flask_socketio import SocketIO
4
7
  from engineio.async_drivers import gevent
5
- from engineio.async_drivers import threading
6
8
  from flask_cors import CORS
7
9
  import logging , os
10
+ from flask import jsonify, request , current_app
8
11
  from flask import Flask
9
12
  from threading import Thread
13
+ import threading
10
14
  from easy_utils_dev.custom_env import cenv
11
15
  from easy_utils_dev.utils import kill_thread
12
16
  from multiprocessing import Process
13
17
  from werkzeug.serving import make_ssl_devcert
14
18
  from time import sleep
15
19
  from easy_utils_dev.utils import start_thread , getRandomKeysAndStr , mkdirs
20
+ from easy_utils_dev.temp_memory import TemporaryMemory
16
21
 
17
22
  def getClassById( id ) :
18
23
  return cenv[id]
@@ -23,87 +28,54 @@ def create_ssl(host,output) :
23
28
  output: the output locaiton to generate the ssl certificate. it should end with filename without extension
24
29
  '''
25
30
  return make_ssl_devcert( output , host=host)
26
-
27
-
28
31
 
29
- class Abort :
30
- def __init__(self, requestId=getRandomKeysAndStr(50)) :
31
- self.requestId = requestId
32
- self.result= None
32
+ def clone_request(request):
33
+ """Return a plain dict clone of Flask request data."""
34
+ return {
35
+ "method": request.method,
36
+ "path": request.path,
37
+ "url": request.url,
38
+ "headers": dict(request.headers),
39
+ "args": request.args.to_dict(flat=False),
40
+ "form": request.form.to_dict(flat=False),
41
+ "json": request.get_json(silent=True),
42
+ "data": request.get_data(), # raw body bytes
43
+ "files": {k: v.filename for k, v in request.files.items()},
44
+ "remote_addr": request.remote_addr,
45
+ "cookies": request.cookies,
46
+ }
47
+
48
+ class AbortRequest :
49
+ def __init__(self, request ) :
50
+ self.request = clone_request(request)
51
+ self.abort_id = None
52
+ self.abortable = False
33
53
  self.thread = None
34
- self.aborted=False
35
- self.response = {}
36
- self.error = False
37
- self.message = ''
38
- self.starttimestamp = getTimestamp()
39
- self.endtimestamp = 0
40
- self.kill = self.api_abort
41
- pass
42
-
43
-
44
- def abortable_process(self , operation , args=[] , kwargs={}) :
45
- def thread_run() :
46
- try :
47
- self.result = operation( *args, **kwargs )
48
- self.error = False
49
- self.message = ''
50
- self.endtimestamp = getTimestamp()
51
- except Exception as error :
52
- self.error = True
53
- self.message = str(error)
54
- thread = self.thread = start_thread( target = thread_run )
55
- while thread.is_alive() :
56
- sleep(.1)
57
- if self.aborted :
58
- self.response = {
59
- 'message' : 'request aborted.' ,
60
- 'id' : self.requestId ,
61
- 'status' : 405 ,
62
- 'result' : None,
63
- 'error' : self.error ,
64
- 'error_message' : '',
65
- 'starttimestamp' : self.starttimestamp,
66
- 'endtimestamp' : self.endtimestamp,
67
- 'aborted' : True,
68
- 'threadIsAlive' : thread.is_alive()
69
- }
70
- return self.response
71
- sleep(.2)
72
- self.response = {
73
- 'message' : 'request completed.' ,
74
- 'id' : self.requestId ,
75
- 'status' : 200 ,
76
- 'result' : self.result ,
77
- 'error' : self.error ,
78
- 'error_message' : self.message ,
79
- 'starttimestamp' : self.starttimestamp,
80
- 'endtimestamp' : self.endtimestamp,
81
- 'aborted' : False ,
82
- 'threadIsAlive' : thread.is_alive()
83
- }
84
- return self.response
85
-
86
-
87
- def api_abort(self) :
88
- self.endtimestamp = getTimestamp()
54
+ self.cache = None
55
+ self.start_ts = getTimestamp()
56
+
57
+ def abort(self) :
89
58
  kill_thread(self.thread)
90
- sleep(.5)
91
- self.aborted=True
59
+ self.cache.delete(self.abort_id)
60
+ try :
61
+ gc.collect()
62
+ except :
63
+ pass
92
64
 
93
65
 
94
66
  class UISERVER :
95
67
  def __init__(self ,
96
- id=getRandomKey(n=15),
97
- secretkey=generateToken(),
98
- address='localhost',
99
- port=5312 ,
100
- https=False ,
101
- ssl_crt=None,
102
- ssl_key=None,
103
- template_folder='templates/' ,
104
- static_folder = 'templates/assets'
105
- ,**kwargs
106
- ) -> None:
68
+ id=getRandomKey(n=15),
69
+ secretkey=generateToken(),
70
+ address='localhost',
71
+ port=5312 ,
72
+ https=False ,
73
+ ssl_crt=None,
74
+ ssl_key=None,
75
+ template_folder='templates/' ,
76
+ static_folder = 'templates/assets'
77
+ ,**kwargs
78
+ ) -> None:
107
79
  self.id = id
108
80
  self.static_folder = static_folder
109
81
  self.app = app = Flask(self.id , template_folder=template_folder , static_folder=self.static_folder )
@@ -116,6 +88,7 @@ class UISERVER :
116
88
  self.ssl_key=ssl_key
117
89
  self.enable_test_url=True
118
90
  self.abort_requests = {}
91
+ self.abort_base_url = '/request/abort'
119
92
  if https :
120
93
  self.httpProtocol = 'https'
121
94
  else :
@@ -123,18 +96,78 @@ class UISERVER :
123
96
  self.socketio = SocketIO(app , cors_allowed_origins="*" ,async_mode='threading' , engineio_logger=False , always_connect=True ,**kwargs )
124
97
  cenv[id] = self
125
98
  self.fullAddress = f"{self.httpProtocol}://{self.address}:{self.port}"
99
+ self.cache = TemporaryMemory()
126
100
 
127
101
  def update_cert(self , crt, ssl ) :
128
102
  self.ssl_crt=crt
129
103
  self.ssl_key=ssl
130
104
 
105
+ def register_abortable_request(self , request , abort_id = None ) :
106
+ path = request.path
107
+ Abort = AbortRequest(request)
108
+ if not path.startswith(self.abort_base_url) :
109
+ if not abort_id :
110
+ if not request.headers.get('abortid') :
111
+ abort_id = getRandomKeysAndStr(n=20)
112
+ else :
113
+ abort_id = request.headers.get('abortid')
114
+
115
+ Abort.abort_id = abort_id
116
+ current_thread = threading.current_thread()
117
+ Abort.thread = current_thread
118
+ Abort.cache = self.cache
119
+ Abort.start_ts = getTimestamp()
120
+ self.cache.set( Abort , custom_key=abort_id , auto_destroy_period=120 , store_deleted_key=False )
121
+ return Abort
122
+
123
+ def start_before_request(self) :
124
+
125
+ @self.app.route(f'{self.abort_base_url}/<id>' , methods=['DELETE'])
126
+ def abort_request(id : str ) :
127
+ abort : AbortRequest = self.cache.get(id)
128
+ timestamp = getTimestamp()
129
+ if abort :
130
+ abort.abort()
131
+ for i in range(30) :
132
+ th = abort.thread
133
+ alive = th.is_alive()
134
+ if not alive :
135
+ break
136
+ time.sleep(.25)
137
+ return { 'status' : 200 , 'message' : 'Request aborted' , 'abort_timestamp' : timestamp , 'abort_id' : id , 'alive' : alive , 'url' : abort.request.get('path')}
138
+ else :
139
+ return { 'status' : 404 , 'message' : 'Request not found or request is not abortable. Check request headers for abortable flag.'}
140
+
141
+ @self.app.before_request
142
+ def before_request() :
143
+ abortable = request.headers.get('abortable')
144
+ if abortable :
145
+ abort = self.register_abortable_request(request)
146
+ request.abortable = True
147
+ request.abort_id = abort.abort_id
148
+ # check here if async in the headers
149
+ # if yes . i will trigger the function in thread
150
+ # start_tread(#how to get the target function here ? )
151
+ # now i want to return response to UI { status : 200 , message : 'request now in running bg' , abort_id : abort.abort_id }
152
+ # the flask function should not be called again
153
+ if request.headers.get('async') == 'false' :
154
+ target_func = current_app.view_functions.get(request.endpoint)
155
+ if not target_func:
156
+ return jsonify({"error": "Route not found"}), 404
157
+ th = start_thread(target=target_func, args=request.args, kwargs=request.form)
158
+ abort.thread = th
159
+ return {"status": 200, "message": "Request now in running bg", "abort_id": abort.abort_id} , 200
160
+
161
+ @self.app.after_request
162
+ def after_request(response) :
163
+ try :
164
+ if request.abortable :
165
+ response.headers['abortid'] = request.abort_id
166
+ response.headers['abortable'] = True
167
+ except :
168
+ response.headers['abortable'] = False
169
+ return response
131
170
 
132
- def getAbort(self , id ) :
133
- result : Abort = self.abort_requests.get(id , Abort)
134
- return result
135
-
136
- def updateAbort( self , id , abort ) :
137
- self.abort_requests[id] = abort
138
171
 
139
172
  def getInstance(self) :
140
173
  return self.getFlask() , self.getSocketio() , self.getWsgi()
@@ -181,6 +214,7 @@ class UISERVER :
181
214
  wsgi_server.serve_forever()
182
215
 
183
216
  def startUi(self ,daemon , suppress_prints=True) :
217
+ self.start_before_request()
184
218
  self.thread = self.flaskprocess = Thread(target=self.thrStartUi , args=[suppress_prints])
185
219
  self.flaskprocess.daemon = False
186
220
  self.flaskprocess.start()
@@ -1,7 +1,7 @@
1
1
  from easy_utils_dev.debugger import DEBUGGER
2
2
  import requests , json , subprocess
3
3
  from requests.auth import HTTPBasicAuth as BAuth
4
- from .utils import pingAddress , fixTupleForSql , start_thread , mkdirs
4
+ from .utils import pingAddress , fixTupleForSql , start_thread , mkdirs , getTimestamp
5
5
  from time import sleep
6
6
  from urllib3.exceptions import InsecureRequestWarning
7
7
  from urllib3 import disable_warnings
@@ -15,7 +15,8 @@ import tempfile , os
15
15
  from kafka import KafkaConsumer
16
16
  from easy_utils_dev.utils import kill_thread
17
17
  import atexit
18
-
18
+ from snakebite.client import Client as HdfsClient
19
+ from datetime import datetime
19
20
 
20
21
 
21
22
  class KafkaConfig :
@@ -38,7 +39,6 @@ class KafkaConfig :
38
39
  self.enable_auto_refresh = False
39
40
 
40
41
 
41
-
42
42
  class WSNOCLIB :
43
43
  def __init__(
44
44
  self,
@@ -51,7 +51,8 @@ class WSNOCLIB :
51
51
  request_max_count=30,
52
52
  tmp_dir = tempfile.gettempdir() ,
53
53
  kafka = KafkaConfig(),
54
- register_atexit=True
54
+ register_atexit=True,
55
+ trust_env=True
55
56
  ):
56
57
  self.logger = DEBUGGER(f'{debug_name}-{ip}',level=debug_level,homePath=debug_homepath)
57
58
  self.disabledWarnings = self.disableUrlWarnings()
@@ -59,6 +60,7 @@ class WSNOCLIB :
59
60
  self.address = ip
60
61
  self.username = username
61
62
  self.password = password
63
+ self.trust_env = trust_env
62
64
  self.external_nsp = False
63
65
  self.api_count = 0
64
66
  self.api_count_limit = 999999999999
@@ -82,6 +84,8 @@ class WSNOCLIB :
82
84
  self.refresh_thread = None
83
85
  self.token_refresh_count = 0
84
86
  self.session = WSNOCSession(self)
87
+ self.connected = False
88
+ self.pm_hadoop = PmHadoopClient(self)
85
89
  if register_atexit :
86
90
  atexit.register(self.goodbye)
87
91
 
@@ -150,6 +154,7 @@ class WSNOCLIB :
150
154
  if auto_refresh_token :
151
155
  self.autoRefreshThread = self.refresh_thread = start_thread(target=self.runAutoRefreshThread)
152
156
  self.logger.debug(f'token => {r.text}')
157
+ self.connected = True
153
158
  return self.token
154
159
 
155
160
 
@@ -182,6 +187,7 @@ class WSNOCLIB :
182
187
  r.close()
183
188
  except :
184
189
  pass
190
+ self.connected = False
185
191
  return True
186
192
 
187
193
  def goodbye(self):
@@ -610,6 +616,7 @@ class WSNOCLIB :
610
616
  self.bearer_token = f'Bearer {self.access_token}'
611
617
  self.token = r.json()
612
618
  self.token.update({'bearer_token' : self.bearer_token })
619
+ self.connected = True
613
620
  return r
614
621
 
615
622
  def session_info(self) :
@@ -706,6 +713,9 @@ class WSNOCSession(requests.Session):
706
713
  self._wsnoc = wsnoc
707
714
  self.verify = False
708
715
  self.retries = 0
716
+ if not wsnoc.trust_env :
717
+ os.environ['https_proxy'] = ""
718
+ os.environ['http_proxy'] = ""
709
719
  self.debug_this_request = False
710
720
  self.skip_hold_for_token_refresh = False
711
721
 
@@ -749,6 +759,96 @@ class WSNOCSession(requests.Session):
749
759
  self._wsnoc.logger.info(f'[{method}] : {url} - [{request.status_code}]')
750
760
  return request
751
761
 
762
+ class PmHadoopClient :
763
+
764
+
765
+ def __init__(self , _wsnoc : WSNOCLIB):
766
+ self.ip = _wsnoc.address
767
+ self.wsnoc = _wsnoc
768
+ self.client : HdfsClient = None
769
+ self.hdfs_port = 8020
770
+ self.hdfs_user = 'otn'
771
+ self.hdfs_root = '/'
772
+ self.hdfs_use_trash = False
773
+ self.PM24H = 1
774
+ self.PM15M = 2
775
+ self.KPIAGGR = 3
776
+ self.logger : DEBUGGER = self.wsnoc.logger
777
+
778
+
779
+ def connect(self) :
780
+ if not self.wsnoc.connected :
781
+ raise Exception('WSNOC is not connected')
782
+ try :
783
+ self.client = HdfsClient(self.ip, self.hdfs_port, use_trash=self.hdfs_use_trash)
784
+ except Exception as e:
785
+ self.logger.warning('PM Hadoop client is using WSNOC port 8020/custom port. Please check if it is not blocked by firewall.' , source='PmHadoopClient')
786
+ self.logger.error(f'Failed to connect to PM Hadoop: {e}' , source='PmHadoopClient')
787
+ raise
788
+
789
+ def change_date_to_timestamp(self , date_str) :
790
+ '''
791
+ this is a helper function to convert date string to timestamp
792
+ date_str : must be in the format of %Y%m%d example: 20250101
793
+ return : timestamp
794
+ '''
795
+ dt = datetime.strptime(date_str, "%Y%m%d")
796
+ return int(dt.timestamp())
797
+
798
+ def pm_list(self , mode , date_range=[]) :
799
+ '''
800
+ mode : must be one of the following:
801
+ - self.PM24H
802
+ - self.PM15M
803
+ - self.KPIAGGR
804
+ date_range : must be a list of two integers in the format of [start_timestamp, end_timestamp]
805
+ - for example: [1718217600, 1718221200]
806
+ - if date_range is not provided, all available PM dates will be returned
807
+ '''
808
+ if mode == self.PM24H :
809
+ _mode = 'ONE_DAY'
810
+ elif mode == self.PM15M :
811
+ _mode = 'FIFTEEN_MINS'
812
+ elif mode == self.KPIAGGR :
813
+ _mode = 'KPIAGGR'
814
+ else :
815
+ raise Exception(f'Invalid mode: {mode}')
816
+ self.logger.info(f"Getting available PM dates for {_mode}" , source='PmHadoopClient')
817
+ dirs = list(self.client.ls([f'/PMDATA/{_mode}']))
818
+ ts_now = getTimestamp()
819
+ for index , dir in enumerate(dirs) :
820
+ self.logger.info(f"Processing {dir.get('path')}" , source='PmHadoopClient')
821
+ date_str = dir.get('path').split('=')[-1]
822
+ dir['pm_date'] = int(date_str)
823
+ dir['mode'] = _mode
824
+ dt = datetime.strptime(date_str, "%Y%m%d")
825
+ dir['pm_date_timestamp'] = int(dt.timestamp())
826
+ if len(date_range) > 0 :
827
+ if dir['pm_date_timestamp'] < date_range[0] :
828
+ del dirs[index]
829
+ if dir['pm_date_timestamp'] > date_range[1] :
830
+ del dirs[index]
831
+ return dirs
832
+
833
+ def _download_dir(self , hdfs_path, local_path):
834
+ for entry in self.client.ls([hdfs_path]):
835
+ # print(entry)
836
+ entry_path = entry['path']
837
+ entry_type = entry['file_type']
838
+ if entry_type == 'd':
839
+ subdir = os.path.join(local_path, os.path.basename(entry_path))
840
+ mkdirs(subdir)
841
+ self._download_dir(entry_path, subdir)
842
+ else: # FILE
843
+ self.logger.debug(f"Downloading {entry_path} → {local_path}" , source='PmHadoopClient')
844
+ self.client.copyToLocal([entry_path], local_path)
845
+
846
+ def download(self , obj , destination_path) :
847
+ self.wsnoc.logger.info(f'Downloading {obj.get("pm_date")} to {destination_path}')
848
+ destination_path = f"{destination_path}/{obj.get('mode')}/{obj.get('pm_date')}"
849
+ mkdirs(destination_path)
850
+ self._download_dir(obj.get('path'), destination_path)
851
+
752
852
 
753
853
  if __name__ == '__main__' :
754
854
  # noc = WSNOCLIB('10.20.30.55' , 'admin' , 'Nokia@2024')
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easy_utils_dev
3
- Version: 2.140
3
+ Version: 2.142
4
4
  Keywords: python3
5
5
  Classifier: Programming Language :: Python :: 3
6
6
  Requires-Dist: psutil
7
7
  Requires-Dist: ping3
8
+ Requires-Dist: snakebite-py3
8
9
  Requires-Dist: flask
9
10
  Requires-Dist: flask_cors
10
11
  Requires-Dist: xmltodict
@@ -25,14 +25,14 @@ easy_utils_dev/optics_utils.py,sha256=G-hFX2iiUCSJjk7BICBRGvVoDq0IBONLZSjagoB5FM
25
25
  easy_utils_dev/require_auth.py,sha256=UsYAxfLX5wda6hd0nfLR_tl0bGQ4DYIpaTowsYSku-E,881
26
26
  easy_utils_dev/simple_sqlite.py,sha256=J-mcTUnHmAn0eCPD8j-WEoA19uzHRXJ4YRJsyx9B-do,13113
27
27
  easy_utils_dev/temp_memory.py,sha256=0Dx_vNUSFQRMtJZNT8tUZXubcG7T6jxevw2WYuGmVe8,1658
28
- easy_utils_dev/uiserver.py,sha256=d9jImwgSURMH4MIU4Zkro2b20cAbuV8rO_If42FVyZ4,6826
28
+ easy_utils_dev/uiserver.py,sha256=zR-3DCo2FUF6Z7vpnzrKuWUl1MC0TparS8_LEDOyuuc,8886
29
29
  easy_utils_dev/utils.py,sha256=BmVnbxc336c6WTeDFcEHN6Mavt7fJrIEyK4GXODV3gI,13345
30
30
  easy_utils_dev/winserviceapi.py,sha256=2ZP6jaSt1-5vEJYXqwBhwX-1-eQ3V3YzntsoOoko2cw,18804
31
- easy_utils_dev/wsnoclib.py,sha256=kqkOkiafXnrIgdlV__3uyrAoqxG5dmIHnwqm0EkwS2U,33457
31
+ easy_utils_dev/wsnoclib.py,sha256=idg6V2K9CEHq_SvMqqmAihZdl61LAmqvTGDmp4hl4sM,37733
32
32
  easy_utils_dev/wsselib.py,sha256=YweScnoAAH_t29EeIjBpkQ6HtX0Rp9mQudRsRce2SE8,7920
33
33
  easy_utils_dev/ept_sql/create_dirs.sql,sha256=KWfX-Nc6lvr_BC-P6O97NE0idoPW4GNKUKUCgonJhto,3508
34
34
  easy_utils_dev/ept_sql/create_ept_tables.sql,sha256=WDHyIyeReV8_QaYBPIpSy-lto3OKvZtex1tWs-FPURQ,67737
35
- easy_utils_dev-2.140.dist-info/METADATA,sha256=11wRCPmmDl1s7jptVfwy-_ru_jnkTn2nJMcJ-dM07uM,572
36
- easy_utils_dev-2.140.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
- easy_utils_dev-2.140.dist-info/top_level.txt,sha256=7vBsrpq7NmilkdU3YUvfd5iVDNBaT07u_-ut4F7zc7A,15
38
- easy_utils_dev-2.140.dist-info/RECORD,,
35
+ easy_utils_dev-2.142.dist-info/METADATA,sha256=YHAkh7rEopSSfFi9AxuqfXERrMEYFPDkcItFbPN8wGs,602
36
+ easy_utils_dev-2.142.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
+ easy_utils_dev-2.142.dist-info/top_level.txt,sha256=7vBsrpq7NmilkdU3YUvfd5iVDNBaT07u_-ut4F7zc7A,15
38
+ easy_utils_dev-2.142.dist-info/RECORD,,