api-24sea 2.2.1__tar.gz → 2.3.0__tar.gz
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.
- {api_24sea-2.2.1 → api_24sea-2.3.0}/LICENSE +0 -0
- {api_24sea-2.2.1 → api_24sea-2.3.0}/PKG-INFO +1 -1
- {api_24sea-2.2.1 → api_24sea-2.3.0}/README.md +0 -0
- {api_24sea-2.2.1 → api_24sea-2.3.0}/api_24sea/__init__.py +0 -0
- {api_24sea-2.2.1 → api_24sea-2.3.0}/api_24sea/abc.py +0 -0
- {api_24sea-2.2.1 → api_24sea-2.3.0}/api_24sea/datasignals/__init__.py +0 -0
- {api_24sea-2.2.1 → api_24sea-2.3.0}/api_24sea/datasignals/core.py +208 -62
- {api_24sea-2.2.1 → api_24sea-2.3.0}/api_24sea/datasignals/fatigue.py +0 -0
- {api_24sea-2.2.1 → api_24sea-2.3.0}/api_24sea/datasignals/schemas.py +0 -0
- {api_24sea-2.2.1 → api_24sea-2.3.0}/api_24sea/exceptions.py +0 -0
- {api_24sea-2.2.1 → api_24sea-2.3.0}/api_24sea/singleton.py +0 -0
- {api_24sea-2.2.1 → api_24sea-2.3.0}/api_24sea/utils.py +132 -13
- {api_24sea-2.2.1 → api_24sea-2.3.0}/api_24sea/version.py +1 -1
- {api_24sea-2.2.1 → api_24sea-2.3.0}/pyproject.toml +2 -2
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: api-24sea
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: Facilitate the users' interaction with the 24SEA API (https://api/24sea/eu) by providing pandas interfaces to the API endpoints.
|
|
5
5
|
Author-email: Pietro D'Antuono <pietro.dantuono@24sea.eu>
|
|
6
6
|
Maintainer-email: Pietro D'Antuono <pietro.dantuono@24sea.eu>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -57,13 +57,62 @@ def _collapse_timestamp_rows(
|
|
|
57
57
|
return collapsed.sort_index()
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
def merge_data_frames(
|
|
61
|
+
data_frames: List[pd.DataFrame],
|
|
62
|
+
outer_join_on_timestamp: bool,
|
|
63
|
+
) -> pd.DataFrame:
|
|
64
|
+
"""Merge split datasignals responses in deterministic query order.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
data_frames : list of pandas.DataFrame
|
|
69
|
+
Response frames from location and/or timestamp-window requests.
|
|
70
|
+
outer_join_on_timestamp : bool
|
|
71
|
+
Whether to return one row per timestamp without site and location
|
|
72
|
+
columns.
|
|
73
|
+
|
|
74
|
+
Returns
|
|
75
|
+
-------
|
|
76
|
+
pandas.DataFrame
|
|
77
|
+
Merged response ordered by timestamp, or by site, location, and
|
|
78
|
+
timestamp when location columns are retained.
|
|
79
|
+
"""
|
|
80
|
+
if not data_frames:
|
|
81
|
+
return pd.DataFrame()
|
|
82
|
+
|
|
83
|
+
merged = pd.concat(data_frames, ignore_index=True, sort=False)
|
|
84
|
+
if outer_join_on_timestamp:
|
|
85
|
+
return _collapse_timestamp_rows(merged)
|
|
86
|
+
|
|
87
|
+
sort_columns = [
|
|
88
|
+
column
|
|
89
|
+
for column in ["site", "location", "timestamp"]
|
|
90
|
+
if column in merged.columns
|
|
91
|
+
]
|
|
92
|
+
if sort_columns:
|
|
93
|
+
merged = merged.sort_values(sort_columns, kind="stable")
|
|
94
|
+
return merged.reset_index(drop=True)
|
|
95
|
+
|
|
96
|
+
|
|
60
97
|
class API(AuthABC):
|
|
61
98
|
"""Accessor for working with data signals coming from the 24SEA API."""
|
|
62
99
|
|
|
100
|
+
@property
|
|
101
|
+
def base_url(self) -> str:
|
|
102
|
+
"""Return the normalized datasignals API base URL."""
|
|
103
|
+
return self._base_url
|
|
104
|
+
|
|
105
|
+
@base_url.setter
|
|
106
|
+
def base_url(self, value: str) -> None:
|
|
107
|
+
"""Accept either the version root or datasignals application URL."""
|
|
108
|
+
normalized = value.rstrip("/")
|
|
109
|
+
if not normalized.endswith("/datasignals"):
|
|
110
|
+
normalized = f"{normalized}/datasignals"
|
|
111
|
+
self._base_url = f"{normalized}/"
|
|
112
|
+
|
|
63
113
|
def __init__(self):
|
|
64
114
|
super().__init__()
|
|
65
115
|
self._metrics_overview = self._permissions_overview
|
|
66
|
-
self.base_url: str = str(f"{self.base_url}datasignals/")
|
|
67
116
|
self._selected_metrics: Optional[pd.DataFrame] = None
|
|
68
117
|
|
|
69
118
|
@property
|
|
@@ -362,56 +411,107 @@ class API(AuthABC):
|
|
|
362
411
|
)
|
|
363
412
|
grouped_metrics = query.group_metrics_by_site(self._metrics_overview)
|
|
364
413
|
|
|
414
|
+
split_plan = U.estimate_chunk_size(
|
|
415
|
+
tasks=list(grouped_metrics),
|
|
416
|
+
start_timestamp=query.start_timestamp, # type: ignore[arg-type]
|
|
417
|
+
end_timestamp=query.end_timestamp, # type: ignore[arg-type]
|
|
418
|
+
grouped_metrics=grouped_metrics,
|
|
419
|
+
selected_metrics=self._selected_metrics,
|
|
420
|
+
)
|
|
421
|
+
time_windows = U.split_timestamp_range(
|
|
422
|
+
query.start_timestamp, # type: ignore[arg-type]
|
|
423
|
+
query.end_timestamp, # type: ignore[arg-type]
|
|
424
|
+
split_plan.get("time_splits", 1),
|
|
425
|
+
)
|
|
426
|
+
|
|
365
427
|
data_frames = []
|
|
366
428
|
import concurrent.futures
|
|
367
429
|
|
|
368
|
-
|
|
430
|
+
request_items: List[Dict[str, Any]] = []
|
|
431
|
+
for site, group in grouped_metrics:
|
|
432
|
+
site_name = _group_site_name(site)
|
|
433
|
+
group_locations = _get_group_locations(group)
|
|
434
|
+
location_batches = (
|
|
435
|
+
[[loc] for loc in group_locations]
|
|
436
|
+
if split_plan.get("split_by_location", False)
|
|
437
|
+
else [group_locations]
|
|
438
|
+
)
|
|
439
|
+
for location_batch in location_batches:
|
|
440
|
+
if not location_batch:
|
|
441
|
+
continue
|
|
442
|
+
group_for_request = group[
|
|
443
|
+
group["location"].isin(location_batch)
|
|
444
|
+
].copy()
|
|
445
|
+
if group_for_request.empty:
|
|
446
|
+
# Fallback to original group when no explicit location
|
|
447
|
+
# column is available for filtering.
|
|
448
|
+
group_for_request = group
|
|
449
|
+
for start_ts, end_ts in time_windows:
|
|
450
|
+
request_items.append(
|
|
451
|
+
{
|
|
452
|
+
"site": site_name,
|
|
453
|
+
"locations": location_batch,
|
|
454
|
+
"start_timestamp": start_ts,
|
|
455
|
+
"end_timestamp": end_ts,
|
|
456
|
+
"group": group_for_request,
|
|
457
|
+
}
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
if split_plan.get("split_by_location", False):
|
|
461
|
+
logging.info(
|
|
462
|
+
"\033[32;1m⏳ Splitting data query into "
|
|
463
|
+
f"{len(request_items)} request(s) "
|
|
464
|
+
f"[{split_plan.get('location_splits', 1)} location split(s), "
|
|
465
|
+
f"{split_plan.get('time_splits', 1)} time window(s)].\033[0m"
|
|
466
|
+
)
|
|
467
|
+
|
|
369
468
|
try:
|
|
370
469
|
with concurrent.futures.ThreadPoolExecutor(
|
|
371
470
|
max_workers=threads, thread_name_prefix="24SEA"
|
|
372
471
|
) as executor:
|
|
373
|
-
|
|
472
|
+
futures = [
|
|
374
473
|
executor.submit(
|
|
375
474
|
U.fetch_data_sync,
|
|
376
475
|
f"{self.base_url}data/",
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
476
|
+
item["site"],
|
|
477
|
+
item["locations"],
|
|
478
|
+
item["start_timestamp"],
|
|
479
|
+
item["end_timestamp"],
|
|
381
480
|
query.headers,
|
|
382
|
-
group,
|
|
481
|
+
item["group"],
|
|
383
482
|
self._auth,
|
|
384
483
|
timeout,
|
|
385
484
|
force_cache_miss=query.force_cache_miss,
|
|
386
485
|
method=query.method,
|
|
387
|
-
)
|
|
388
|
-
for
|
|
389
|
-
|
|
390
|
-
for future in
|
|
486
|
+
)
|
|
487
|
+
for item in request_items
|
|
488
|
+
]
|
|
489
|
+
for future in futures:
|
|
391
490
|
data_frames.append(pd.DataFrame(future.result()))
|
|
392
491
|
except RuntimeError:
|
|
393
|
-
for
|
|
394
|
-
data_frames.append(
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
492
|
+
for item in request_items:
|
|
493
|
+
data_frames.append(
|
|
494
|
+
pd.DataFrame(
|
|
495
|
+
U.fetch_data_sync(
|
|
496
|
+
f"{self.base_url}data/",
|
|
497
|
+
item["site"],
|
|
498
|
+
item["locations"],
|
|
499
|
+
item["start_timestamp"],
|
|
500
|
+
item["end_timestamp"],
|
|
501
|
+
query.headers,
|
|
502
|
+
item["group"],
|
|
503
|
+
self._auth,
|
|
504
|
+
timeout,
|
|
505
|
+
force_cache_miss=query.force_cache_miss,
|
|
506
|
+
method=query.method,
|
|
507
|
+
)
|
|
508
|
+
)
|
|
401
509
|
)
|
|
402
|
-
# fmt: on
|
|
403
510
|
# data_frames.append(pd.DataFrame(r_.json()))
|
|
404
511
|
|
|
405
512
|
# if outer_join_on_timestamp is True, lose the location and site columns
|
|
406
513
|
# and join on timestamp
|
|
407
|
-
|
|
408
|
-
for i, df in enumerate(data_frames):
|
|
409
|
-
if df.empty:
|
|
410
|
-
continue
|
|
411
|
-
data_frames[i] = _collapse_timestamp_rows(df)
|
|
412
|
-
data_ = pd.concat([data_] + data_frames, axis=1, join="outer")
|
|
413
|
-
else:
|
|
414
|
-
data_ = pd.concat([data_] + data_frames, ignore_index=True)
|
|
514
|
+
data_ = merge_data_frames(data_frames, outer_join_on_timestamp)
|
|
415
515
|
|
|
416
516
|
logging.info("\033[32;1m✔️ Data successfully retrieved.\033[0m")
|
|
417
517
|
# fmt: off
|
|
@@ -845,6 +945,7 @@ class API(AuthABC):
|
|
|
845
945
|
df = _collapse_timestamp_rows(df, index_column="bucket_start")
|
|
846
946
|
df.index.name = "timestamp"
|
|
847
947
|
df = U.parse_timestamp(df)
|
|
948
|
+
df = df.apply(pd.to_numeric, errors="coerce")
|
|
848
949
|
# Convert NaN to 0
|
|
849
950
|
df = df.fillna(0)
|
|
850
951
|
if as_dict:
|
|
@@ -1138,38 +1239,82 @@ class AsyncAPI(API):
|
|
|
1138
1239
|
self._selected_metrics = query.get_selected_metrics(metrics_overview)
|
|
1139
1240
|
grouped_metrics = query.group_metrics_by_site(metrics_overview)
|
|
1140
1241
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
query.headers, group, self._auth, timeout,
|
|
1148
|
-
max_retries,
|
|
1149
|
-
force_cache_miss=query.force_cache_miss,
|
|
1150
|
-
method=query.method)
|
|
1151
|
-
for site, group in grouped_metrics]
|
|
1152
|
-
chunk_size_dict = U.estimate_chunk_size(
|
|
1153
|
-
tasks,
|
|
1154
|
-
query.start_timestamp, # type: ignore
|
|
1155
|
-
query.end_timestamp, # type: ignore
|
|
1156
|
-
grouped_metrics,
|
|
1157
|
-
self._selected_metrics
|
|
1242
|
+
split_plan = U.estimate_chunk_size(
|
|
1243
|
+
tasks=list(grouped_metrics),
|
|
1244
|
+
start_timestamp=query.start_timestamp, # type: ignore[arg-type]
|
|
1245
|
+
end_timestamp=query.end_timestamp, # type: ignore[arg-type]
|
|
1246
|
+
grouped_metrics=grouped_metrics,
|
|
1247
|
+
selected_metrics=self._selected_metrics,
|
|
1158
1248
|
)
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1249
|
+
time_windows = U.split_timestamp_range(
|
|
1250
|
+
query.start_timestamp, # type: ignore[arg-type]
|
|
1251
|
+
query.end_timestamp, # type: ignore[arg-type]
|
|
1252
|
+
split_plan.get("time_splits", 1),
|
|
1163
1253
|
)
|
|
1164
1254
|
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1255
|
+
request_items: List[Dict[str, Any]] = []
|
|
1256
|
+
for site, group in grouped_metrics:
|
|
1257
|
+
site_name = _group_site_name(site)
|
|
1258
|
+
group_locations = _get_group_locations(group)
|
|
1259
|
+
location_batches = (
|
|
1260
|
+
[[loc] for loc in group_locations]
|
|
1261
|
+
if split_plan.get("split_by_location", False)
|
|
1262
|
+
else [group_locations]
|
|
1263
|
+
)
|
|
1264
|
+
for location_batch in location_batches:
|
|
1265
|
+
if not location_batch:
|
|
1168
1266
|
continue
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1267
|
+
group_for_request = group[
|
|
1268
|
+
group["location"].isin(location_batch)
|
|
1269
|
+
].copy()
|
|
1270
|
+
if group_for_request.empty:
|
|
1271
|
+
# Fallback to original group when no explicit location
|
|
1272
|
+
# column is available for filtering.
|
|
1273
|
+
group_for_request = group
|
|
1274
|
+
for start_ts, end_ts in time_windows:
|
|
1275
|
+
request_items.append(
|
|
1276
|
+
{
|
|
1277
|
+
"site": site_name,
|
|
1278
|
+
"locations": location_batch,
|
|
1279
|
+
"start_timestamp": start_ts,
|
|
1280
|
+
"end_timestamp": end_ts,
|
|
1281
|
+
"group": group_for_request,
|
|
1282
|
+
}
|
|
1283
|
+
)
|
|
1284
|
+
|
|
1285
|
+
if split_plan.get("split_by_location", False):
|
|
1286
|
+
logging.info(
|
|
1287
|
+
"\033[32;1m⏳ Splitting data query into "
|
|
1288
|
+
f"{len(request_items)} request(s) "
|
|
1289
|
+
f"[{split_plan.get('location_splits', 1)} location split(s), "
|
|
1290
|
+
f"{split_plan.get('time_splits', 1)} time window(s)].\033[0m"
|
|
1291
|
+
)
|
|
1292
|
+
|
|
1293
|
+
tasks = [
|
|
1294
|
+
U.fetch_data_async(
|
|
1295
|
+
f"{self.base_url}data/",
|
|
1296
|
+
item["site"],
|
|
1297
|
+
item["locations"],
|
|
1298
|
+
item["start_timestamp"],
|
|
1299
|
+
item["end_timestamp"],
|
|
1300
|
+
query.headers,
|
|
1301
|
+
item["group"],
|
|
1302
|
+
self._auth,
|
|
1303
|
+
timeout,
|
|
1304
|
+
max_retries,
|
|
1305
|
+
force_cache_miss=query.force_cache_miss,
|
|
1306
|
+
method=query.method,
|
|
1307
|
+
)
|
|
1308
|
+
for item in request_items
|
|
1309
|
+
]
|
|
1310
|
+
|
|
1311
|
+
chunk_size = split_plan["chunk_size"] or len(tasks)
|
|
1312
|
+
chunk_size = max(1, min(chunk_size, len(tasks))) if tasks else 1
|
|
1313
|
+
data_frames = await U.gather_in_chunks(
|
|
1314
|
+
tasks, chunk_size=chunk_size, timeout=timeout
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1317
|
+
data_ = merge_data_frames(data_frames, outer_join_on_timestamp)
|
|
1173
1318
|
|
|
1174
1319
|
if all(
|
|
1175
1320
|
getattr(r, "status_code", 200) == 200
|
|
@@ -1605,9 +1750,10 @@ class AsyncAPI(API):
|
|
|
1605
1750
|
logging.warning("\033[33;1mAvailability response missing "
|
|
1606
1751
|
"'bucket_start' column.\033[0m")
|
|
1607
1752
|
# fmt: on
|
|
1608
|
-
return df
|
|
1753
|
+
return df.apply(pd.to_numeric, errors="coerce")
|
|
1609
1754
|
df = _collapse_timestamp_rows(df, index_column="bucket_start")
|
|
1610
1755
|
df.index.name = "timestamp"
|
|
1756
|
+
df = df.apply(pd.to_numeric, errors="coerce")
|
|
1611
1757
|
# Convert NaN to 0
|
|
1612
1758
|
df = df.fillna(0)
|
|
1613
1759
|
if as_dict:
|
|
@@ -1762,7 +1908,7 @@ def to_category_value(
|
|
|
1762
1908
|
categorized["metric"]
|
|
1763
1909
|
.str.extract(r"(_TP_)|(_TW_)|(_MP_)")
|
|
1764
1910
|
.bfill(axis=1)
|
|
1765
|
-
.infer_objects(
|
|
1911
|
+
.infer_objects()[0] # type: ignore
|
|
1766
1912
|
.str.replace("_", "")
|
|
1767
1913
|
)
|
|
1768
1914
|
else:
|
|
@@ -2025,9 +2171,9 @@ def to_star_schema(
|
|
|
2025
2171
|
)
|
|
2026
2172
|
# Convert object columns to string in fact_pivot_metrics
|
|
2027
2173
|
if convert_object_columns_to_string:
|
|
2028
|
-
object_columns = fact_pivot_metrics.
|
|
2029
|
-
|
|
2030
|
-
|
|
2174
|
+
object_columns = fact_pivot_metrics.columns[
|
|
2175
|
+
fact_pivot_metrics.dtypes == "object"
|
|
2176
|
+
]
|
|
2031
2177
|
for col in object_columns:
|
|
2032
2178
|
fact_pivot_metrics = U.column_to_type(fact_pivot_metrics, col, str)
|
|
2033
2179
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -3,19 +3,21 @@
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import datetime
|
|
5
5
|
import logging
|
|
6
|
+
import math
|
|
6
7
|
import multiprocessing
|
|
7
8
|
import sys
|
|
8
9
|
import time
|
|
9
10
|
import warnings
|
|
10
11
|
from collections import defaultdict
|
|
11
|
-
from types import CoroutineType
|
|
12
12
|
from typing import (
|
|
13
13
|
Any,
|
|
14
|
+
Awaitable,
|
|
14
15
|
DefaultDict,
|
|
15
16
|
Dict,
|
|
16
17
|
Iterable,
|
|
17
18
|
List,
|
|
18
19
|
Optional,
|
|
20
|
+
Sequence,
|
|
19
21
|
Tuple,
|
|
20
22
|
Union,
|
|
21
23
|
)
|
|
@@ -576,10 +578,13 @@ def estimate_chunk_size(
|
|
|
576
578
|
logging.debug(f"Number of tasks: {len(tasks)}")
|
|
577
579
|
logging.debug(f"Start timestamp: {start_timestamp}")
|
|
578
580
|
logging.debug(f"End timestamp: {end_timestamp}")
|
|
579
|
-
|
|
580
|
-
logging.debug(f"
|
|
581
|
+
grouped_metrics_list = list(grouped_metrics)
|
|
582
|
+
logging.debug(f"Number of grouped metrics: {len(grouped_metrics_list)}")
|
|
583
|
+
logging.debug(f"Grouped metrics: {grouped_metrics_list}")
|
|
581
584
|
logging.debug(f"Selected metrics:\n{selected_metrics}")
|
|
582
585
|
|
|
586
|
+
max_request_mb = 100.0
|
|
587
|
+
|
|
583
588
|
def parse_dt(dt):
|
|
584
589
|
if isinstance(dt, str):
|
|
585
590
|
try:
|
|
@@ -588,7 +593,9 @@ def estimate_chunk_size(
|
|
|
588
593
|
_dt = parse_shorthand_datetime(dt)
|
|
589
594
|
if _dt is not None:
|
|
590
595
|
dt = _dt.replace(tzinfo=None)
|
|
591
|
-
|
|
596
|
+
return pd.to_datetime(dt)
|
|
597
|
+
return pd.to_datetime(dt)
|
|
598
|
+
return pd.to_datetime(dt)
|
|
592
599
|
|
|
593
600
|
start_dt = parse_dt(start_timestamp)
|
|
594
601
|
end_dt = parse_dt(end_timestamp)
|
|
@@ -610,43 +617,155 @@ def estimate_chunk_size(
|
|
|
610
617
|
bytes_per_metric[metric] *= 7
|
|
611
618
|
if "_flt_" in metric.lower():
|
|
612
619
|
bytes_per_metric[metric] *= 35
|
|
620
|
+
total_locations = 0
|
|
613
621
|
total_bytes = 0
|
|
614
|
-
for _, group in
|
|
622
|
+
for _, group in grouped_metrics_list:
|
|
615
623
|
if isinstance(group, pd.DataFrame):
|
|
616
624
|
group_met = normalize_group_values(group[target].tolist())
|
|
625
|
+
if "location" in group.columns:
|
|
626
|
+
location_count = max(1, group["location"].nunique())
|
|
627
|
+
else:
|
|
628
|
+
location_count = 1
|
|
617
629
|
else:
|
|
618
630
|
group_met = [group[target]] if hasattr(group, target) else []
|
|
631
|
+
location_count = 1
|
|
619
632
|
# If group_met contains only "all", use all selected metrics
|
|
620
633
|
if group_met == ["all"] and selected_metrics is not None:
|
|
621
634
|
group_met = selected_metrics[target].tolist()
|
|
622
635
|
group_bytes = sum(bytes_per_metric.get(m, 8) for m in group_met)
|
|
623
636
|
total_bytes += n_points * group_bytes
|
|
637
|
+
total_locations += location_count
|
|
638
|
+
|
|
639
|
+
if grouped_metrics_list and total_locations == 0:
|
|
640
|
+
total_locations = 1
|
|
641
|
+
|
|
642
|
+
# If location detail is unavailable in grouped metrics, keep a conservative
|
|
643
|
+
# fallback so tests and callers without location data remain stable.
|
|
644
|
+
if not grouped_metrics_list:
|
|
645
|
+
total_locations = max(n_tasks, 1)
|
|
624
646
|
|
|
625
647
|
# Check for negative or zero values (sanity check)
|
|
626
648
|
if total_bytes <= 0 or n_points <= 0:
|
|
627
649
|
total_bytes = 0
|
|
628
650
|
total_mb = total_bytes / (1024 * 1024)
|
|
629
651
|
|
|
652
|
+
per_location_mb = (
|
|
653
|
+
total_mb / max(total_locations, 1) if total_locations > 0 else total_mb
|
|
654
|
+
)
|
|
655
|
+
time_splits = (
|
|
656
|
+
max(1, math.ceil(per_location_mb / max_request_mb))
|
|
657
|
+
if per_location_mb > 0
|
|
658
|
+
else 1
|
|
659
|
+
)
|
|
660
|
+
needs_split = total_mb > max_request_mb
|
|
661
|
+
planned_tasks = max(n_tasks, max(1, total_locations) * max(1, time_splits))
|
|
662
|
+
|
|
630
663
|
# Determine chunk_size
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
664
|
+
request_size = (10, 20, 30, 40, 50, 60, 70, 80, 90, 100) # in MB
|
|
665
|
+
threshold = request_size[-1]
|
|
666
|
+
for threshold in request_size:
|
|
667
|
+
if total_mb < threshold:
|
|
668
|
+
break
|
|
669
|
+
|
|
670
|
+
if n_tasks == 0:
|
|
671
|
+
chunk_size = 0
|
|
637
672
|
else:
|
|
638
|
-
chunk_size = max(
|
|
673
|
+
chunk_size = max(
|
|
674
|
+
1,
|
|
675
|
+
planned_tasks // (request_size.index(threshold) + 1),
|
|
676
|
+
)
|
|
639
677
|
|
|
640
678
|
logging.info(f"Estimated request size: {total_mb:.2f} MB")
|
|
641
679
|
return {
|
|
642
680
|
"total_mb": total_mb,
|
|
643
681
|
"n_tasks": n_tasks,
|
|
644
682
|
"chunk_size": chunk_size,
|
|
683
|
+
"split_by_location": needs_split,
|
|
684
|
+
"location_splits": max(total_locations, 1),
|
|
685
|
+
"time_splits": time_splits,
|
|
686
|
+
"planned_tasks": planned_tasks,
|
|
687
|
+
"max_request_mb": max_request_mb,
|
|
645
688
|
}
|
|
646
689
|
|
|
647
690
|
|
|
691
|
+
def split_timestamp_range(
|
|
692
|
+
start_timestamp: Union[str, datetime.datetime],
|
|
693
|
+
end_timestamp: Union[str, datetime.datetime],
|
|
694
|
+
n_splits: int,
|
|
695
|
+
) -> List[Tuple[str, str]]:
|
|
696
|
+
"""Split a timestamp range into contiguous half-open windows.
|
|
697
|
+
|
|
698
|
+
The returned windows are suitable for repeated API calls that should be
|
|
699
|
+
merged back into one DataFrame without gaps or duplicate boundary
|
|
700
|
+
timestamps.
|
|
701
|
+
|
|
702
|
+
Parameters
|
|
703
|
+
----------
|
|
704
|
+
start_timestamp : str or datetime.datetime
|
|
705
|
+
Inclusive start of the full query window.
|
|
706
|
+
end_timestamp : str or datetime.datetime
|
|
707
|
+
Exclusive end of the full query window.
|
|
708
|
+
n_splits : int
|
|
709
|
+
Requested number of windows.
|
|
710
|
+
|
|
711
|
+
Returns
|
|
712
|
+
-------
|
|
713
|
+
list of tuple of str
|
|
714
|
+
Ordered ``(start, end)`` timestamp pairs. Each window end is the next
|
|
715
|
+
window start because the datasignals SQL queries use half-open ranges.
|
|
716
|
+
"""
|
|
717
|
+
|
|
718
|
+
def parse_timestamp(
|
|
719
|
+
dt: Union[str, datetime.datetime],
|
|
720
|
+
) -> pd.Timestamp:
|
|
721
|
+
if isinstance(dt, str):
|
|
722
|
+
try:
|
|
723
|
+
return pd.to_datetime(dt)
|
|
724
|
+
except pd._libs.tslibs.parsing.DateParseError: # type: ignore
|
|
725
|
+
_dt = parse_shorthand_datetime(dt)
|
|
726
|
+
if _dt is not None:
|
|
727
|
+
return pd.to_datetime(_dt.replace(tzinfo=None))
|
|
728
|
+
return pd.to_datetime(dt)
|
|
729
|
+
return pd.to_datetime(dt)
|
|
730
|
+
|
|
731
|
+
start_dt = parse_timestamp(start_timestamp)
|
|
732
|
+
end_dt = parse_timestamp(end_timestamp)
|
|
733
|
+
|
|
734
|
+
def format_timestamp(ts: pd.Timestamp) -> str:
|
|
735
|
+
if ts.tzinfo is not None:
|
|
736
|
+
return str(ts.tz_convert("UTC").strftime("%Y-%m-%dT%H:%M:%SZ"))
|
|
737
|
+
return str(ts.strftime("%Y-%m-%dT%H:%M:%S"))
|
|
738
|
+
|
|
739
|
+
if n_splits <= 1 or end_dt <= start_dt:
|
|
740
|
+
return [(format_timestamp(start_dt), format_timestamp(end_dt))]
|
|
741
|
+
|
|
742
|
+
total_seconds = int((end_dt - start_dt).total_seconds())
|
|
743
|
+
if total_seconds <= 0:
|
|
744
|
+
return [(format_timestamp(start_dt), format_timestamp(end_dt))]
|
|
745
|
+
|
|
746
|
+
effective_splits = min(n_splits, total_seconds)
|
|
747
|
+
windows: List[Tuple[str, str]] = []
|
|
748
|
+
for idx in range(effective_splits):
|
|
749
|
+
start_seconds = (total_seconds * idx) // effective_splits
|
|
750
|
+
end_seconds = (total_seconds * (idx + 1)) // effective_splits
|
|
751
|
+
window_start = start_dt + pd.Timedelta(seconds=start_seconds)
|
|
752
|
+
window_end = start_dt + pd.Timedelta(seconds=end_seconds)
|
|
753
|
+
windows.append(
|
|
754
|
+
(
|
|
755
|
+
format_timestamp(window_start),
|
|
756
|
+
format_timestamp(window_end),
|
|
757
|
+
)
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
last_start, _ = windows[-1]
|
|
761
|
+
windows[-1] = (last_start, format_timestamp(end_dt))
|
|
762
|
+
return windows
|
|
763
|
+
|
|
764
|
+
|
|
648
765
|
async def gather_in_chunks(
|
|
649
|
-
tasks:
|
|
766
|
+
tasks: Sequence[Awaitable[Any]],
|
|
767
|
+
chunk_size: int = 5,
|
|
768
|
+
timeout: int = 3600,
|
|
650
769
|
) -> List:
|
|
651
770
|
results = []
|
|
652
771
|
chunk_results = []
|
|
@@ -240,12 +240,12 @@ norecursedirs = [".bzr", ".direnv", ".eggs", ".git", ".git-rewrite", ".hg",
|
|
|
240
240
|
"logs"]
|
|
241
241
|
minversion = "6.0"
|
|
242
242
|
addopts = "-ra -q"
|
|
243
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
243
244
|
testpaths = [
|
|
244
245
|
"test",
|
|
245
246
|
"tests",
|
|
246
247
|
]
|
|
247
248
|
filterwarnings = [
|
|
248
|
-
'ignore:configuration option "asyncio_default_fixture_loop_scope" is unset',
|
|
249
249
|
'ignore:was never awaited',
|
|
250
250
|
]
|
|
251
251
|
|
|
@@ -256,7 +256,7 @@ filterwarnings = [
|
|
|
256
256
|
# directories from being committed.
|
|
257
257
|
[tool.commitizen]
|
|
258
258
|
name = "cz_conventional_commits"
|
|
259
|
-
version = "3.
|
|
259
|
+
version = "2.3.0"
|
|
260
260
|
bump_message = "bump(version): {current_version} → {new_version}"
|
|
261
261
|
update_changelog_on_bump = true
|
|
262
262
|
style = [
|