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.
- network_core/__init__.py +0 -0
- network_core/conn.py +188 -0
- network_core/connOps.py +206 -0
- network_core/dataModels.py +62 -0
- network_core/utils/__init__.py +0 -0
- network_core/utils/csvIO.py +9 -0
- network_core/utils/dt.py +47 -0
- network_core/utils/jsonIO.py +124 -0
- network_core-0.1.0.dist-info/METADATA +12 -0
- network_core-0.1.0.dist-info/RECORD +12 -0
- network_core-0.1.0.dist-info/WHEEL +5 -0
- network_core-0.1.0.dist-info/top_level.txt +1 -0
network_core/__init__.py
ADDED
|
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
|
network_core/connOps.py
ADDED
|
@@ -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
|
network_core/utils/dt.py
ADDED
|
@@ -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 @@
|
|
|
1
|
+
network_core
|