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.
File without changes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: api-24sea
3
- Version: 2.2.1
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
@@ -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
- # fmt: off
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
- future_to_data = {
472
+ futures = [
374
473
  executor.submit(
375
474
  U.fetch_data_sync,
376
475
  f"{self.base_url}data/",
377
- _group_site_name(site),
378
- _get_group_locations(group),
379
- query.start_timestamp,
380
- query.end_timestamp,
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
- ): site
388
- for site, group in grouped_metrics
389
- }
390
- for future in concurrent.futures.as_completed(future_to_data):
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 site, group in grouped_metrics:
394
- data_frames.append(pd.DataFrame(U.fetch_data_sync(
395
- f"{self.base_url}data/", _group_site_name(site),
396
- _get_group_locations(group),
397
- query.start_timestamp, query.end_timestamp, query.headers,
398
- group, self._auth, timeout,
399
- force_cache_miss=query.force_cache_miss,
400
- method=query.method))
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
- if outer_join_on_timestamp:
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
- # Split tasks into chunks of 5 to avoid firing tens of requests together
1142
-
1143
- # fmt: off
1144
- tasks = [U.fetch_data_async(f"{self.base_url}data/", _group_site_name(site),
1145
- _get_group_locations(group),
1146
- query.start_timestamp, query.end_timestamp,
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
- # fmt: on
1160
- data_frames = []
1161
- data_frames = await U.gather_in_chunks(
1162
- tasks, chunk_size=chunk_size_dict["chunk_size"], timeout=timeout # type: ignore # pylint: disable=C0301 # noqa:E501
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
- if outer_join_on_timestamp:
1166
- for i, df in enumerate(data_frames):
1167
- if df.empty:
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
- data_frames[i] = _collapse_timestamp_rows(df)
1170
- data_ = pd.concat([data_] + data_frames, axis=1, join="outer")
1171
- else:
1172
- data_ = pd.concat([data_] + data_frames, ignore_index=True)
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(copy=False)[0] # type: ignore
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.select_dtypes(
2029
- include=["object"]
2030
- ).columns # noqa: E501 # pylint: disable=C0301
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
 
@@ -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
- logging.debug(f"Number of grouped metrics: {len(list(grouped_metrics))}")
580
- logging.debug(f"Grouped metrics: {list(grouped_metrics)}")
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
- return dt
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 grouped_metrics:
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
- if total_mb < 40:
632
- chunk_size = n_tasks
633
- elif total_mb < 80:
634
- chunk_size = max(1, n_tasks // 2)
635
- elif total_mb < 160:
636
- chunk_size = max(1, n_tasks // 4)
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(1, n_tasks // 8)
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: List[CoroutineType], chunk_size: int = 5, timeout: int = 3600
766
+ tasks: Sequence[Awaitable[Any]],
767
+ chunk_size: int = 5,
768
+ timeout: int = 3600,
650
769
  ) -> List:
651
770
  results = []
652
771
  chunk_results = []
@@ -19,7 +19,7 @@ Attributes:
19
19
  import re
20
20
  from typing import NamedTuple, Optional
21
21
 
22
- __version__: str = "2.2.1"
22
+ __version__: str = "2.3.0"
23
23
 
24
24
  _REGEX = "".join(
25
25
  [
@@ -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.13.0"
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 = [