bmtool 0.8.2__tar.gz → 0.8.3__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.
Files changed (45) hide show
  1. {bmtool-0.8.2 → bmtool-0.8.3}/PKG-INFO +1 -1
  2. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/analysis/netcon_reports.py +17 -11
  3. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/connectors.py +238 -0
  4. bmtool-0.8.3/bmtool/stimulus/__init__.py +2 -0
  5. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/stimulus/assemblies.py +1 -0
  6. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/stimulus/core.py +6 -3
  7. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/stimulus/generators.py +5 -5
  8. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/synapses.py +1 -1
  9. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool.egg-info/PKG-INFO +1 -1
  10. {bmtool-0.8.2 → bmtool-0.8.3}/setup.py +1 -1
  11. bmtool-0.8.2/bmtool/stimulus/__init__.py +0 -3
  12. {bmtool-0.8.2 → bmtool-0.8.3}/LICENSE +0 -0
  13. {bmtool-0.8.2 → bmtool-0.8.3}/README.md +0 -0
  14. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/SLURM.py +0 -0
  15. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/__init__.py +0 -0
  16. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/__main__.py +0 -0
  17. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/analysis/__init__.py +0 -0
  18. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/analysis/entrainment.py +0 -0
  19. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/analysis/lfp.py +0 -0
  20. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/analysis/spikes.py +0 -0
  21. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/bmplot/__init__.py +0 -0
  22. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/bmplot/connections.py +0 -0
  23. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/bmplot/entrainment.py +0 -0
  24. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/bmplot/lfp.py +0 -0
  25. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/bmplot/netcon_reports.py +0 -0
  26. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/bmplot/spikes.py +0 -0
  27. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/debug/__init__.py +0 -0
  28. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/debug/commands.py +0 -0
  29. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/debug/debug.py +0 -0
  30. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/graphs.py +0 -0
  31. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/manage.py +0 -0
  32. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/plot_commands.py +0 -0
  33. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/singlecell.py +0 -0
  34. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/util/__init__.py +0 -0
  35. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/util/commands.py +0 -0
  36. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/util/neuron/__init__.py +0 -0
  37. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/util/neuron/celltuner.py +0 -0
  38. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool/util/util.py +1 -1
  39. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool.egg-info/SOURCES.txt +0 -0
  40. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool.egg-info/dependency_links.txt +0 -0
  41. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool.egg-info/entry_points.txt +0 -0
  42. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool.egg-info/requires.txt +0 -0
  43. {bmtool-0.8.2 → bmtool-0.8.3}/bmtool.egg-info/top_level.txt +0 -0
  44. {bmtool-0.8.2 → bmtool-0.8.3}/pyproject.toml +0 -0
  45. {bmtool-0.8.2 → bmtool-0.8.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bmtool
3
- Version: 0.8.2
3
+ Version: 0.8.3
4
4
  Summary: BMTool
5
5
  Home-page: https://github.com/cyneuro/bmtool
6
6
  Download-URL:
@@ -1,7 +1,8 @@
1
+ from typing import Any, Dict, List, Union
2
+
1
3
  import h5py
2
4
  import numpy as np
3
5
  import xarray as xr
4
- from typing import Union, List, Dict, Any
5
6
 
6
7
  from ..util.util import load_nodes_from_config
7
8
 
@@ -26,10 +27,10 @@ def load_synapse_report(
26
27
  Edge name in format 'source_to_target' (e.g., 'thalamic_tone_to_LA')
27
28
  This determines which source and target networks to load for population mapping
28
29
  source_groupby : str or List[str]
29
- Node property column name(s) to use for labeling source synapses.
30
+ Node property column name(s) to load as coordinates for filtering.
30
31
  Examples: 'pop_name', ['pop_name', 'model_type']
31
32
  target_groupby : str or List[str]
32
- Node property column name(s) to use for labeling target synapses.
33
+ Node property column name(s) to load as coordinates for filtering.
33
34
  Examples: 'pop_name', ['pop_name', 'model_type']
34
35
 
35
36
  Returns:
@@ -38,8 +39,8 @@ def load_synapse_report(
38
39
  An xarray containing the synapse report data with proper population labeling.
39
40
  For each column in source_groupby/target_groupby, separate coordinates are created:
40
41
  'source_{column}', 'target_{column}', etc.
41
- A 'connection_label' coordinate is also created with pipe-delimited values
42
- (e.g., 'Pyr|biophys->PV|biophys').
42
+ The 'connection_label' coordinate uses only pop_name for simple labeling
43
+ (e.g., 'Pyr->PV'), while all groupby columns are available as coordinates for filtering.
43
44
 
44
45
  Examples:
45
46
  ---------
@@ -52,7 +53,7 @@ def load_synapse_report(
52
53
  target_groupby='pop_name'
53
54
  )
54
55
 
55
- # Group by multiple columns:
56
+ # Group by multiple columns for filtering (connection_label still uses only pop_name):
56
57
  ds = load_synapse_report(
57
58
  h5_file_path='output/synapse_report.h5',
58
59
  config_path='simulation_config.json',
@@ -61,7 +62,8 @@ def load_synapse_report(
61
62
  target_groupby=['pop_name', 'model_type']
62
63
  )
63
64
  # Returns dataset with coordinates:
64
- # source_pop_name, source_model_type, target_pop_name, target_model_type, connection_label
65
+ # source_pop_name, source_model_type, target_pop_name, target_model_type
66
+ # connection_label (based on pop_name only)
65
67
  """
66
68
  # Normalize groupby parameters to lists
67
69
  if isinstance(source_groupby, str):
@@ -194,10 +196,14 @@ def load_synapse_report(
194
196
  target_values[col].append(val)
195
197
  trg_label_parts.append(str(val))
196
198
 
197
- # Create connection label with pipe-delimited format
198
- src_label = "|".join(src_label_parts)
199
- trg_label = "|".join(trg_label_parts)
200
- connection_labels.append(f"{src_label}->{trg_label}")
199
+ # Create connection label using only pop_name (not all groupby columns)
200
+ if src_id == -1:
201
+ src_pop = src_external_values.get('pop_name', 'unknown')
202
+ else:
203
+ src_pop = source_mappings['pop_name'].get(src_id, f"unknown_{src_id}")
204
+
205
+ trg_pop = target_mappings['pop_name'].get(trg_id, f"unknown_{trg_id}")
206
+ connection_labels.append(f"{src_pop}->{trg_pop}")
201
207
 
202
208
  # Create coordinates dictionary dynamically based on groupby columns
203
209
  coords = {
@@ -1447,6 +1447,244 @@ class UnidirectionConnector(AbstractConnector):
1447
1447
  df.to_csv(self.report_name, mode="w", header=True, index=False)
1448
1448
 
1449
1449
 
1450
+ class FileBasedConnector(UnidirectionConnector):
1451
+ """
1452
+ Connector that reads source and target node IDs from a CSV file and creates
1453
+ connections based on exact matches of node ID pairs.
1454
+
1455
+ Overview:
1456
+ ---------
1457
+ FileBasedConnector enables you to specify connections between two populations
1458
+ (source and target) by reading node ID pairs from a CSV file. During network
1459
+ building, the connector iterates through all possible source-target pairs and
1460
+ creates connections wherever a match is found in the loaded CSV file. This is
1461
+ useful when you want to replicate connection patterns from an existing network
1462
+ or apply a pre-computed connectivity matrix.
1463
+
1464
+ How it Works:
1465
+ -------------
1466
+ 1. The connector loads node ID pairs from a CSV file during initialization.
1467
+ 2. When setup_nodes() is called, it receives the source and target NodePool
1468
+ objects and creates a mapping from absolute node IDs to relative indices
1469
+ (0 to N-1 within each population).
1470
+ 3. During network.build(), the connector's make_connection() method is called
1471
+ for each possible source-target pair. It checks whether the pair exists in
1472
+ the loaded connections set and returns the number of synapses if a match
1473
+ is found, or 0 otherwise.
1474
+ 4. The connector automatically detects whether node IDs in the CSV are
1475
+ "relative" (0 to N-1, numbered within each population) or "absolute"
1476
+ (global node IDs). This detection happens by checking if any CSV ID is
1477
+ outside the valid range of absolute IDs in the target network.
1478
+
1479
+ CSV File Format:
1480
+ ----------------
1481
+ The CSV file must contain exactly two columns with these exact names:
1482
+ - 'source_node_id': The ID of the source node
1483
+ - 'target_node_id': The ID of the target node
1484
+
1485
+ Each row represents a single connection to be created. The file should have
1486
+ no header row requirement (pandas reads the column names from the first row).
1487
+
1488
+ Example CSV format:
1489
+ source_node_id,target_node_id
1490
+ 0,5
1491
+ 0,12
1492
+ 1,8
1493
+ 2,15
1494
+ ...
1495
+
1496
+ Node ID Specifications:
1497
+ ----------------------
1498
+ Node IDs in the CSV can be specified in two ways:
1499
+
1500
+ 1. RELATIVE IDs (Recommended when replaying from a different network):
1501
+ - Node IDs number from 0 to N-1 within each population.
1502
+ - Example: For a PV population with 85 nodes, use IDs 0-84.
1503
+ For an ET population with 425 nodes, use IDs 0-424.
1504
+ - Relative IDs are robust to network offset changes (e.g., if the target
1505
+ ET population starts at node 65 instead of 0, connections still work).
1506
+ - Use relative IDs when extracting connections from an existing network
1507
+ and applying them to a newly built network with different global offsets.
1508
+
1509
+ To generate relative IDs from an existing network:
1510
+ bio_nodes['relative_id'] = bio_nodes.groupby('pop_name').cumcount()
1511
+ pv_to_et_df[['source_relative_id', 'target_relative_id']].rename(
1512
+ columns={'source_relative_id': 'source_node_id',
1513
+ 'target_relative_id': 'target_node_id'}
1514
+ ).to_csv('connections.csv', index=False)
1515
+
1516
+ 2. ABSOLUTE IDs (Global node IDs from the current network):
1517
+ - Node IDs are the global node indices used by the BMTK network.
1518
+ - Example: If PV nodes are 0-84 and ET nodes are 65-489 in the network,
1519
+ use those exact values in the CSV.
1520
+ - Absolute IDs only work if the target network has the exact same node
1521
+ numbering scheme as when the CSV was generated.
1522
+ - Use absolute IDs when applying connections within the same network or
1523
+ when network structure is fixed.
1524
+
1525
+ ID Format Specification:
1526
+ -----------------------
1527
+ You must explicitly specify which ID format the CSV uses via the use_relative_ids
1528
+ parameter when creating the connector:
1529
+ - use_relative_ids=True: Node IDs in the CSV are relative (0 to N-1 within each
1530
+ population). The connector maps them to absolute IDs using the target network.
1531
+ - use_relative_ids=False: Node IDs in the CSV are absolute/global IDs. The
1532
+ connector uses them directly without mapping.
1533
+ - The choice is printed when verbose=True for confirmation.
1534
+
1535
+ Usage Example:
1536
+ ---------------
1537
+ # Load connections from CSV
1538
+ conn = FileBasedConnector('my_connections.csv', n_syn=1)
1539
+
1540
+ # Set up the connector with source and target populations
1541
+ conn.setup_nodes(
1542
+ source=network.nodes(pop_name='PV'),
1543
+ target=network.nodes(pop_name='ET')
1544
+ )
1545
+
1546
+ # Add edges to the network
1547
+ network.add_edges(
1548
+ **conn.edge_params(),
1549
+ dynamics_params='synapse_params.json',
1550
+ model_template='Exp2Syn'
1551
+ )
1552
+
1553
+ # Build the network
1554
+ network.build()
1555
+
1556
+ Parameters:
1557
+ -----------
1558
+ filename : str
1559
+ Path to the CSV file containing connections. The CSV must have exactly
1560
+ two columns named 'source_node_id' and 'target_node_id'.
1561
+
1562
+ n_syn : int or callable, optional
1563
+ Number of synapses for each connection found in the file. Can be:
1564
+ - A constant integer (default: 1)
1565
+ - A callable function that takes (source_node, target_node) and returns
1566
+ the number of synapses (useful for distance-dependent synapses)
1567
+
1568
+ verbose : bool, optional
1569
+ Whether to print detailed information about the connector's operation,
1570
+ including ID detection results and connection statistics. Default: True.
1571
+
1572
+ save_report : bool, optional
1573
+ Whether to save a connection report to a file. Default: False.
1574
+
1575
+ report_name : str, optional
1576
+ Name of the report file. Only used if save_report=True.
1577
+ Default: 'conn.csv'
1578
+
1579
+ use_relative_ids : bool, optional
1580
+ Whether the CSV file uses relative node IDs (0 to N-1 within each population)
1581
+ or absolute/global node IDs. Default: True (use relative IDs).
1582
+ - True: CSV IDs will be mapped from relative (0-based per population) to
1583
+ absolute node IDs using the source and target node pools.
1584
+ - False: CSV IDs are used as absolute/global node IDs without mapping.
1585
+
1586
+ Notes:
1587
+ ------
1588
+ - Connections are stored as a set of (source_id, target_id) tuples for O(1)
1589
+ lookup during network building.
1590
+ - The total number of possible connections checked during building is
1591
+ len(source_nodes) * len(target_nodes).
1592
+ - For large networks and large CSV files, this can be time-consuming.
1593
+ Consider using vectorized operations or pre-filtering if performance
1594
+ is a concern.
1595
+ - Each row in the CSV should represent a single connection. If you need
1596
+ multiple synapses between the same pair or distance-dependent synapses,
1597
+ use the n_syn parameter.
1598
+ """
1599
+
1600
+ def __init__(
1601
+ self, filename, n_syn=1, verbose=True, save_report=False, report_name=None,
1602
+ use_relative_ids=True
1603
+ ):
1604
+ super().__init__(
1605
+ p=1.0,
1606
+ n_syn=n_syn,
1607
+ verbose=verbose,
1608
+ save_report=save_report,
1609
+ report_name=report_name,
1610
+ )
1611
+ self.filename = filename
1612
+ self.use_relative_ids = use_relative_ids
1613
+ self.connections = set()
1614
+ self._load_connections()
1615
+
1616
+ def _load_connections(self):
1617
+ """Load source and target node IDs from the CSV file"""
1618
+ import pandas as pd
1619
+
1620
+ df = pd.read_csv(self.filename)
1621
+ if (
1622
+ "source_node_id" not in df.columns
1623
+ or "target_node_id" not in df.columns
1624
+ ):
1625
+ raise ValueError(
1626
+ f"CSV file {self.filename} must contain 'source_node_id' and 'target_node_id' columns"
1627
+ )
1628
+
1629
+ for _, row in df.iterrows():
1630
+ self.connections.add(
1631
+ (int(row["source_node_id"]), int(row["target_node_id"]))
1632
+ )
1633
+
1634
+ def make_connection(self, source, target, *args, **kwargs):
1635
+ """Assign number of synapses based on the loaded connections"""
1636
+ # Initialize in the first iteration
1637
+ if self.iter_count == 0:
1638
+ self.initialize()
1639
+ if self.verbose:
1640
+ src_str, trg_str = self.get_nodes_info()
1641
+ print(
1642
+ f"\nStart building file-based connection from {self.filename}"
1643
+ f"\n from {src_str}\n to {trg_str}",
1644
+ flush=True,
1645
+ )
1646
+
1647
+ # Build mapping from absolute node_id to relative index if using relative IDs
1648
+ if self.use_relative_ids:
1649
+ self.source_id_map = {node.node_id: i for i, node in enumerate(self.source)}
1650
+ self.target_id_map = {node.node_id: i for i, node in enumerate(self.target)}
1651
+
1652
+ if self.verbose:
1653
+ id_type = 'relative' if self.use_relative_ids else 'absolute/global'
1654
+ print(f" Using {id_type} node IDs from {self.filename}", flush=True)
1655
+
1656
+ sid, tid = source.node_id, target.node_id
1657
+
1658
+ match = False
1659
+ if self.use_relative_ids:
1660
+ rel_sid = self.source_id_map.get(sid)
1661
+ rel_tid = self.target_id_map.get(tid)
1662
+ if (rel_sid, rel_tid) in self.connections:
1663
+ match = True
1664
+ else:
1665
+ if (sid, tid) in self.connections:
1666
+ match = True
1667
+
1668
+ if match:
1669
+ nsyns = self.n_syn(source, target)
1670
+ self.n_conn += 1
1671
+ else:
1672
+ nsyns = 0
1673
+
1674
+ self.iter_count += 1
1675
+ self.n_poss += 1
1676
+
1677
+ # Detect end of iteration
1678
+ if self.iter_count == self.n_pair:
1679
+ if self.verbose:
1680
+ self.connection_number_info()
1681
+ self.timer.report("Done! \nTime for building connections")
1682
+ if self.save_report:
1683
+ self.save_connection_report()
1684
+
1685
+ return nsyns
1686
+
1687
+
1450
1688
  class GapJunction(UnidirectionConnector):
1451
1689
  """
1452
1690
  Object for buiilding gap junction connections in bmtk network model with
@@ -0,0 +1,2 @@
1
+ from . import assemblies, generators
2
+ from .core import StimulusBuilder
@@ -1,5 +1,6 @@
1
1
  import numpy as np
2
2
  import pandas as pd
3
+
3
4
  from bmtool.util.util import num_prop
4
5
 
5
6
 
@@ -1,9 +1,12 @@
1
1
  import os
2
+
2
3
  import numpy as np
3
- import pandas as pd
4
- from bmtool.util import util
5
4
  from bmtk.utils.reports.spike_trains import PoissonSpikeGenerator
6
- from . import generators, assemblies
5
+
6
+ from bmtool.util import util
7
+
8
+ from . import assemblies, generators
9
+
7
10
 
8
11
  class StimulusBuilder:
9
12
  """Class to manage and generate stimuli for BMTK networks.
@@ -74,7 +74,7 @@ def get_fr_short(n_assemblies, firing_rate=(0., 0., 0.),
74
74
  n_rounds_int = int(np.ceil(n_rounds))
75
75
 
76
76
  if verbose:
77
- print(f"\nCycle information:")
77
+ print("\nCycle information:")
78
78
  print(f"Time per cycle: {t_cycle}")
79
79
  print(f"Number of cycles: {n_cycle}")
80
80
  print(f"Bursts per cycle: {n_bursts_per_cycle}")
@@ -218,7 +218,7 @@ def get_fr_long(n_assemblies, firing_rate=(0., 0., 0.),
218
218
  n_selected = len(assembly_index)
219
219
 
220
220
  if verbose:
221
- print(f"\nCycle information:")
221
+ print("\nCycle information:")
222
222
  print(f"Time per cycle: {t_cycle}")
223
223
  print(f"Number of cycles: {n_cycle}")
224
224
 
@@ -367,7 +367,7 @@ def get_fr_ramp(n_assemblies, firing_rate=(0., 0., 0., 0.),
367
367
  n_selected = len(assembly_index)
368
368
 
369
369
  if verbose:
370
- print(f"\nCycle information:")
370
+ print("\nCycle information:")
371
371
  print(f"Time per cycle: {t_cycle}")
372
372
  print(f"Number of cycles: {n_cycle}")
373
373
  print(f"Ramp timing: {ramp_on_time} to {ramp_off_time} within on_time of {on_time}")
@@ -530,7 +530,7 @@ def get_fr_join(n_assemblies, firing_rate=(0., 0., 0.),
530
530
  t_offset = t_offset[::-1]
531
531
 
532
532
  if verbose:
533
- print(f"Cycle information:")
533
+ print("Cycle information:")
534
534
  print(f"Time per cycle: {t_cycle}")
535
535
  print(f"Number of cycles: {n_cycle}")
536
536
  print(f"Recruitment timing: {ramp_on_time} to {ramp_off_time}")
@@ -725,7 +725,7 @@ def get_fr_fade(n_assemblies, off_rate=10., firing_rate=(0., 0., 0., 0.),
725
725
  n_selected = len(assembly_index)
726
726
 
727
727
  if verbose:
728
- print(f"\nCycle information:")
728
+ print("\nCycle information:")
729
729
  print(f"Time per cycle: {t_cycle}")
730
730
  print(f"Number of cycles: {n_cycle}")
731
731
  print(f"Ramp timing: {ramp_on_time} to {ramp_off_time} within on_time of {on_time}")
@@ -2573,7 +2573,7 @@ class GapJunctionTuner:
2573
2573
  gap junction coupling between two cells.
2574
2574
  """
2575
2575
  import ipywidgets as widgets
2576
- from IPython.display import display, clear_output
2576
+ from IPython.display import clear_output, display
2577
2577
 
2578
2578
  # Connection dropdown
2579
2579
  connection_options = sorted(list(self.conn_type_settings.keys()))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bmtool
3
- Version: 0.8.2
3
+ Version: 0.8.3
4
4
  Summary: BMTool
5
5
  Home-page: https://github.com/cyneuro/bmtool
6
6
  Download-URL:
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setup(
7
7
  name="bmtool",
8
- version="0.8.2",
8
+ version="0.8.3",
9
9
  author="Neural Engineering Laboratory at the University of Missouri",
10
10
  author_email="gregglickert@mail.missouri.edu",
11
11
  description="BMTool",
@@ -1,3 +0,0 @@
1
- from .core import StimulusBuilder
2
- from . import generators
3
- from . import assemblies
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -3,12 +3,12 @@ import math
3
3
  import os
4
4
  import smtplib
5
5
  import sys
6
- from functools import partial
7
6
  from argparse import SUPPRESS, RawTextHelpFormatter
8
7
  from email.mime.application import MIMEApplication
9
8
  from email.mime.multipart import MIMEMultipart
10
9
  from email.mime.text import MIMEText
11
10
  from email.utils import COMMASPACE, formatdate
11
+ from functools import partial
12
12
  from os.path import basename
13
13
  from pathlib import Path
14
14
  from typing import Dict, List, Union
File without changes
File without changes