OfflineMeterFileCollection.on_post()   F
last analyzed

Complexity

Conditions 20

Size

Total Lines 136
Code Lines 95

Duplication

Lines 136
Ratio 100 %

Importance

Changes 0
Metric Value
eloc 95
dl 136
loc 136
rs 0
c 0
b 0
f 0
cc 20
nop 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like core.offlinemeterfile.OfflineMeterFileCollection.on_post() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
import os
2
import uuid
3
from datetime import datetime, timezone, timedelta
4
import falcon
5
import mysql.connector
6
from mysql.connector.errors import InterfaceError, OperationalError, ProgrammingError, DataError
7
import simplejson as json
8
import redis
9
from core.useractivity import user_logger, admin_control, access_control, api_key_control
10
import config
11
12
13 View Code Duplication
def clear_offline_meter_file_cache(offline_meter_file_id=None):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
14
    """
15
    Clear offline meter file-related cache after data modification
16
17
    Args:
18
        offline_meter_file_id: Offline meter file ID (optional, for specific file cache)
19
    """
20
    # Check if Redis is enabled
21
    if not config.redis.get('is_enabled', False):
22
        return
23
24
    redis_client = None
25
    try:
26
        redis_client = redis.Redis(
27
            host=config.redis['host'],
28
            port=config.redis['port'],
29
            password=config.redis['password'] if config.redis['password'] else None,
30
            db=config.redis['db'],
31
            decode_responses=True,
32
            socket_connect_timeout=2,
33
            socket_timeout=2
34
        )
35
        redis_client.ping()
36
37
        # Clear offline meter file list cache
38
        list_cache_key = 'offlinemeterfile:list'
39
        redis_client.delete(list_cache_key)
40
41
        # Clear specific offline meter file item cache if offline_meter_file_id is provided
42
        if offline_meter_file_id:
43
            item_cache_key = f'offlinemeterfile:item:{offline_meter_file_id}'
44
            redis_client.delete(item_cache_key)
45
46
    except Exception:
47
        # If cache clear fails, ignore and continue
48
        pass
49
50
51
class OfflineMeterFileCollection:
52
    """
53
    Offline Meter File Collection Resource
54
55
    This class handles CRUD operations for offline meter file collection.
56
    It provides endpoints for listing all uploaded offline meter files and uploading new files.
57
    Offline meter files are used for importing energy data from external sources.
58
    """
59
    def __init__(self):
60
        """Initialize OfflineMeterFileCollection"""
61
        pass
62
63
    @staticmethod
64
    def on_options(req, resp):
65
        """Handle OPTIONS requests for CORS preflight"""
66
        _ = req
67
        resp.status = falcon.HTTP_200
68
69
    @staticmethod
70
    def on_get(req, resp):
71
        """
72
        Handle GET requests to retrieve all offline meter files
73
74
        Returns a list of all uploaded offline meter files with their metadata including:
75
        - File ID, name, and UUID
76
        - Upload datetime (converted to local timezone)
77
        - Processing status
78
79
        Args:
80
            req: Falcon request object
81
            resp: Falcon response object
82
        """
83
        # Check authentication - use API key if provided, otherwise use access control
84
        if 'API-KEY' not in req.headers or \
85
                not isinstance(req.headers['API-KEY'], str) or \
86
                len(str.strip(req.headers['API-KEY'])) == 0:
87
            access_control(req)
88
        else:
89
            api_key_control(req)
90
91
        # Redis cache key
92
        cache_key = 'offlinemeterfile:list'
93
        cache_expire = 28800  # 8 hours in seconds (long-term cache)
94
95
        # Try to get from Redis cache (only if Redis is enabled)
96
        redis_client = None
97
        if config.redis.get('is_enabled', False):
98
            try:
99
                redis_client = redis.Redis(
100
                    host=config.redis['host'],
101
                    port=config.redis['port'],
102
                    password=config.redis['password'] if config.redis['password'] else None,
103
                    db=config.redis['db'],
104
                    decode_responses=True,
105
                    socket_connect_timeout=2,
106
                    socket_timeout=2
107
                )
108
                redis_client.ping()
109
                cached_result = redis_client.get(cache_key)
110
                if cached_result:
111
                    resp.text = cached_result
112
                    return
113
            except Exception:
114
                # If Redis connection fails, continue to database query
115
                pass
116
117
        # Cache miss or Redis error - query database
118
        # Connect to historical database
119
        cnx = mysql.connector.connect(**config.myems_historical_db)
120
        cursor = cnx.cursor()
121
122
        # Get all offline meter files ordered by upload time
123
        query = (" SELECT id, file_name, uuid, upload_datetime_utc, status "
124
                 " FROM tbl_offline_meter_files "
125
                 " ORDER BY upload_datetime_utc desc ")
126
        cursor.execute(query)
127
        rows = cursor.fetchall()
128
        cursor.close()
129
        cnx.close()
130
131
        # Calculate timezone offset for datetime conversion
132
        timezone_offset = int(config.utc_offset[1:3]) * 60 + int(config.utc_offset[4:6])
133
        if config.utc_offset[0] == '-':
134
            timezone_offset = -timezone_offset
135
136
        # Build result list with converted datetime
137
        result = list()
138
        if rows is not None and len(rows) > 0:
139
            for row in rows:
140
                meta_result = {"id": row[0],
141
                               "file_name": row[1],
142
                               "uuid": row[2],
143
                               "upload_datetime": (row[3].replace(tzinfo=timezone.utc) +
144
                                                   timedelta(minutes=timezone_offset)).isoformat()[0:19],
145
                               "status": row[4]}
146
                result.append(meta_result)
147
148
        # Store result in Redis cache
149
        result_json = json.dumps(result)
150
        if redis_client:
151
            try:
152
                redis_client.setex(cache_key, cache_expire, result_json)
153
            except Exception:
154
                # If cache set fails, ignore and continue
155
                pass
156
157
        resp.text = result_json
158
159 View Code Duplication
    @staticmethod
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
160
    @user_logger
161
    def on_post(req, resp):
162
        """
163
        Handle POST requests to upload a new offline meter file
164
165
        Uploads a file containing offline meter data for processing.
166
        The file is saved to disk and metadata is stored in the database.
167
168
        Args:
169
            req: Falcon request object containing file upload
170
            resp: Falcon response object
171
        """
172
        admin_control(req)
173
174
        try:
175
            # Get uploaded file from request
176
            upload = req.get_param('file')
177
            # Read upload file as binary
178
            raw_blob = upload.file.read()
179
            # Retrieve filename
180
            filename = upload.filename
181
            file_uuid = str(uuid.uuid4())
182
183
            # Define file_path
184
            file_path = os.path.join(config.upload_path, file_uuid)
185
186
            # Write to a temporary file to prevent incomplete files from being used.
187
            with open(file_path + '~', 'wb') as f:
188
                f.write(raw_blob)
189
190
            # Now that we know the file has been fully saved to disk move it into place.
191
            os.rename(file_path + '~', file_path)
192
        except OSError as ex:
193
            print("Failed to stream request")
194
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.ERROR',
195
                                   description='API.FAILED_TO_UPLOAD_OFFLINE_METER_FILE')
196
        except Exception as ex:
197
            print("Unexpected error reading request stream")
198
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.ERROR',
199
                                   description='API.FAILED_TO_UPLOAD_OFFLINE_METER_FILE')
200
201
        # Verify User Session
202
        token = req.headers.get('TOKEN')
203
        user_uuid = req.headers.get('USER-UUID')
204
        if token is None:
205
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
206
                                   description='API.TOKEN_NOT_FOUND_IN_HEADERS_PLEASE_LOGIN')
207
        if user_uuid is None:
208
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
209
                                   description='API.USER_UUID_NOT_FOUND_IN_HEADERS_PLEASE_LOGIN')
210
211
        # Validate user session
212
        cnx = mysql.connector.connect(**config.myems_user_db)
213
        cursor = cnx.cursor()
214
215
        query = (" SELECT utc_expires "
216
                 " FROM tbl_sessions "
217
                 " WHERE user_uuid = %s AND token = %s")
218
        cursor.execute(query, (user_uuid, token,))
219
        row = cursor.fetchone()
220
221
        if row is None:
222
            if cursor:
223
                cursor.close()
224
            if cnx:
225
                cnx.close()
226
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
227
                                   description='API.INVALID_SESSION_PLEASE_RE_LOGIN')
228
        else:
229
            utc_expires = row[0]
230
            if datetime.utcnow() > utc_expires:
231
                if cursor:
232
                    cursor.close()
233
                if cnx:
234
                    cnx.close()
235
                raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
236
                                       description='API.USER_SESSION_TIMEOUT')
237
238
        # Verify user exists
239
        cursor.execute(" SELECT id "
240
                       " FROM tbl_users "
241
                       " WHERE uuid = %s ",
242
                       (user_uuid,))
243
        row = cursor.fetchone()
244
        if row is None:
245
            if cursor:
246
                cursor.close()
247
            if cnx:
248
                cnx.close()
249
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
250
                                   description='API.INVALID_USER_PLEASE_RE_LOGIN')
251
252
        # Save file metadata to database
253
        try:
254
            cnx = mysql.connector.connect(**config.myems_historical_db)
255
            cursor = cnx.cursor()
256
257
            add_values = (" INSERT INTO tbl_offline_meter_files "
258
                          " (file_name, uuid, upload_datetime_utc, status, file_object ) "
259
                          " VALUES (%s, %s, %s, %s, %s) ")
260
            cursor.execute(add_values, (filename,
261
                                        file_uuid,
262
                                        datetime.utcnow(),
263
                                        'new',
264
                                        raw_blob))
265
            new_id = cursor.lastrowid
266
            cnx.commit()
267
            cursor.close()
268
            cnx.close()
269
        except InterfaceError as e:
270
            print("Failed to connect request")
271
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.ERROR',
272
                                   description='API.FAILED_TO_SAVE_OFFLINE_METER_FILE')
273
        except OperationalError as e:
274
            print("Failed to SQL operate request")
275
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.ERROR',
276
                                   description='API.FAILED_TO_SAVE_OFFLINE_METER_FILE')
277
        except ProgrammingError as e:
278
            print("Failed to SQL request")
279
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.ERROR',
280
                                   description='API.FAILED_TO_SAVE_OFFLINE_METER_FILE')
281
        except DataError as e:
282
            print("Failed to SQL Data request")
283
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.ERROR',
284
                                   description='API.FAILED_TO_SAVE_OFFLINE_METER_FILE')
285
        except Exception as e:
286
            print("API.FAILED_TO_SAVE_OFFLINE_METER_FILE " + str(e))
287
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.ERROR',
288
                                   description='API.FAILED_TO_SAVE_OFFLINE_METER_FILE')
289
290
        # Clear cache after creating new offline meter file
291
        clear_offline_meter_file_cache()
292
293
        resp.status = falcon.HTTP_201
294
        resp.location = '/offlinemeterfiles/' + str(new_id)
295
296
297
class OfflineMeterFileItem:
298
    """
299
    Offline Meter File Item Resource
300
301
    This class handles individual offline meter file operations.
302
    It provides endpoints for retrieving and deleting specific offline meter files.
303
    """
304
305
    def __init__(self):
306
        """Initialize OfflineMeterFileItem"""
307
        pass
308
309
    @staticmethod
310
    def on_options(req, resp, id_):
311
        """Handle OPTIONS requests for CORS preflight"""
312
        _ = req
313
        resp.status = falcon.HTTP_200
314
        _ = id_
315
316
    @staticmethod
317
    def on_get(req, resp, id_):
318
        """
319
        Handle GET requests to retrieve a specific offline meter file
320
321
        Returns the offline meter file details for the specified ID.
322
323
        Args:
324
            req: Falcon request object
325
            resp: Falcon response object
326
            id_: Offline meter file ID to retrieve
327
        """
328
        admin_control(req)
329
330
        # Validate file ID
331
        if not id_.isdigit() or int(id_) <= 0:
332
            raise falcon.HTTPError(status=falcon.HTTP_400,
333
                                   title='API.BAD_REQUEST',
334
                                   description='API.INVALID_OFFLINE_METER_FILE_ID')
335
336
        # Redis cache key
337
        cache_key = f'offlinemeterfile:item:{id_}'
338
        cache_expire = 28800  # 8 hours in seconds (long-term cache)
339
340
        # Try to get from Redis cache (only if Redis is enabled)
341
        redis_client = None
342
        if config.redis.get('is_enabled', False):
343
            try:
344
                redis_client = redis.Redis(
345
                    host=config.redis['host'],
346
                    port=config.redis['port'],
347
                    password=config.redis['password'] if config.redis['password'] else None,
348
                    db=config.redis['db'],
349
                    decode_responses=True,
350
                    socket_connect_timeout=2,
351
                    socket_timeout=2
352
                )
353
                redis_client.ping()
354
                cached_result = redis_client.get(cache_key)
355
                if cached_result:
356
                    resp.text = cached_result
357
                    return
358
            except Exception:
359
                # If Redis connection fails, continue to database query
360
                pass
361
362
        # Cache miss or Redis error - query database
363
        # Connect to historical database
364
        cnx = mysql.connector.connect(**config.myems_historical_db)
365
        cursor = cnx.cursor()
366
367
        # Get file details
368
        query = (" SELECT id, file_name, uuid, upload_datetime_utc, status "
369
                 " FROM tbl_offline_meter_files "
370
                 " WHERE id = %s ")
371
        cursor.execute(query, (id_,))
372
        row = cursor.fetchone()
373
        cursor.close()
374
        cnx.close()
375
376
        # Check if file exists
377
        if row is None:
378
            raise falcon.HTTPError(status=falcon.HTTP_404, title='API.NOT_FOUND',
379
                                   description='API.OFFLINE_METER_FILE_NOT_FOUND')
380
381
        # Calculate timezone offset for datetime conversion
382
        timezone_offset = int(config.utc_offset[1:3]) * 60 + int(config.utc_offset[4:6])
383
        if config.utc_offset[0] == '-':
384
            timezone_offset = -timezone_offset
385
386
        # Build result with converted datetime
387
        result = {"id": row[0],
388
                  "file_name": row[1],
389
                  "uuid": row[2],
390
                  "upload_datetime": (row[3].replace(tzinfo=timezone.utc) +
391
                                      timedelta(minutes=timezone_offset)).isoformat()[0:19],
392
                  "status": row[4]}
393
394
        # Store result in Redis cache
395
        result_json = json.dumps(result)
396
        if redis_client:
397
            try:
398
                redis_client.setex(cache_key, cache_expire, result_json)
399
            except Exception:
400
                # If cache set fails, ignore and continue
401
                pass
402
403
        resp.text = result_json
404
405
    @staticmethod
406
    @user_logger
407
    def on_delete(req, resp, id_):
408
        """
409
        Handle DELETE requests to delete a specific offline meter file
410
411
        Deletes the offline meter file with the specified ID.
412
        Removes both the file from disk and metadata from database.
413
        Note: Energy data imported from the deleted file will not be deleted.
414
415
        Args:
416
            req: Falcon request object
417
            resp: Falcon response object
418
            id_: Offline meter file ID to delete
419
        """
420
        admin_control(req)
421
422
        # Validate file ID
423
        if not id_.isdigit() or int(id_) <= 0:
424
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
425
                                   description='API.INVALID_OFFLINE_METER_FILE_ID')
426
427
        # Connect to historical database
428
        cnx = mysql.connector.connect(**config.myems_historical_db)
429
        cursor = cnx.cursor()
430
431
        # Get file UUID for file deletion
432
        cursor.execute(" SELECT uuid "
433
                       " FROM tbl_offline_meter_files "
434
                       " WHERE id = %s ", (id_,))
435
        row = cursor.fetchone()
436
        if row is None:
437
            cursor.close()
438
            cnx.close()
439
            raise falcon.HTTPError(status=falcon.HTTP_404, title='API.NOT_FOUND',
440
                                   description='API.OFFLINE_METER_FILE_NOT_FOUND')
441
442
        # Remove file from disk
443
        try:
444
            file_uuid = row[0]
445
            # Define file_path
446
            file_path = os.path.join(config.upload_path, file_uuid)
447
448
            # remove the file from disk
449
            os.remove(file_path)
450
        except OSError as ex:
451
            print("Failed to stream request")
452
        except Exception as ex:
453
            print("Unexpected error reading request stream")
454
            # ignore exception and don't return API.OFFLINE_METER_FILE_NOT_FOUND error
455
            pass
456
457
        # Note: the energy data imported from the deleted file will not be deleted
458
        cursor.execute(" DELETE FROM tbl_offline_meter_files WHERE id = %s ", (id_,))
459
        cnx.commit()
460
461
        cursor.close()
462
        cnx.close()
463
464
        # Clear cache after deleting offline meter file
465
        clear_offline_meter_file_cache(offline_meter_file_id=int(id_))
466
467
        resp.status = falcon.HTTP_204
468
469
470 View Code Duplication
class OfflineMeterFileRestore:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
471
    """
472
    Offline Meter File Restore Resource
473
474
    This class handles restoring offline meter files from database to disk.
475
    It provides functionality to restore files that may have been lost from disk.
476
    """
477
478
    def __init__(self):
479
        """Initialize OfflineMeterFileRestore"""
480
        pass
481
482
    @staticmethod
483
    def on_options(req, resp, id_):
484
        """Handle OPTIONS requests for CORS preflight"""
485
        _ = req
486
        resp.status = falcon.HTTP_200
487
        _ = id_
488
489
    @staticmethod
490
    def on_get(req, resp, id_):
491
        """
492
        Handle GET requests to restore a specific offline meter file
493
494
        Restores the offline meter file from database to disk.
495
        This is useful when files have been lost from disk but metadata still exists.
496
497
        Args:
498
            req: Falcon request object
499
            resp: Falcon response object
500
            id_: Offline meter file ID to restore
501
        """
502
        admin_control(req)
503
504
        # Validate file ID
505
        if not id_.isdigit() or int(id_) <= 0:
506
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.BAD_REQUEST',
507
                                   description='API.INVALID_OFFLINE_METER_FILE_ID')
508
509
        # Connect to historical database
510
        cnx = mysql.connector.connect(**config.myems_historical_db)
511
        cursor = cnx.cursor()
512
513
        # Get file data from database
514
        query = (" SELECT uuid, file_object "
515
                 " FROM tbl_offline_meter_files "
516
                 " WHERE id = %s ")
517
        cursor.execute(query, (id_,))
518
        row = cursor.fetchone()
519
        cursor.close()
520
        cnx.close()
521
522
        # Check if file exists in database
523
        if row is None:
524
            raise falcon.HTTPError(status=falcon.HTTP_404, title='API.NOT_FOUND',
525
                                   description='API.OFFLINE_METER_FILE_NOT_FOUND')
526
527
        # Restore file to disk
528
        result = {"uuid": row[0],
529
                  "file_object": row[1]}
530
        try:
531
            raw_blob = result["file_object"]
532
            file_uuid = result["uuid"]
533
534
            # Define file_path
535
            file_path = os.path.join(config.upload_path, file_uuid)
536
537
            # Write to a temporary file to prevent incomplete files from being used.
538
            temp_file_path = file_path + '~'
539
540
            with open(temp_file_path, 'wb') as f:
541
                f.write(raw_blob)
542
543
            # Now that we know the file has been fully saved to disk move it into place.
544
            os.replace(temp_file_path, file_path)
545
        except OSError as ex:
546
            print("Failed to stream request")
547
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.ERROR',
548
                                   description='API.FAILED_TO_RESTORE_OFFLINE_METER_FILE')
549
        except Exception as ex:
550
            print("Unexpected error reading request stream")
551
            raise falcon.HTTPError(status=falcon.HTTP_400, title='API.ERROR',
552
                                   description='API.FAILED_TO_RESTORE_OFFLINE_METER_FILE')
553
        resp.text = json.dumps('success')
554