sliderule-cli 5.6.1__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.
@@ -0,0 +1,533 @@
1
+ import csv
2
+ import sys
3
+ import copy
4
+ import time
5
+ import argparse
6
+ import boto3
7
+ import pandas as pd
8
+ import numpy as np
9
+ import geoip2.database
10
+ from PIL import Image
11
+ from matplotlib.colors import LogNorm
12
+ import matplotlib.cm as cm
13
+ from datetime import datetime, timezone
14
+ from collections import defaultdict
15
+
16
+ # -------------------------------------------
17
+ # command line arguments
18
+ # -------------------------------------------
19
+ parser = argparse.ArgumentParser(
20
+ description="""SlideRule Usage Report""",
21
+ formatter_class=argparse.RawDescriptionHelpFormatter,
22
+ epilog=(
23
+ "Examples:\n"
24
+ " python usage_report.py --start \"2026-01-10\" --end \"2026-01-29\"\n"
25
+ " python usage_report.py --start \"2026-01-10\"\n"
26
+ )
27
+ )
28
+ parser.add_argument('--start', type=str, required=True, help='start is required as ISO datetime string YYYY-MM-DD HH:MM:SS)') #
29
+ parser.add_argument('--end', type=str, default=f'{datetime.now()}') # optional ISO datetime string
30
+ parser.add_argument('--grid', action='store_true', default=False, help="generates grids")
31
+ parser.add_argument('--grid_aoi', type=str, default="/data/web/sliderule_aoi_grid.png")
32
+ parser.add_argument('--grid_usage', type=str, default="/data/web/sliderule_usage_grid.png")
33
+ parser.add_argument('--geonames', type=str, default="/data/geopy/cities500.txt") # from http://download.geonames.org/export/dump/cities500.zip
34
+ parser.add_argument('--country_info', type=str, default="/data/geopy/countryInfo.txt") # from http://download.geonames.org/export/dump/countryInfo.txt
35
+ args = parser.parse_args()
36
+
37
+ # -------------------------------------------
38
+ # constants
39
+ # -------------------------------------------
40
+ GLUE_DATABASE = 'recorder-database'
41
+ ATHENA_WORKGROUP = 'recorder-workgroup'
42
+ TELEMETRY_TABLE = 'telemetry'
43
+ ALERTS_TABLE = 'alerts'
44
+ PROJECT_BUCKET = 'sliderule'
45
+ AWS_REGION = 'us-west-2'
46
+ GEOLITE2_CITY_DB = '/data/GeoLite2-City.mmdb'
47
+ GEOLITE2_COUNTRY_DB = '/data/GeoLite2-Country.mmdb'
48
+ QUERY_WAIT_SECONDS = 300
49
+ QUERY_POLL_SECONDS = 2
50
+ MAX_LABEL_LEN = 30
51
+ MAX_VALUE_LEN = 20
52
+ ICESAT2_ENDPOINTS = ['atl03x', 'atl03s', 'atl03v', 'atl06', 'atl06s', 'atl06x', 'atl08', 'atl08x', 'atl13s', 'atl13x', 'atl24x']
53
+ ICESAT2_PROXY_ENDPOINTS = ['atl03x', 'atl03sp', 'atl03vp', 'atl06p', 'atl06sp', 'atl08p', 'atl13sp', 'atl13x', 'atl24x', 'atl06x', 'atl08x']
54
+ GEDI_ENDPOINTS = ['gedi01b', 'gedi02a', 'gedi04a']
55
+ GEDI_PROXY_ENDPOINTS = ['gedi01bp', 'gedi02ap', 'gedi04ap']
56
+
57
+ # -------------------------------------------
58
+ # globals
59
+ # -------------------------------------------
60
+ athena = boto3.client('athena', region_name=AWS_REGION)
61
+ glue = boto3.client('glue', region_name=AWS_REGION)
62
+ s3 = boto3.client('s3', region_name=AWS_REGION)
63
+ geo_country = geoip2.database.Reader(GEOLITE2_COUNTRY_DB)
64
+ geo_city = geoip2.database.Reader(GEOLITE2_CITY_DB)
65
+
66
+ # -------------------------------------------
67
+ # build where clause
68
+ # -------------------------------------------
69
+ def build_where_clause(start_dt, end_dt, days):
70
+ conditions = []
71
+ # build partition filter
72
+ if len(days) > 31: # enumerate individual days when the number of days is less than a typical month
73
+ parts = [f"(year = '{dt.year:04d}' AND month = '{dt.month:02d}' AND day = '{dt.day:02d}')" for dt in days]
74
+ else: # enumerate months
75
+ parts = list({f"(year = '{dt.year:04d}' AND month = '{dt.month:02d}')" for dt in days})
76
+ conditions.append( '(' + ' OR '.join(parts) + ')' )
77
+ # build timestamp expression
78
+ start_iso = start_dt.strftime('%Y-%m-%dT%H:%M:%S')
79
+ end_iso = end_dt.strftime('%Y-%m-%dT%H:%M:%S')
80
+ ts_expr = (
81
+ "CASE "
82
+ "WHEN regexp_like(timestamp, '^[0-9]+$') "
83
+ "THEN from_unixtime(CAST(timestamp AS bigint)) "
84
+ "ELSE from_iso8601_timestamp(timestamp) "
85
+ "END"
86
+ )
87
+ conditions.append(
88
+ f"{ts_expr} >= from_iso8601_timestamp('{start_iso}') AND "
89
+ f"{ts_expr} <= from_iso8601_timestamp('{end_iso}')"
90
+ )
91
+ # return where clause
92
+ return ' AND '.join(conditions)
93
+
94
+ # -------------------------------------------
95
+ # execute query
96
+ # -------------------------------------------
97
+ def execute_query(query, context=''):
98
+
99
+ # initiate athena query
100
+ request = {
101
+ 'QueryString': query,
102
+ 'QueryExecutionContext': {'Database': GLUE_DATABASE, 'Catalog': 'AwsDataCatalog'},
103
+ 'WorkGroup': ATHENA_WORKGROUP
104
+ }
105
+ response = athena.start_query_execution(**request)
106
+ query_execution_id = response['QueryExecutionId']
107
+ print(f'Initiated query on {context}', end='')
108
+
109
+ # wait for athena query to complete
110
+ status = 'IN PROGRESS'
111
+ start_time = time.time()
112
+ while (time.time() - start_time) < QUERY_WAIT_SECONDS:
113
+ response = athena.get_query_execution(QueryExecutionId=query_execution_id)
114
+ status = response['QueryExecution']['Status']['State']
115
+ if status in ['SUCCEEDED', 'FAILED', 'CANCELLED']:
116
+ break
117
+ sys.stdout.write(".")
118
+ sys.stdout.flush()
119
+ time.sleep(QUERY_POLL_SECONDS)
120
+
121
+ # check athena query status
122
+ if status == 'FAILED':
123
+ reason = response['QueryExecution']['Status'].get('StateChangeReason', 'Unknown')
124
+ raise Exception(f'Query failed: {reason}')
125
+ elif status == 'IN PROGRESS':
126
+ raise Exception('Query execution timeout')
127
+
128
+ # get athena query results
129
+ results = []
130
+ paginator = athena.get_paginator('get_query_results')
131
+ page_iterator = paginator.paginate(QueryExecutionId=query_execution_id)
132
+ column_names = None
133
+ for page in page_iterator:
134
+ for row in page['ResultSet']['Rows']:
135
+ if column_names is None:
136
+ column_names = [col.get('VarCharValue') for col in row['Data']]
137
+ continue
138
+ row_data = {}
139
+ for i, col in enumerate(row['Data']):
140
+ col_name = column_names[i]
141
+ col_value = col.get('VarCharValue') if col else None
142
+ row_data[col_name] = col_value
143
+ results.append(row_data)
144
+ sys.stdout.write("!")
145
+ sys.stdout.flush()
146
+
147
+ # return athena query results
148
+ sys.stdout.write("\n")
149
+ sys.stdout.flush()
150
+ return results
151
+
152
+ # -------------------------------------------
153
+ # list s3 day partitions
154
+ # -------------------------------------------
155
+ def list_s3_day_partitions(bucket, label, start_dt, end_dt):
156
+ found = set()
157
+ curr_dt = start_dt
158
+ while curr_dt <= end_dt:
159
+ prefix = f"{label}/year={curr_dt.year:04d}/month={curr_dt.month:02d}/"
160
+ token = None
161
+ while True:
162
+ if token:
163
+ response = s3.list_objects_v2(Bucket=bucket, Prefix=prefix, Delimiter='/', ContinuationToken=token)
164
+ else:
165
+ response = s3.list_objects_v2(Bucket=bucket, Prefix=prefix, Delimiter='/')
166
+ for cp in response.get('CommonPrefixes', []):
167
+ p = cp.get('Prefix', '')
168
+ if p.endswith('/') and p.startswith(prefix):
169
+ day_part = p[len(prefix):].strip('/')
170
+ if day_part.startswith('day='):
171
+ found.add(f"year={curr_dt.year:04d}/month={curr_dt.month:02d}/{day_part}")
172
+ token = response.get('NextContinuationToken')
173
+ if not response.get('IsTruncated'):
174
+ break
175
+ if curr_dt.month == 12:
176
+ curr_dt = curr_dt.replace(year=curr_dt.year+1, month=1, day=1)
177
+ else:
178
+ curr_dt = curr_dt.replace(month=curr_dt.month+1, day=1)
179
+ return found
180
+
181
+ # -------------------------------------------
182
+ # glue partitions
183
+ # -------------------------------------------
184
+ def glue_partitions(table_name):
185
+ partitions = []
186
+ paginator = glue.get_paginator('get_partitions')
187
+ for page in paginator.paginate(DatabaseName=GLUE_DATABASE, TableName=table_name):
188
+ for part in page.get('Partitions', []):
189
+ values = part.get('Values', [])
190
+ if len(values) >= 3:
191
+ year, month, day = values[0], values[1], values[2]
192
+ partitions.append(f'year={year}/month={month}/day={day}')
193
+ return set(partitions)
194
+
195
+ # -------------------------------------------
196
+ # ensure partitions for range
197
+ # -------------------------------------------
198
+ def ensure_partitions_for_range(table_name, expected, label, start_dt, end_dt):
199
+ # build list of requested partitions that are missing from the glue table
200
+ existing_glue = glue_partitions(table_name)
201
+ existing_s3 = list_s3_day_partitions(PROJECT_BUCKET, label, start_dt, end_dt)
202
+ missing = [p for p in expected if ((p not in existing_glue) and (p in existing_s3))]
203
+ print(f'Existing glue partitions for {label}: {len(existing_glue)}')
204
+ print(f'Existing s3 partitions for {label}: {len(existing_s3)}')
205
+ print(f'Missing partitions for {label}: {len(missing)}')
206
+
207
+ # add missing partitions to the glue table
208
+ table_def = glue.get_table(DatabaseName=GLUE_DATABASE, Name=table_name)
209
+ base_sd = table_def['Table']['StorageDescriptor']
210
+ inputs = []
211
+ for partition in missing:
212
+ kv = dict([item.split('=') for item in partition.split('/')])
213
+ location = f"s3://{PROJECT_BUCKET}/{label}/year={kv['year']}/month={kv['month']}/day={kv['day']}/"
214
+ sd = copy.deepcopy(base_sd)
215
+ sd['Location'] = location
216
+ inputs.append({'Values': [kv['year'], kv['month'], kv['day']], 'StorageDescriptor': sd})
217
+ for i in range(0, len(inputs), 100):
218
+ batch = inputs[i:i+100]
219
+ response = glue.batch_create_partition(DatabaseName=GLUE_DATABASE, TableName=table_name, PartitionInputList=batch)
220
+ for err in response.get('Errors', []):
221
+ code = err.get('ErrorDetail', {}).get('ErrorCode')
222
+ msg = err.get('ErrorDetail', {}).get('ErrorMessage')
223
+ if code != 'AlreadyExistsException':
224
+ print(f'Glue partition error: {msg}')
225
+
226
+ # -------------------------------------------
227
+ # value counts
228
+ # -------------------------------------------
229
+ def value_counts(table, field, where_clause):
230
+ query = f"""
231
+ SELECT {field} AS key, COUNT(*) AS count
232
+ FROM {table}
233
+ WHERE {where_clause}
234
+ GROUP BY {field}
235
+ """
236
+ rows = execute_query(query, f'{field} of {table}')
237
+ return {row['key']: int(row['count']) for row in rows}
238
+
239
+ # -------------------------------------------
240
+ # sum counts
241
+ # -------------------------------------------
242
+ def sum_counts(counts, include_list=None, match_str=None):
243
+ total = 0
244
+ for item in counts:
245
+ if (not include_list and not match_str) or \
246
+ (include_list and item in include_list) or \
247
+ (match_str and match_str in item):
248
+ total += counts[item]
249
+ return total
250
+
251
+ # -------------------------------------------
252
+ # locate it
253
+ # -------------------------------------------
254
+ def locateit(source_ip, debug_info):
255
+ try:
256
+ if source_ip == '0.0.0.0' or source_ip == '127.0.0.1':
257
+ return 'localhost, localhost'
258
+ country = geo_country.country(source_ip).country.name
259
+ city = geo_city.city(source_ip).city.name
260
+ return f'{country}, {city}'
261
+ except Exception as e:
262
+ print(f'Failed to get location information for <{debug_info}>: {e}')
263
+ return 'unknown, unknown'
264
+
265
+ # -------------------------------------------
266
+ # build location counts
267
+ # -------------------------------------------
268
+ def build_location_counts(ip_counts):
269
+ location_counts = {}
270
+ for ip in ip_counts:
271
+ location = locateit(ip, ip)
272
+ if location not in location_counts:
273
+ location_counts[location] = 0
274
+ location_counts[location] += ip_counts[ip]
275
+ return location_counts
276
+
277
+ # -------------------------------------------
278
+ # get timespan
279
+ # -------------------------------------------
280
+ def get_timespan(table, where_clause):
281
+ query = f"""
282
+ SELECT MIN(timestamp) AS start_time, MAX(timestamp) AS end_time
283
+ FROM {table}
284
+ WHERE {where_clause}
285
+ """
286
+ rows = execute_query(query, f'timespan of {table}')
287
+ start_unix_ts = int(rows[0]['start_time'].strip())
288
+ end_unix_ts = int(rows[0]['end_time'].strip())
289
+ start_dt = datetime.fromtimestamp(start_unix_ts, tz=timezone.utc)
290
+ end_dt = datetime.fromtimestamp(end_unix_ts, tz=timezone.utc)
291
+ return {'start': start_dt, 'end': end_dt, 'span': end_dt - start_dt}
292
+
293
+ # -------------------------------------------
294
+ # generate map
295
+ # -------------------------------------------
296
+ def generate_map(grid, filename):
297
+ data = grid.astype(np.float64)
298
+ data[data == 0] = np.nan
299
+
300
+ # Log-scale normalization to accentuate lower values
301
+ vmin = max(np.nanmin(data), 1.0) # clamp min to 1 for log scale
302
+ norm = LogNorm(vmin=vmin, vmax=np.nanmax(data))
303
+ mapped = cm.jet(norm(data)) # RGBA float array
304
+ mapped = (mapped * 255).astype(np.uint8)
305
+
306
+ # Make NaN pixels fully transparent
307
+ mask = np.isnan(data)
308
+ mapped[mask] = [0, 0, 0, 0]
309
+
310
+ # Save image
311
+ img = Image.fromarray(np.flipud(mapped), mode='RGBA')
312
+ img.save(filename)
313
+ print(f'Saved PNG {filename}: {img.size[0]}x{img.size[1]} pixels')
314
+
315
+ # -------------------------------------------
316
+ # AIO grid
317
+ # -------------------------------------------
318
+ def aoi_sql_grid(table, where_clause, filename):
319
+ query = f"""
320
+ SELECT
321
+ CAST(FLOOR((aoi_x + 180.0) / 0.25) AS INTEGER) AS grid_x,
322
+ CAST(FLOOR((aoi_y + 90.0) / 0.25) AS INTEGER) AS grid_y,
323
+ COUNT(*) AS point_count
324
+ FROM "recorder-database"."telemetry"
325
+ WHERE aoi_x BETWEEN -180.0 AND 180.0
326
+ AND aoi_y BETWEEN -90.0 AND 90.0
327
+ AND ({where_clause})
328
+ GROUP BY
329
+ FLOOR((aoi_x + 180.0) / 0.25),
330
+ FLOOR((aoi_y + 90.0) / 0.25)
331
+ ORDER BY
332
+ grid_x, grid_y;
333
+ """
334
+ rows = execute_query(query, f'AOI of {table}')
335
+ grid = np.zeros((720, 1440), dtype=np.uint32)
336
+ for row in rows:
337
+ x = int(row["grid_x"])
338
+ y = int(row["grid_y"])
339
+ if x == 1440: x = 1439
340
+ if y == 720: y = 719
341
+ if(x < 1440 and x > 0 and y < 720 and y > 0): # 0,0 is an unfortunate artifact of not being populated
342
+ grid[y, x] = row["point_count"]
343
+ else:
344
+ print(f"Not gridding {y},{x} => {row['point_count']}")
345
+ generate_map(grid, filename)
346
+
347
+ # -------------------------------------------
348
+ # grid usage
349
+ # -------------------------------------------
350
+ def load_geonames(path):
351
+ """Load GeoNames cities500.txt into a dict keyed by city name (lowercase).
352
+ Each entry is a list of (city_name, country_code, lat, lon, population)."""
353
+ print("Loading GeoNames database...")
354
+ cities = defaultdict(list)
355
+ with open(path, "r", encoding="utf-8") as f:
356
+ reader = csv.reader(f, delimiter="\t", quoting=csv.QUOTE_NONE)
357
+ for row in reader:
358
+ name = row[1]
359
+ asciiname = row[2]
360
+ lat = float(row[4])
361
+ lon = float(row[5])
362
+ country_code = row[8]
363
+ population = int(row[14]) if row[14] else 0
364
+ entry = (name, country_code, lat, lon, population)
365
+ cities[name.lower()].append(entry)
366
+ if asciiname.lower() != name.lower():
367
+ cities[asciiname.lower()].append(entry)
368
+ # Also index alternate names
369
+ for alt in row[3].split(","):
370
+ alt = alt.strip().lower()
371
+ if alt and alt not in cities:
372
+ cities[alt].append(entry)
373
+ print(f"Loaded {len(cities)} unique city name keys.")
374
+ return cities
375
+
376
+ def load_countrycodes(path):
377
+ """Country code lookup from country name"""
378
+ print("Loading CountryCode database...")
379
+ country_to_code = {}
380
+ with open(path, "r", encoding="utf-8") as f:
381
+ for line in f:
382
+ if line.startswith("#"):
383
+ continue
384
+ parts = line.strip().split("\t")
385
+ if len(parts) >= 5:
386
+ country_to_code[parts[4].lower()] = parts[0]
387
+ print(f"Loaded {len(country_to_code)} country codes.")
388
+ return country_to_code
389
+
390
+ def geocode_offline(place, geonames_db, countrycode_db):
391
+ """Lookup lat/lon from 'Country, City' string using local GeoNames database."""
392
+ parts = [p.strip() for p in place.split(",")]
393
+ city_name = parts[-1] if len(parts) > 1 else parts[0]
394
+ country_name = parts[0] if len(parts) > 1 else None
395
+ # If city is "None", use the most populous city in the country as a fallback
396
+ if city_name.lower() == "none":
397
+ if country_name:
398
+ country_code = countrycode_db.get(country_name.lower())
399
+ if country_code:
400
+ # Find the most populous city in that country
401
+ best = None
402
+ for candidates_list in geonames_db.values():
403
+ for c in candidates_list:
404
+ if c[1] == country_code:
405
+ if best is None or c[4] > best[4]:
406
+ best = c
407
+ if best:
408
+ return best[2], best[3]
409
+ return None
410
+ candidates = geonames_db.get(city_name.lower(), [])
411
+ if not candidates:
412
+ return None
413
+ # filter by country if provided
414
+ if country_name:
415
+ country_code = countrycode_db.get(country_name.lower())
416
+ if country_code:
417
+ filtered = [c for c in candidates if c[1] == country_code]
418
+ if filtered:
419
+ candidates = filtered
420
+ # pick the one with highest population
421
+ best = max(candidates, key=lambda c: c[4])
422
+ return best[2], best[3]
423
+
424
+ def usage_grid(locations, filename):
425
+ grid = np.zeros((720, 1440), dtype=np.uint32)
426
+ geonames_db = load_geonames(args.geonames)
427
+ countrycode_db = load_countrycodes(args.country_info)
428
+ # create grid of locations
429
+ for place, count in locations.items():
430
+ loc = geocode_offline(place, geonames_db, countrycode_db)
431
+ if loc is not None:
432
+ lat, lon = loc
433
+ else:
434
+ print(f"Could not geocode {place}")
435
+ continue
436
+ # Convert lat/lon to 0.25 degree grid indices
437
+ # Lat: -90 to 90 -> row 0 (south) to 719 (north)
438
+ # Lon: -180 to 180 -> col 0 (west) to 1439 (east)
439
+ row = int((lat + 90) / 0.25)
440
+ col = int((lon + 180) / 0.25)
441
+ row = min(max(row, 0), 719)
442
+ col = min(max(col, 0), 1439)
443
+ grid[row, col] = count
444
+ # expand grid points into boxes based on value partitions
445
+ expanded = np.zeros_like(grid, dtype=np.uint32)
446
+ ys, xs = np.where(grid > 0) # Find all non-zero cells
447
+ for y, x in zip(ys, xs):
448
+ val = grid[y, x]
449
+ # determine partition
450
+ partition = val // 50 + 1
451
+ partition = min(partition, 5)
452
+ radius = partition
453
+ # define box bounds, clipped to grid
454
+ y_min = max(y - radius, 0)
455
+ y_max = min(y + radius, 719)
456
+ x_min = max(x - radius, 0)
457
+ x_max = min(x + radius, 1439)
458
+ # add value to all cells in the box
459
+ expanded[y_min:y_max+1, x_min:x_max+1] += val
460
+ # create png
461
+ generate_map(expanded, filename)
462
+
463
+ # -------------------------------------------
464
+ # display stats
465
+ # -------------------------------------------
466
+ def display_stats(title, stats, sort_values=False):
467
+ print(f'\n===================\n{title}\n===================')
468
+ if sort_values:
469
+ stat_list = sorted(stats.items(), key=lambda item: item[1], reverse=True)
470
+ else:
471
+ stat_list = stats.items()
472
+ for count in stat_list:
473
+ print(f'{str(count[0]).ljust(MAX_LABEL_LEN)} {str(count[1]).rjust(MAX_VALUE_LEN)}')
474
+
475
+ # -------------------------------------------
476
+ # main
477
+ # -------------------------------------------
478
+ def main():
479
+
480
+ # build needed athena partitions to handle request
481
+ start_dt = datetime.fromisoformat(args.start)
482
+ end_dt = datetime.fromisoformat(args.end)
483
+ days = pd.date_range(start_dt.date(), end_dt.date(), freq='D').date.tolist()
484
+ expected = [f"year={d.year:04d}/month={d.month:02d}/day={d.day:02d}" for d in days]
485
+ ensure_partitions_for_range(TELEMETRY_TABLE, expected, 'telemetry', start_dt, end_dt)
486
+ ensure_partitions_for_range(ALERTS_TABLE, expected, 'alerts', start_dt, end_dt)
487
+
488
+ # query for usage statistics
489
+ telemetry_table = f'"{GLUE_DATABASE}".{TELEMETRY_TABLE}'
490
+ alerts_table = f'"{GLUE_DATABASE}".{ALERTS_TABLE}'
491
+ where_clause = build_where_clause(start_dt, end_dt, days)
492
+ time_stats = get_timespan(telemetry_table, where_clause)
493
+ unique_ip_counts = value_counts(telemetry_table, 'source_ip', where_clause)
494
+ source_location_counts = build_location_counts(unique_ip_counts)
495
+ client_counts = value_counts(telemetry_table, 'client', where_clause)
496
+ endpoint_counts = value_counts(telemetry_table, 'endpoint', where_clause)
497
+ telemetry_status_code_counts = value_counts(telemetry_table, 'code', where_clause)
498
+ alert_status_code_counts = value_counts(alerts_table, 'code', where_clause)
499
+ summary = {
500
+ 'Start': time_stats["start"].strftime("%Y-%m-%d %H:%M:%S"),
501
+ 'End': time_stats["end"].strftime("%Y-%m-%d %H:%M:%S"),
502
+ 'Duration': f"{time_stats['span'].days} days, {(time_stats['span'].total_seconds() / 3600) % 24:.2f} hours",
503
+ 'Unique IPs': len(unique_ip_counts),
504
+ 'Unique Locations': len(source_location_counts),
505
+ 'Total Requests': sum_counts(endpoint_counts),
506
+ 'Python Client Requests': sum_counts(client_counts, match_str="python"),
507
+ 'Web Client Requests': sum_counts(client_counts, match_str="web"),
508
+ 'Unknown Client Requests': sum_counts(client_counts, match_str="unknown"),
509
+ 'ICESat-2 Granules Processed': sum_counts(endpoint_counts, ICESAT2_ENDPOINTS),
510
+ 'ICESat-2 Proxied Requests': sum_counts(endpoint_counts, ICESAT2_PROXY_ENDPOINTS),
511
+ 'GEDI Granules Processed': sum_counts(endpoint_counts, GEDI_ENDPOINTS),
512
+ 'GEDI Proxied Requests': sum_counts(endpoint_counts, GEDI_PROXY_ENDPOINTS)
513
+ }
514
+
515
+ # grid requests
516
+ if args.grid:
517
+ usage_grid(source_location_counts, args.grid_usage)
518
+ aoi_sql_grid(telemetry_table, where_clause, args.grid_aoi)
519
+
520
+ # display usage statistics
521
+ display_stats('Source Locations', source_location_counts, True)
522
+ display_stats('Clients', client_counts, True)
523
+ display_stats('Endpoints', endpoint_counts, True)
524
+ display_stats('Request Codes', telemetry_status_code_counts, True)
525
+ display_stats('Alert Codes', alert_status_code_counts, True)
526
+ display_stats('Summary', summary, False)
527
+ display_stats('Globe', {
528
+ 'icesat2': sum_counts(endpoint_counts, ICESAT2_ENDPOINTS),
529
+ 'gedi': sum_counts(endpoint_counts, GEDI_ENDPOINTS),
530
+ }, False)
531
+
532
+ # running via direct invocation
533
+ if __name__ == "__main__": main()
File without changes
@@ -0,0 +1,106 @@
1
+ # {
2
+ # "submissions": {
3
+ # "<name>": {
4
+ # "run_url": <run url>,
5
+ # "job_id": <job id>,
6
+ # "status": {
7
+ # "SUBMITTED": <x>,
8
+ # "PENDING": <x>,
9
+ # "RUNNABLE": <x>,
10
+ # "STARTING": <x>,
11
+ # "RUNNING": <x>,
12
+ # "SUCCEEDED": <x>,
13
+ # "FAILED": <x>
14
+ # },
15
+ # "complete": <true|false>,
16
+ # "results": [ {result 1}, {result 2}, ... {result N} ]
17
+ # },
18
+ # ...
19
+ # }
20
+ # }
21
+
22
+ import json
23
+ import os
24
+ from enum import Enum
25
+
26
+ # ###############################
27
+ # JobState (AWS Batch)
28
+ # ###############################
29
+
30
+ class JobState(str, Enum):
31
+
32
+ SUBMITTED = "SUBMITTED"
33
+ PENDING = "PENDING"
34
+ RUNNABLE = "RUNNABLE"
35
+ STARTING = "STARTING"
36
+ RUNNING = "RUNNING"
37
+ SUCCEEDED = "SUCCEEDED"
38
+ FAILED = "FAILED"
39
+
40
+ def __str__(self):
41
+ return self.value
42
+
43
+ # ###############################
44
+ # JobStatus (SlideRule Runner)
45
+ # ###############################
46
+
47
+ class JobStatus(str, Enum):
48
+
49
+ PENDING = "pending" # run has not been processed yet
50
+ SUCCESS = "success" # run completed and produced output
51
+ FAILURE = "failure" # run completed and produced no output
52
+ UNSUPPORTED = "unsupported" # results of run incompatible with automatic parsing
53
+ ERROR = "error" # run errored out and did not complete
54
+
55
+ def __str__(self):
56
+ return self.value
57
+
58
+ # ###############################
59
+ # QueuePriority
60
+ # ###############################
61
+
62
+ class QueuePriority(str, Enum):
63
+
64
+ URGENT = "urgent"
65
+ DEFAULT = "default"
66
+ BACKGROUND = "background"
67
+
68
+ def __str__(self):
69
+ return self.value
70
+
71
+ # ###############################
72
+ # Database
73
+ # ###############################
74
+
75
+ class Database:
76
+
77
+ def __init__(self, filename):
78
+ self.filename = filename
79
+ try:
80
+ # read database
81
+ with open(filename, "r") as file:
82
+ self.database = json.load(file)
83
+ except FileNotFoundError:
84
+ # create database
85
+ os.makedirs(os.path.dirname(filename), exist_ok=True)
86
+ with open(filename, "w") as file:
87
+ self.database = {"submissions": {}}
88
+ json.dump(self.database, file)
89
+
90
+ @property
91
+ def submissions(self):
92
+ return self.database["submissions"]
93
+
94
+ # Write database out to file
95
+ def write(self, filename=None):
96
+ filename = filename or self.filename
97
+ # written via a temporary file so that an interrupt cannot truncate the database
98
+ tmp_filename = f"{filename}.tmp"
99
+ with open(tmp_filename, "w") as file:
100
+ json.dump(self.database, file, indent=2)
101
+ os.replace(tmp_filename, filename)
102
+
103
+ # Remove database (not recoverable)
104
+ def remove(self):
105
+ if os.path.exists(self.filename):
106
+ os.remove(self.filename)