network-core 0.1.0__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.
File without changes
network_core/conn.py ADDED
@@ -0,0 +1,188 @@
1
+ # Define PacketInfo class
2
+ from typing import List
3
+ from .dataModels import PacketType, TransPortType, PacketInfo, FiveTuple
4
+ from scapy.all import UDP, TCP, IP, IPv6 # type: ignore
5
+
6
+
7
+ def getPacketType(packet):
8
+ """
9
+ Here packet is a scapy packet
10
+ """
11
+ if TCP in packet:
12
+ return PacketType.TCP
13
+ elif UDP in packet:
14
+ return PacketType.UDP
15
+ return PacketType.UNKNOWN
16
+
17
+
18
+ def getPacketInfoFromPacket(packet, direction: int):
19
+ packet_type = getPacketType(packet)
20
+
21
+ payload_bytes = b""
22
+ if UDP in packet:
23
+ payload_bytes = bytes(packet[UDP].payload)
24
+ elif TCP in packet:
25
+ payload_bytes = bytes(packet[TCP].payload)
26
+
27
+ return PacketInfo(
28
+ length=len(packet),
29
+ timestamp=float(packet.time),
30
+ direction=direction,
31
+ packet_type=packet_type,
32
+ transport_payload=payload_bytes,
33
+ other_info=(
34
+ {"seq": packet[TCP].seq} if packet_type == PacketType.TCP else dict()
35
+ ),
36
+ )
37
+
38
+
39
+ class Connection:
40
+ def __init__(
41
+ self,
42
+ five_tuple: FiveTuple,
43
+ other_info=None,
44
+ ):
45
+ self.five_tuple = five_tuple
46
+ self.connection_type: set[PacketType] = set()
47
+ self.packet_stream: List[PacketInfo] = []
48
+ self.other_info = other_info
49
+ self.__sni = None
50
+ self.is_quic = False
51
+
52
+ @staticmethod
53
+ def getTransPortType(packet):
54
+ if TCP in packet:
55
+ return TransPortType.TCP
56
+ elif UDP in packet:
57
+ return TransPortType.UDP
58
+ return TransPortType.UNKNOWN
59
+
60
+ @staticmethod
61
+ def getConnFromPacket(packet):
62
+ transport_type = Connection.getTransPortType(packet)
63
+
64
+ packet_type = getPacketType(packet)
65
+ if packet_type == PacketType.UNKNOWN:
66
+ return None
67
+ ip_layer = IP if IP in packet else IPv6
68
+
69
+ if transport_type == TransPortType.TCP:
70
+ ft = FiveTuple(
71
+ src_ip=packet[ip_layer].src,
72
+ dst_ip=packet[ip_layer].dst,
73
+ src_port=packet[TCP].sport,
74
+ dst_port=packet[TCP].dport,
75
+ transport_type=transport_type,
76
+ )
77
+ return Connection(five_tuple=ft)
78
+ elif transport_type == TransPortType.UDP:
79
+ ft = FiveTuple(
80
+ src_ip=packet[ip_layer].src,
81
+ dst_ip=packet[ip_layer].dst,
82
+ src_port=packet[UDP].sport,
83
+ dst_port=packet[UDP].dport,
84
+ transport_type=transport_type,
85
+ )
86
+ return Connection(five_tuple=ft)
87
+ return None
88
+
89
+ def addPacket(self, packet: PacketInfo):
90
+ if packet.packet_type == PacketType.UNKNOWN:
91
+ assert False, "Packet Type is Unknown"
92
+ self.connection_type.add(packet.packet_type)
93
+ self.packet_stream.append(packet)
94
+
95
+ def __len__(self):
96
+ return len(self.packet_stream)
97
+
98
+ def setSNI(self, sni: str):
99
+ self.__sni = sni
100
+
101
+ def getSNI(self):
102
+ return self.__sni
103
+
104
+ def getPacketsOfTypes(self, packet_types: set[PacketType]) -> List[PacketInfo]:
105
+ return list(filter(lambda x: x.packet_type in packet_types, self.packet_stream))
106
+
107
+ def getRemovedPacketsOfTypes(self, packet_types: set[PacketType]):
108
+ return list(
109
+ filter(lambda x: x.packet_type not in packet_types, self.packet_stream)
110
+ )
111
+
112
+ def getPacketsOfDirection(self, direction: int) -> list[PacketInfo]:
113
+ return [p for p in self.packet_stream if p.direction == direction]
114
+
115
+ def getSubConnection(self, packet_types: List[PacketType]):
116
+ packets = self.getPacketsOfTypes(packet_types=set(packet_types))
117
+
118
+ new_conn = Connection(five_tuple=self.five_tuple)
119
+ for packet in packets:
120
+ new_conn.addPacket(packet=packet)
121
+ return new_conn
122
+
123
+ def getDuration(self):
124
+ """
125
+ Returns the duration of connection on seconds
126
+ """
127
+ if len(self.packet_stream) == 0:
128
+ return 0
129
+
130
+ return self.packet_stream[-1].timestamp - self.packet_stream[0].timestamp
131
+
132
+ def sort(self):
133
+ self.packet_stream.sort(key=lambda x: x.timestamp)
134
+
135
+ def pps(self):
136
+ num_packets = len(self.packet_stream)
137
+ duration = self.getDuration()
138
+ if num_packets == 0 or duration == 0:
139
+ return 0
140
+
141
+ return num_packets / duration
142
+
143
+ def removePayload(self):
144
+ """
145
+ This is an irreversable operation, removes packets inplace
146
+ """
147
+ for packet in self.packet_stream:
148
+ packet.transport_payload = bytes()
149
+
150
+ @property
151
+ def down_bytes(self):
152
+ total = 0
153
+ for p in self.packet_stream:
154
+ if p.direction == 1:
155
+ total += p.length
156
+ return total
157
+
158
+ @property
159
+ def up_bytes(self):
160
+ total = 0
161
+ for p in self.packet_stream:
162
+ if p.direction == 0:
163
+ total += p.length
164
+ return total
165
+
166
+ @property
167
+ def down_packets(self):
168
+ total = 0
169
+ for p in self.packet_stream:
170
+ if p.direction == 1:
171
+ total += 1
172
+ return total
173
+
174
+ @property
175
+ def up_packets(self):
176
+ total = 0
177
+ for p in self.packet_stream:
178
+ if p.direction == 0:
179
+ total += 1
180
+ return total
181
+
182
+ @property
183
+ def start_timestamp(self):
184
+ return self.packet_stream[0].timestamp
185
+
186
+ @property
187
+ def end_timestamp(self):
188
+ return self.packet_stream[-1].timestamp
@@ -0,0 +1,206 @@
1
+ from .conn import Connection
2
+ from .dataModels import PacketInfo, PacketType, FiveTuple
3
+ from .utils.dt import convertUNIXToHumanReadable
4
+
5
+ import numpy as np
6
+
7
+
8
+ class ConnStats:
9
+ def __init__(self, conn: Connection):
10
+ self.conn = conn
11
+
12
+ def getUpStreamBytes(self):
13
+ packet_stream = self.conn.packet_stream
14
+
15
+ up_stream_bytes = 0
16
+ for packet in packet_stream:
17
+ if packet.direction == 0:
18
+ up_stream_bytes += packet.length
19
+ return up_stream_bytes
20
+
21
+ def getDownStreamBytes(self):
22
+ packet_stream = self.conn.packet_stream
23
+
24
+ down_stream_bytes = 0
25
+ for packet in packet_stream:
26
+ if packet.direction == 1:
27
+ down_stream_bytes += packet.length
28
+ return down_stream_bytes
29
+
30
+ def getConnSpan(self):
31
+ packet_stream = self.conn.packet_stream
32
+ return packet_stream[-1].timestamp - packet_stream[0].timestamp
33
+
34
+ def getPacketTypeCounts(self):
35
+ packet_stream = self.conn.packet_stream
36
+ packet_type_counts = dict()
37
+ for packet in packet_stream:
38
+ packet_type_counts[packet.packet_type] = 1 + packet_type_counts.get(
39
+ packet.packet_type, 0
40
+ )
41
+ return packet_type_counts
42
+
43
+ def getStartTime(self):
44
+ return convertUNIXToHumanReadable(self.conn.packet_stream[0].timestamp)
45
+
46
+ def getEndTime(self):
47
+ return convertUNIXToHumanReadable(self.conn.packet_stream[-1].timestamp)
48
+
49
+
50
+ def filterConnections(
51
+ connections: list[Connection],
52
+ connection_types: list[PacketType],
53
+ min_packets: int | None = None,
54
+ min_duration_in_seconds: float | None = None,
55
+ ):
56
+ """
57
+ filter connections by connection types, the connection must have atleast one packet of the types in the connection_types
58
+ :param connections: List of connections to filter
59
+ :param connection_types: List of connection types to filter by
60
+ :return: Filtered connections
61
+ """
62
+
63
+ def doesContainConnectionTypes(
64
+ connection: Connection, connection_types: list[PacketType]
65
+ ):
66
+ for connection_type in connection_types:
67
+ if connection_type not in connection.connection_type:
68
+ return False
69
+ return True
70
+
71
+ f_connections = [
72
+ connection
73
+ for connection in connections
74
+ if doesContainConnectionTypes(connection, connection_types)
75
+ ]
76
+
77
+ if min_packets is not None:
78
+ f_connections = [
79
+ connection for connection in f_connections if len(connection) >= min_packets
80
+ ]
81
+ if min_duration_in_seconds is not None:
82
+ f_connections = [
83
+ connection
84
+ for connection in f_connections
85
+ if ConnStats(connection).getConnSpan() >= min_duration_in_seconds
86
+ ]
87
+ return f_connections
88
+
89
+
90
+ def combine_connections(connections: list[Connection], enforce_transport=False):
91
+ """
92
+ Combine connections with the same src_ip, src_port, dst_ip, dst_port and transport_type in the end. The first connection's ip and port will be used
93
+ :param connections: List of connections to combine
94
+ :param enforce_transport: If True, enforce that all connections have the same transport_type
95
+ :return: Combined connection sorted by timestamp
96
+ """
97
+ if len(connections) == 0:
98
+ return None
99
+ if len(connections) == 1:
100
+ return connections[0]
101
+
102
+ combined_connection = Connection(
103
+ five_tuple=connections[0].five_tuple,
104
+ )
105
+ for connection in connections:
106
+ if len(connection) == 0:
107
+ continue
108
+ if enforce_transport:
109
+ assert (
110
+ connection.five_tuple.transport_type
111
+ == combined_connection.five_tuple.transport_type
112
+ ), f"{connection.five_tuple.transport_type}, {combined_connection.five_tuple.transport_type}, {len(connection)}"
113
+ for packet in connection.packet_stream:
114
+ combined_connection.addPacket(packet)
115
+ # important to sort it in order of timestamp
116
+ combined_connection.packet_stream = sorted(
117
+ combined_connection.packet_stream, key=lambda x: x.timestamp
118
+ )
119
+ return combined_connection
120
+
121
+
122
+ def printConns(connections: list[Connection]):
123
+ """
124
+ Utility to print connections
125
+ """
126
+
127
+ def splitTimeString(time_str):
128
+ return time_str.split(" ")[1]
129
+
130
+ for conn in connections:
131
+ stat = ConnStats(conn)
132
+ print(
133
+ conn.five_tuple,
134
+ f"SNI {conn.getSNI()}",
135
+ f"Start time {splitTimeString(stat.getStartTime())}",
136
+ f"End Time {splitTimeString(stat.getEndTime())}",
137
+ f"Duration {stat.getConnSpan()}",
138
+ f"Upstream Bytes {stat.getUpStreamBytes()}",
139
+ f"Downstream Bytes {stat.getDownStreamBytes()}",
140
+ conn.connection_type,
141
+ f"Num Packets {len(conn)}",
142
+ )
143
+
144
+
145
+ def getPacketsInInterval(
146
+ packet_stream: list[PacketInfo],
147
+ start_time_timestamp: float,
148
+ end_timestamp: float,
149
+ ) -> list[PacketInfo]:
150
+ """
151
+ packet_stream is assumed to be sorted, by timestamp
152
+ Also, ideally packet_stream should contain packets from the same direction.
153
+
154
+ Returns packets from with timestamp larger or equal to start_timestamp and just smaller than end_timestamp
155
+ It then becomes the function user's responsibility to validate if there are enough packets in the interval.
156
+ """
157
+
158
+ def bSearch(time: float):
159
+ start_index = 0
160
+ end_index = len(packet_stream) - 1
161
+ just_left_of_time, just_right_or_equal_of_time = None, None
162
+ while start_index <= end_index:
163
+ mid_index = (start_index + end_index) // 2
164
+ mid_timestamp = packet_stream[mid_index].timestamp
165
+ if mid_timestamp >= time:
166
+ just_right_or_equal_of_time = mid_index
167
+ end_index = mid_index - 1
168
+ elif mid_timestamp < time:
169
+ just_left_of_time = mid_index
170
+ start_index = mid_index + 1
171
+
172
+ return just_left_of_time, just_right_or_equal_of_time
173
+
174
+ _, left = bSearch(time=start_time_timestamp)
175
+ right, _ = bSearch(time=end_timestamp)
176
+
177
+ if (left is None or right is None) or left > right:
178
+ return []
179
+
180
+ return packet_stream[left : right + 1]
181
+
182
+
183
+ def normalizePacketStream(packet_stream: list[PacketInfo]) -> dict:
184
+ def normIAT(iat: np.ndarray, limit=200) -> np.ndarray:
185
+ """
186
+ iats are in miliseconds
187
+ """
188
+ iat = np.clip(iat, 0, limit) # Same as capping
189
+ return np.log1p(iat) / np.log1p(limit)
190
+
191
+ sizes = np.array([p.length for p in packet_stream])
192
+ times = np.array([p.timestamp * 100 for p in packet_stream])
193
+ iats = np.diff(times) if len(times) > 1 else np.array([])
194
+
195
+ return {
196
+ "mean_size": sizes.mean(),
197
+ "std_size": sizes.std(),
198
+ "burstiness": (sizes.max() - sizes.min()) / (sizes.mean() + 1e-5),
199
+ "mean_iat": iats.mean(),
200
+ "std_iat": iats.std(),
201
+ "packet_count": len(sizes),
202
+ "unique_sizes": len(set(sizes)),
203
+ "lengths": (sizes / 1500).tolist(),
204
+ "iats": [0] + normIAT(iats).tolist(),
205
+ "timestamps": [p.timestamp for p in packet_stream],
206
+ }
@@ -0,0 +1,62 @@
1
+ from enum import Enum
2
+ from dataclasses import dataclass, field
3
+
4
+
5
+ class PacketType(Enum):
6
+ TLS = 0
7
+ DTLS = 1
8
+ RTP = 2
9
+ RTCP = 3
10
+ STUN = 4
11
+ TURN = 5
12
+ QUIC = 6
13
+
14
+ TCP = 7
15
+ UDP = 8
16
+ UNKNOWN = 9
17
+
18
+
19
+ class TransPortType(Enum):
20
+ TCP = 0
21
+ UDP = 1
22
+ UNKNOWN = 2
23
+
24
+
25
+ @dataclass
26
+ class FiveTuple:
27
+ src_ip: str
28
+ dst_ip: str
29
+ src_port: int
30
+ dst_port: int
31
+ transport_type: TransPortType
32
+
33
+ def _get_key(self):
34
+ return (
35
+ self.src_ip,
36
+ self.dst_ip,
37
+ self.src_port,
38
+ self.dst_port,
39
+ self.transport_type.name,
40
+ )
41
+
42
+ def __hash__(self) -> int:
43
+ return hash(self._get_key())
44
+
45
+ def rev_ft(self) -> "FiveTuple":
46
+ return FiveTuple(
47
+ src_ip=self.dst_ip,
48
+ src_port=self.dst_port,
49
+ dst_ip=self.src_ip,
50
+ dst_port=self.src_port,
51
+ transport_type=self.transport_type,
52
+ )
53
+
54
+
55
+ @dataclass
56
+ class PacketInfo:
57
+ length: int
58
+ timestamp: float
59
+ direction: int
60
+ packet_type: PacketType
61
+ transport_payload: bytes = field(default_factory=bytes)
62
+ other_info: dict = field(default_factory=dict)
File without changes
@@ -0,0 +1,9 @@
1
+ import csv
2
+ from typing import Iterator
3
+
4
+
5
+ def read_csv_to_dicts(file_path) -> Iterator[dict]:
6
+ with open(file_path, newline="") as f:
7
+ reader = csv.DictReader(f)
8
+ for row in reader:
9
+ yield row
@@ -0,0 +1,47 @@
1
+ import datetime, pytz
2
+
3
+
4
+ def convertUNIXToDT(timestamp: float, timezone="Australia/Sydney") -> datetime.datetime:
5
+ australian_timezone = pytz.timezone(timezone)
6
+ utc_dt = datetime.datetime.fromtimestamp(timestamp, tz=pytz.utc)
7
+ return utc_dt.astimezone(australian_timezone)
8
+
9
+
10
+ def convertUNIXToHumanReadable(timestamp: float, timezone="Australia/Sydney") -> str:
11
+ dt = convertUNIXToDT(timestamp=timestamp, timezone=timezone)
12
+ ms = dt.microsecond // 1000 # get milliseconds, doing this so it isn't 0
13
+ return dt.strftime("%Y-%m-%d %H:%M:%S") + f".{ms}"
14
+
15
+
16
+ def getDatetimeFromTime(time: datetime.time) -> datetime.datetime:
17
+ return datetime.datetime.combine(datetime.datetime.today(), time=time)
18
+
19
+
20
+ def addBufferToTime(time: datetime.time, buffer_in_seconds: float) -> datetime.time:
21
+ delta = datetime.timedelta(seconds=buffer_in_seconds)
22
+ return (getDatetimeFromTime(time=time) + delta).time()
23
+
24
+
25
+ def fast_strptime(s: str) -> float:
26
+ """
27
+ 2026-01-11 12:25:31.689768 +0530 IST to UNIX
28
+ """
29
+ parts = s.split()
30
+
31
+ # Drop trailing timezone name (like 'UTC', 'AEDT', etc.)
32
+ if len(parts) > 3:
33
+ s = " ".join(parts[:3])
34
+ else:
35
+ s = " ".join(parts)
36
+
37
+ # Handle too many fractional digits (datetime only supports microseconds)
38
+ date, time_str, offset = s.split()
39
+ if "." in time_str:
40
+ main, frac = time_str.split(".")
41
+ frac = frac[:6] # truncate nanoseconds to microseconds
42
+ time_str = f"{main}.{frac}"
43
+ s = f"{date} {time_str} {offset}"
44
+
45
+ fmt = "%Y-%m-%d %H:%M:%S.%f %z" if "." in time_str else "%Y-%m-%d %H:%M:%S %z"
46
+
47
+ return datetime.datetime.strptime(s, fmt).timestamp()
@@ -0,0 +1,124 @@
1
+ import datetime
2
+ import json
3
+ from typing import Union, List, Dict, Iterator
4
+
5
+ import orjson
6
+ import numpy as np
7
+
8
+
9
+ def numpy_to_lists(obj):
10
+ """
11
+ Recursively convert NumPy arrays (and NumPy scalars) back to Python lists/int/float.
12
+ """
13
+ if isinstance(obj, np.ndarray):
14
+ # Convert array to nested list
15
+ return [numpy_to_lists(x) for x in obj]
16
+ elif isinstance(obj, (np.integer, np.floating)):
17
+ # Convert NumPy scalar to Python scalar
18
+ return obj.item()
19
+ elif isinstance(obj, dict):
20
+ return {k: numpy_to_lists(v) for k, v in obj.items()}
21
+ elif isinstance(obj, list):
22
+ # Already a list, convert its elements recursively
23
+ return [numpy_to_lists(x) for x in obj]
24
+ else:
25
+ return obj
26
+
27
+
28
+ class JSONCustomEncoder(json.JSONEncoder):
29
+ """
30
+ Custom JSON encoder that recursively converts NumPy arrays and scalars
31
+ to lists and native Python types.
32
+ """
33
+
34
+ def default(self, obj):
35
+ # Use your recursive conversion
36
+ obj_converted = numpy_to_lists(obj)
37
+ # After conversion, let the base class handle normal types
38
+ return super().default(obj_converted)
39
+
40
+
41
+ def lists_to_numpy(obj: dict | list):
42
+ if isinstance(obj, list):
43
+ return np.array([lists_to_numpy(x) for x in obj])
44
+ elif isinstance(obj, dict):
45
+ return {k: lists_to_numpy(v) for k, v in obj.items()}
46
+ else:
47
+ return obj
48
+
49
+
50
+ class JSONCustomDecoder(json.JSONDecoder):
51
+ """
52
+ Custom JSON decoder to convert lists back into NumPy arrays.
53
+ """
54
+
55
+ def __init__(self, *args, **kwargs):
56
+ # Use object_hook to intercept all dicts/lists during decoding
57
+ super().__init__(object_hook=self.object_hook, *args, **kwargs)
58
+
59
+ def object_hook(self, obj):
60
+ return lists_to_numpy(obj)
61
+
62
+
63
+ def saveDctAsJSON(path: str, data: Union[Dict, List[Dict]], append: bool = False):
64
+ """
65
+ Save a dict or list of dicts as JSON.
66
+
67
+ ALWAYS SAVES AT A LINE SEPERATED JSON
68
+
69
+ - Dict: saved as a single pretty-printed JSON object.
70
+ - List[Dict]: line-delimited JSON.
71
+ """
72
+ mode = "a" if append else "w"
73
+ with open(path, mode) as f:
74
+ if isinstance(data, dict):
75
+ json.dump(data, f, separators=(",", ":"), cls=JSONCustomEncoder)
76
+ f.write("\n")
77
+ elif isinstance(data, list):
78
+ # Write each item as a line-delimited JSON object
79
+ for item in data:
80
+ json.dump(item, f)
81
+ f.write("\n")
82
+
83
+
84
+ def loadJSONAsDct(
85
+ path: str, use_custom_decoder: bool = False
86
+ ) -> Union[Dict, List[Dict]]:
87
+ """
88
+ Load JSON from a file. Handles:
89
+ - Single dict (pretty-printed or one-line)
90
+ - List of dicts (array or line-delimited)
91
+ """
92
+
93
+ with open(path, "r") as f:
94
+ text = f.read().strip()
95
+ if not text:
96
+ return {} # empty file
97
+ try:
98
+ return json.loads(text) # dict or list saved as JSON array
99
+ except json.JSONDecodeError:
100
+ # fallback: line-delimited JSON
101
+ result = []
102
+ with open(path, "r") as f2:
103
+ for line in f2:
104
+ line = line.strip()
105
+ if line:
106
+ result.append(
107
+ json.loads(
108
+ line,
109
+ cls=JSONCustomDecoder if use_custom_decoder else None,
110
+ )
111
+ )
112
+ return result
113
+
114
+
115
+ def loadJsonLines(path: str, np_convert: bool = False) -> Iterator[dict]:
116
+ with open(path, "rb") as f: # orjson expects bytes
117
+ for line in f:
118
+ line = line.strip()
119
+ if line:
120
+ yield (
121
+ orjson.loads(line)
122
+ if not np_convert
123
+ else lists_to_numpy(orjson.loads(line))
124
+ ) # type: ignore
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.4
2
+ Name: network_core
3
+ Version: 0.1.0
4
+ Summary: Core networking utilities and data models
5
+ Author: Your Name
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: numpy
9
+ Requires-Dist: pandas
10
+ Requires-Dist: pytz
11
+ Requires-Dist: orjson
12
+ Requires-Dist: scapy
@@ -0,0 +1,12 @@
1
+ network_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ network_core/conn.py,sha256=-wngAzOBUtyOIUKLmFnf0YXwSrtbqaJIQuqA6sAe2dY,5444
3
+ network_core/connOps.py,sha256=9frmJ6_JgiTyceuIcx5x3NWJbjAizLA7s_TuZ-9P9wo,7135
4
+ network_core/dataModels.py,sha256=XKV6yYI9b13FzZX_1uaxzS7Och4vQSevC88Q-LpQDnA,1167
5
+ network_core/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ network_core/utils/csvIO.py,sha256=tUqL_2VZR9bMUGX_3mOuvgL_3ihokEDVMXuXZqjR3Lk,220
7
+ network_core/utils/dt.py,sha256=V1v7Ug3DVZoVyVFQZfRPpeVdxXpmPT7Re-Amy4RL6zY,1660
8
+ network_core/utils/jsonIO.py,sha256=oq0vBeiQEV_yHd07kQq1X5HLSvJnz7VAAonAOb8wFXI,3850
9
+ network_core-0.1.0.dist-info/METADATA,sha256=9d6qdHcNMF_r0GnS_sL2wrl_VynTPxxsfJaCDg0SIg4,294
10
+ network_core-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
11
+ network_core-0.1.0.dist-info/top_level.txt,sha256=wnMFOdmtkPtdr2Dm9LYLVttaV_JUMuxDQQux5YmCMo0,13
12
+ network_core-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ network_core