koi-net 1.0.0b5__py3-none-any.whl → 1.0.0b7__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.
Potentially problematic release.
This version of koi-net might be problematic. Click here for more details.
- koi_net/core.py +36 -12
- koi_net/network/graph.py +3 -3
- koi_net/network/interface.py +2 -5
- koi_net/processor/interface.py +39 -12
- {koi_net-1.0.0b5.dist-info → koi_net-1.0.0b7.dist-info}/METADATA +17 -11
- {koi_net-1.0.0b5.dist-info → koi_net-1.0.0b7.dist-info}/RECORD +8 -8
- {koi_net-1.0.0b5.dist-info → koi_net-1.0.0b7.dist-info}/WHEEL +0 -0
- {koi_net-1.0.0b5.dist-info → koi_net-1.0.0b7.dist-info}/licenses/LICENSE +0 -0
koi_net/core.py
CHANGED
|
@@ -11,25 +11,30 @@ from .protocol.event import Event, EventType
|
|
|
11
11
|
|
|
12
12
|
logger = logging.getLogger(__name__)
|
|
13
13
|
|
|
14
|
+
|
|
14
15
|
class NodeInterface:
|
|
15
16
|
cache: Cache
|
|
16
17
|
identity: NodeIdentity
|
|
17
18
|
network: NetworkInterface
|
|
18
19
|
processor: ProcessorInterface
|
|
19
20
|
first_contact: str
|
|
21
|
+
use_kobj_processor_thread: bool
|
|
20
22
|
|
|
21
23
|
def __init__(
|
|
22
24
|
self,
|
|
23
25
|
name: str,
|
|
24
26
|
profile: NodeProfile,
|
|
25
27
|
identity_file_path: str = "identity.json",
|
|
28
|
+
event_queues_file_path: str = "event_queues.json",
|
|
29
|
+
cache_directory_path: str = "rid_cache",
|
|
30
|
+
use_kobj_processor_thread: bool = False,
|
|
26
31
|
first_contact: str | None = None,
|
|
27
32
|
handlers: list[KnowledgeHandler] | None = None,
|
|
28
33
|
cache: Cache | None = None,
|
|
29
34
|
network: NetworkInterface | None = None,
|
|
30
35
|
processor: ProcessorInterface | None = None
|
|
31
36
|
):
|
|
32
|
-
self.cache = cache or Cache(
|
|
37
|
+
self.cache = cache or Cache(cache_directory_path)
|
|
33
38
|
self.identity = NodeIdentity(
|
|
34
39
|
name=name,
|
|
35
40
|
profile=profile,
|
|
@@ -38,7 +43,7 @@ class NodeInterface:
|
|
|
38
43
|
)
|
|
39
44
|
self.first_contact = first_contact
|
|
40
45
|
self.network = network or NetworkInterface(
|
|
41
|
-
file_path=
|
|
46
|
+
file_path=event_queues_file_path,
|
|
42
47
|
first_contact=self.first_contact,
|
|
43
48
|
cache=self.cache,
|
|
44
49
|
identity=self.identity
|
|
@@ -50,30 +55,41 @@ class NodeInterface:
|
|
|
50
55
|
obj for obj in vars(default_handlers).values()
|
|
51
56
|
if isinstance(obj, KnowledgeHandler)
|
|
52
57
|
]
|
|
53
|
-
|
|
58
|
+
|
|
59
|
+
self.use_kobj_processor_thread = use_kobj_processor_thread
|
|
54
60
|
self.processor = processor or ProcessorInterface(
|
|
55
61
|
cache=self.cache,
|
|
56
62
|
network=self.network,
|
|
57
63
|
identity=self.identity,
|
|
64
|
+
use_kobj_processor_thread=self.use_kobj_processor_thread,
|
|
58
65
|
default_handlers=handlers
|
|
59
66
|
)
|
|
60
67
|
|
|
61
|
-
def
|
|
62
|
-
"""
|
|
68
|
+
def start(self) -> None:
|
|
69
|
+
"""Starts a node, call this method first.
|
|
63
70
|
|
|
64
|
-
Loads event queues into memory. Generates network graph from nodes and edges in cache. Processes any state changes of node bundle. Initiates handshake with first contact (if provided) if node doesn't have any neighbors.
|
|
71
|
+
Starts the processor thread (if enabled). Loads event queues into memory. Generates network graph from nodes and edges in cache. Processes any state changes of node bundle. Initiates handshake with first contact (if provided) if node doesn't have any neighbors.
|
|
65
72
|
"""
|
|
66
|
-
self.
|
|
73
|
+
if self.use_kobj_processor_thread:
|
|
74
|
+
logger.info("Starting processor worker thread")
|
|
75
|
+
self.processor.worker_thread.start()
|
|
67
76
|
|
|
77
|
+
self.network._load_event_queues()
|
|
68
78
|
self.network.graph.generate()
|
|
69
79
|
|
|
70
80
|
self.processor.handle(
|
|
71
81
|
bundle=Bundle.generate(
|
|
72
82
|
rid=self.identity.rid,
|
|
73
83
|
contents=self.identity.profile.model_dump()
|
|
74
|
-
)
|
|
75
|
-
flush=True
|
|
84
|
+
)
|
|
76
85
|
)
|
|
86
|
+
|
|
87
|
+
logger.info("Waiting for kobj queue to empty")
|
|
88
|
+
if self.use_kobj_processor_thread:
|
|
89
|
+
self.processor.kobj_queue.join()
|
|
90
|
+
else:
|
|
91
|
+
self.processor.flush_kobj_queue()
|
|
92
|
+
logger.info("Done")
|
|
77
93
|
|
|
78
94
|
if not self.network.graph.get_neighbors() and self.first_contact:
|
|
79
95
|
logger.info(f"I don't have any neighbors, reaching out to first contact {self.first_contact}")
|
|
@@ -94,9 +110,17 @@ class NodeInterface:
|
|
|
94
110
|
return
|
|
95
111
|
|
|
96
112
|
|
|
97
|
-
def
|
|
98
|
-
"""
|
|
113
|
+
def stop(self):
|
|
114
|
+
"""Stops a node, call this method last.
|
|
99
115
|
|
|
100
|
-
Saves event queues to storage.
|
|
116
|
+
Finishes processing knowledge object queue. Saves event queues to storage.
|
|
101
117
|
"""
|
|
118
|
+
logger.info("Stopping node...")
|
|
119
|
+
|
|
120
|
+
if self.use_kobj_processor_thread:
|
|
121
|
+
logger.info("Waiting for kobj queue to empty")
|
|
122
|
+
self.processor.kobj_queue.join()
|
|
123
|
+
else:
|
|
124
|
+
self.processor.flush_kobj_queue()
|
|
125
|
+
|
|
102
126
|
self.network._save_event_queues()
|
koi_net/network/graph.py
CHANGED
|
@@ -74,13 +74,13 @@ class NetworkGraph:
|
|
|
74
74
|
"""Returns edges this node belongs to.
|
|
75
75
|
|
|
76
76
|
All edges returned by default, specify `direction` to restrict to incoming or outgoing edges only."""
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
edges = []
|
|
79
|
-
if direction != "in":
|
|
79
|
+
if direction != "in" and self.dg.out_edges:
|
|
80
80
|
out_edges = self.dg.out_edges(self.identity.rid)
|
|
81
81
|
edges.extend([e for e in out_edges])
|
|
82
82
|
|
|
83
|
-
if direction != "out":
|
|
83
|
+
if direction != "out" and self.dg.in_edges:
|
|
84
84
|
in_edges = self.dg.in_edges(self.identity.rid)
|
|
85
85
|
edges.extend([e for e in in_edges])
|
|
86
86
|
|
koi_net/network/interface.py
CHANGED
|
@@ -165,6 +165,8 @@ class NetworkInterface:
|
|
|
165
165
|
return
|
|
166
166
|
|
|
167
167
|
events = self._flush_queue(self.webhook_event_queue, node)
|
|
168
|
+
if not events: return
|
|
169
|
+
|
|
168
170
|
logger.info(f"Broadcasting {len(events)} events")
|
|
169
171
|
|
|
170
172
|
try:
|
|
@@ -173,11 +175,6 @@ class NetworkInterface:
|
|
|
173
175
|
logger.warning("Broadcast failed, requeuing events")
|
|
174
176
|
for event in events:
|
|
175
177
|
self.push_event_to(event, node)
|
|
176
|
-
|
|
177
|
-
def flush_all_webhook_queues(self):
|
|
178
|
-
"""Flushes all nodes' webhook queues and broadcasts events."""
|
|
179
|
-
for node in self.webhook_event_queue.keys():
|
|
180
|
-
self.flush_webhook_queue(node)
|
|
181
178
|
|
|
182
179
|
def get_state_providers(self, rid_type: RIDType) -> list[KoiNetNode]:
|
|
183
180
|
"""Returns list of node RIDs which provide state for the specified RID type."""
|
koi_net/processor/interface.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
|
|
2
|
+
import queue
|
|
3
|
+
import threading
|
|
3
4
|
from typing import Callable
|
|
4
5
|
from rid_lib.core import RID, RIDType
|
|
5
6
|
from rid_lib.ext import Bundle, Cache, Manifest
|
|
@@ -30,20 +31,30 @@ class ProcessorInterface:
|
|
|
30
31
|
network: NetworkInterface
|
|
31
32
|
identity: NodeIdentity
|
|
32
33
|
handlers: list[KnowledgeHandler]
|
|
33
|
-
kobj_queue: Queue[KnowledgeObject]
|
|
34
|
+
kobj_queue: queue.Queue[KnowledgeObject]
|
|
35
|
+
use_kobj_processor_thread: bool
|
|
36
|
+
worker_thread: threading.Thread | None = None
|
|
34
37
|
|
|
35
38
|
def __init__(
|
|
36
39
|
self,
|
|
37
40
|
cache: Cache,
|
|
38
41
|
network: NetworkInterface,
|
|
39
42
|
identity: NodeIdentity,
|
|
43
|
+
use_kobj_processor_thread: bool,
|
|
40
44
|
default_handlers: list[KnowledgeHandler] = []
|
|
41
45
|
):
|
|
42
46
|
self.cache = cache
|
|
43
47
|
self.network = network
|
|
44
48
|
self.identity = identity
|
|
49
|
+
self.use_kobj_processor_thread = use_kobj_processor_thread
|
|
45
50
|
self.handlers: list[KnowledgeHandler] = default_handlers
|
|
46
|
-
self.kobj_queue = Queue()
|
|
51
|
+
self.kobj_queue = queue.Queue()
|
|
52
|
+
|
|
53
|
+
if self.use_kobj_processor_thread:
|
|
54
|
+
self.worker_thread = threading.Thread(
|
|
55
|
+
target=self.kobj_processor_worker,
|
|
56
|
+
daemon=True
|
|
57
|
+
)
|
|
47
58
|
|
|
48
59
|
def add_handler(self, handler: KnowledgeHandler):
|
|
49
60
|
self.handlers.append(handler)
|
|
@@ -195,18 +206,38 @@ class ProcessorInterface:
|
|
|
195
206
|
logger.info("No network targets set")
|
|
196
207
|
|
|
197
208
|
for node in kobj.network_targets:
|
|
198
|
-
self.network.push_event_to(kobj.normalized_event, node)
|
|
199
|
-
self.network.flush_all_webhook_queues()
|
|
209
|
+
self.network.push_event_to(kobj.normalized_event, node, flush=True)
|
|
200
210
|
|
|
201
211
|
kobj = self.call_handler_chain(HandlerType.Final, kobj)
|
|
202
|
-
|
|
212
|
+
|
|
203
213
|
def flush_kobj_queue(self):
|
|
204
|
-
"""Flushes all knowledge objects from queue and processes them.
|
|
214
|
+
"""Flushes all knowledge objects from queue and processes them.
|
|
215
|
+
|
|
216
|
+
NOTE: ONLY CALL THIS METHOD IN SINGLE THREADED NODES, OTHERWISE THIS WILL CAUSE RACE CONDITIONS.
|
|
217
|
+
"""
|
|
218
|
+
if self.use_kobj_processor_thread:
|
|
219
|
+
logger.warning("You are using a worker thread, calling this method can cause race conditions!")
|
|
220
|
+
|
|
205
221
|
while not self.kobj_queue.empty():
|
|
206
222
|
kobj = self.kobj_queue.get()
|
|
207
223
|
logger.info(f"Dequeued {kobj!r}")
|
|
208
224
|
self.process_kobj(kobj)
|
|
225
|
+
self.kobj_queue.task_done()
|
|
209
226
|
logger.info("Done handling")
|
|
227
|
+
|
|
228
|
+
def kobj_processor_worker(self, timeout=0.1):
|
|
229
|
+
while True:
|
|
230
|
+
try:
|
|
231
|
+
kobj = self.kobj_queue.get(timeout=timeout)
|
|
232
|
+
logger.info(f"Dequeued {kobj!r}")
|
|
233
|
+
self.process_kobj(kobj)
|
|
234
|
+
self.kobj_queue.task_done()
|
|
235
|
+
|
|
236
|
+
except queue.Empty:
|
|
237
|
+
pass
|
|
238
|
+
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.warning(f"Error processing kobj: {e}")
|
|
210
241
|
|
|
211
242
|
def handle(
|
|
212
243
|
self,
|
|
@@ -216,8 +247,7 @@ class ProcessorInterface:
|
|
|
216
247
|
event: Event | None = None,
|
|
217
248
|
kobj: KnowledgeObject | None = None,
|
|
218
249
|
event_type: KnowledgeEventType = None,
|
|
219
|
-
source: KnowledgeSource = KnowledgeSource.Internal
|
|
220
|
-
flush: bool = False
|
|
250
|
+
source: KnowledgeSource = KnowledgeSource.Internal
|
|
221
251
|
):
|
|
222
252
|
"""Queues provided knowledge to be handled by processing pipeline.
|
|
223
253
|
|
|
@@ -238,6 +268,3 @@ class ProcessorInterface:
|
|
|
238
268
|
|
|
239
269
|
self.kobj_queue.put(_kobj)
|
|
240
270
|
logger.info(f"Queued {_kobj!r}")
|
|
241
|
-
|
|
242
|
-
if flush:
|
|
243
|
-
self.flush_kobj_queue()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: koi-net
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.0b7
|
|
4
4
|
Summary: Implementation of KOI-net protocol in Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/BlockScience/koi-net/
|
|
6
6
|
Author-email: Luke Miller <luke@block.science>
|
|
@@ -150,7 +150,7 @@ node = NodeInterface(
|
|
|
150
150
|
|
|
151
151
|
## Knowledge Processing
|
|
152
152
|
|
|
153
|
-
Next we'll set up the knowledge processing flow for our node. This is where most of the node's logic and behavior will come into play. For partial nodes this will be an event loop, and for full nodes we will use webhooks. Make sure to call `node.
|
|
153
|
+
Next we'll set up the knowledge processing flow for our node. This is where most of the node's logic and behavior will come into play. For partial nodes this will be an event loop, and for full nodes we will use webhooks. Make sure to call `node.start()` and `node.stop()` at the beginning and end of your node's life cycle.
|
|
154
154
|
|
|
155
155
|
### Partial Node
|
|
156
156
|
Make sure to set `source=KnowledgeSource.External`, this indicates to the knowledge processing pipeline that the incoming knowledge was received from an external source. Where the knowledge is sourced from will impact decisions in the node's knowledge handlers.
|
|
@@ -159,7 +159,7 @@ import time
|
|
|
159
159
|
from koi_net.processor.knowledge_object import KnowledgeSource
|
|
160
160
|
|
|
161
161
|
if __name__ == "__main__":
|
|
162
|
-
node.
|
|
162
|
+
node.start()
|
|
163
163
|
|
|
164
164
|
try:
|
|
165
165
|
while True:
|
|
@@ -170,20 +170,20 @@ if __name__ == "__main__":
|
|
|
170
170
|
time.sleep(5)
|
|
171
171
|
|
|
172
172
|
finally:
|
|
173
|
-
node.
|
|
173
|
+
node.stop()
|
|
174
174
|
```
|
|
175
175
|
|
|
176
176
|
### Full Node
|
|
177
|
-
Setting up a full node is slightly more complex as we'll need a webserver. For this example, we'll use FastAPI and uvicorn. First we need to setup the "lifespan" of the server, to
|
|
177
|
+
Setting up a full node is slightly more complex as we'll need a webserver. For this example, we'll use FastAPI and uvicorn. First we need to setup the "lifespan" of the server, to start and stop the node before and after execution, as well as the FastAPI app which will be our web server.
|
|
178
178
|
```python
|
|
179
179
|
from contextlib import asynccontextmanager
|
|
180
180
|
from fastapi import FastAPI
|
|
181
181
|
|
|
182
182
|
@asynccontextmanager
|
|
183
183
|
async def lifespan(app: FastAPI):
|
|
184
|
-
node.
|
|
184
|
+
node.start()
|
|
185
185
|
yield
|
|
186
|
-
node.
|
|
186
|
+
node.stop()
|
|
187
187
|
|
|
188
188
|
|
|
189
189
|
app = FastAPI(lifespan=lifespan, root_path="/koi-net")
|
|
@@ -265,12 +265,16 @@ class NodeInterface:
|
|
|
265
265
|
network: NetworkInterface
|
|
266
266
|
processor: ProcessorInterface
|
|
267
267
|
first_contact: str
|
|
268
|
+
use_kobj_processor_thread: bool
|
|
268
269
|
|
|
269
270
|
def __init__(
|
|
270
271
|
self,
|
|
271
272
|
name: str,
|
|
272
273
|
profile: NodeProfile,
|
|
273
274
|
identity_file_path: str = "identity.json",
|
|
275
|
+
event_queues_file_path: str = "event_queues.json",
|
|
276
|
+
cache_directory_path: str = "rid_cache",
|
|
277
|
+
use_kobj_processor_thread: bool = False,
|
|
274
278
|
first_contact: str | None = None,
|
|
275
279
|
handlers: list[KnowledgeHandler] | None = None,
|
|
276
280
|
cache: Cache | None = None,
|
|
@@ -278,8 +282,8 @@ class NodeInterface:
|
|
|
278
282
|
processor: ProcessorInterface | None = None
|
|
279
283
|
): ...
|
|
280
284
|
|
|
281
|
-
def
|
|
282
|
-
def
|
|
285
|
+
def start(self): ...
|
|
286
|
+
def stop(self): ...
|
|
283
287
|
```
|
|
284
288
|
As you can see, only a name and profile are required. The other fields allow for additional customization if needed.
|
|
285
289
|
|
|
@@ -313,7 +317,6 @@ class NetworkInterface:
|
|
|
313
317
|
|
|
314
318
|
def flush_poll_queue(self, node: KoiNetNode) -> list[Event]: ...
|
|
315
319
|
def flush_webhook_queue(self, node: RID): ...
|
|
316
|
-
def flush_all_webhook_queues(self): ...
|
|
317
320
|
|
|
318
321
|
def fetch_remote_bundle(self, rid: RID): ...
|
|
319
322
|
def fetch_remote_manifest(self, rid: RID): ...
|
|
@@ -432,11 +435,14 @@ def poll_events(req: PollEvents) -> EventsPayload:
|
|
|
432
435
|
The `ProcessorInterface` class provides access to a node's internal knowledge processing pipeline.
|
|
433
436
|
```python
|
|
434
437
|
class ProcessorInterface:
|
|
438
|
+
worker_thread: threading.Thread | None = None
|
|
439
|
+
|
|
435
440
|
def __init__(
|
|
436
441
|
self,
|
|
437
442
|
cache: Cache,
|
|
438
443
|
network: NetworkInterface,
|
|
439
444
|
identity: NodeIdentity,
|
|
445
|
+
use_kobj_processor_thread: bool,
|
|
440
446
|
default_handlers: list[KnowledgeHandler] = []
|
|
441
447
|
): ...
|
|
442
448
|
|
|
@@ -506,5 +512,5 @@ python -m build
|
|
|
506
512
|
```
|
|
507
513
|
Push new package build to PyPI:
|
|
508
514
|
```shell
|
|
509
|
-
python -m twine upload
|
|
515
|
+
python -m twine upload dist/*
|
|
510
516
|
```
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
koi_net/__init__.py,sha256=b0Ze0pZmJAuygpWUFHM6Kvqo3DkU_uzmkptv1EpAArw,31
|
|
2
|
-
koi_net/core.py,sha256=
|
|
2
|
+
koi_net/core.py,sha256=dE4sE2qsoIRUU1zsnrjx7aqYtYdHyCx-Dv4cwbkRjy4,4613
|
|
3
3
|
koi_net/identity.py,sha256=PBgmAx5f3zzQmHASB1TJW2g19n9TLfmSJMXg2eQFg0A,2386
|
|
4
4
|
koi_net/network/__init__.py,sha256=r_RN-q_mDYC-2RAkN-lJoMUX76TXyfEUc_MVKW87z0g,39
|
|
5
|
-
koi_net/network/graph.py,sha256=
|
|
6
|
-
koi_net/network/interface.py,sha256=
|
|
5
|
+
koi_net/network/graph.py,sha256=KMUCU3AweRvivwy7GuWgX2zX74FPgHeVMO5ydvhVyvA,4833
|
|
6
|
+
koi_net/network/interface.py,sha256=MpqIW-mf1y9eWteMUyTG7kQ9pwB1ubuiAiF9cVBTA84,10647
|
|
7
7
|
koi_net/network/request_handler.py,sha256=fhuCDsxI8fZ4p5TntcTZR4mnLrLQ61zDy7Oca3ooFCE,4402
|
|
8
8
|
koi_net/network/response_handler.py,sha256=mA3FtrN3aTZATcLaHQhJUWrJdIKNv6d24fhvOl-nDKY,1890
|
|
9
9
|
koi_net/processor/__init__.py,sha256=x4fAY0hvQEDcpfdTB3POIzxBQjYAtn0qQazPo1Xm0m4,41
|
|
10
10
|
koi_net/processor/default_handlers.py,sha256=Yc7a9n5sAOYMHzzY59VMXYOxQL-6O9zbMQzd61XbIEs,7184
|
|
11
11
|
koi_net/processor/handler.py,sha256=APCECwU7MFcgP7Vu6UTngs0XIjaXSQ_f8rqy8cH5_rM,2242
|
|
12
|
-
koi_net/processor/interface.py,sha256=
|
|
12
|
+
koi_net/processor/interface.py,sha256=szLLeDfMgeqU35F2na-LvzytJ0irpCtR9g0empo4JoI,12169
|
|
13
13
|
koi_net/processor/knowledge_object.py,sha256=cGv33fwNZQMylkhlTaQTbk96FVIVbdOUaBsG06u0m4k,4187
|
|
14
14
|
koi_net/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
15
|
koi_net/protocol/api_models.py,sha256=79B5IWQ7gsJ_QIsSRv9424F1frF_DMGkhBbYWkXgtOI,1118
|
|
@@ -18,7 +18,7 @@ koi_net/protocol/edge.py,sha256=G3D9Ie0vbTSMJdoTw9g_oBmFCqzJ1gO7U1PVrw7p3j8,447
|
|
|
18
18
|
koi_net/protocol/event.py,sha256=dzJmcHbimo7p5NwH2drccF0vMcAj9oQRj3iZ9Bjf7kg,1275
|
|
19
19
|
koi_net/protocol/helpers.py,sha256=9E9PaoIuSNrTBATGCLJ_kSBMZ2z-KIMnLJzGOTqQDC0,719
|
|
20
20
|
koi_net/protocol/node.py,sha256=Ntrx01dbm39ViKGtr4gLmztcMwKpTIweS6rRL-zoU_Y,391
|
|
21
|
-
koi_net-1.0.
|
|
22
|
-
koi_net-1.0.
|
|
23
|
-
koi_net-1.0.
|
|
24
|
-
koi_net-1.0.
|
|
21
|
+
koi_net-1.0.0b7.dist-info/METADATA,sha256=18EhyJV1HMwYJi0wFRw-wDstKE-Q0KRFq07fGth93jI,21407
|
|
22
|
+
koi_net-1.0.0b7.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
23
|
+
koi_net-1.0.0b7.dist-info/licenses/LICENSE,sha256=XBcvl8yjCAezfuqN1jadQykrX7H2g4nr2WRDmHLW6ik,1090
|
|
24
|
+
koi_net-1.0.0b7.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|