koi-net 1.1.0b4__py3-none-any.whl → 1.1.0b6__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 CHANGED
@@ -174,6 +174,7 @@ class NodeInterface(Generic[T]):
174
174
  processor=self.processor,
175
175
  effector=self.effector,
176
176
  actor=self.actor,
177
+ handler_context=self.handler_context,
177
178
  use_kobj_processor_thread=use_kobj_processor_thread
178
179
  )
179
180
 
koi_net/lifecycle.py CHANGED
@@ -1,4 +1,7 @@
1
1
  import logging
2
+ from contextlib import contextmanager, asynccontextmanager
3
+
4
+ from koi_net.context import HandlerContext
2
5
 
3
6
  from .network.behavior import Actor
4
7
  from .effector import Effector
@@ -25,6 +28,7 @@ class NodeLifecycle:
25
28
  processor: ProcessorInterface,
26
29
  effector: Effector,
27
30
  actor: Actor,
31
+ handler_context: HandlerContext,
28
32
  use_kobj_processor_thread: bool
29
33
  ):
30
34
  self.config = config
@@ -33,9 +37,34 @@ class NodeLifecycle:
33
37
  self.processor = processor
34
38
  self.effector = effector
35
39
  self.actor = actor
40
+ self.handler_context = handler_context
36
41
  self.use_kobj_processor_thread = use_kobj_processor_thread
42
+
43
+ @contextmanager
44
+ def run(self):
45
+ try:
46
+ logger.info("Starting node lifecycle...")
47
+ self.start()
48
+ yield
49
+ except KeyboardInterrupt:
50
+ logger.info("Keyboard interrupt!")
51
+ finally:
52
+ logger.info("Stopping node lifecycle...")
53
+ self.stop()
54
+
55
+ @asynccontextmanager
56
+ async def async_run(self):
57
+ try:
58
+ logger.info("Starting async node lifecycle...")
59
+ self.start()
60
+ yield
61
+ except KeyboardInterrupt:
62
+ logger.info("Keyboard interrupt!")
63
+ finally:
64
+ logger.info("Stopping async node lifecycle...")
65
+ self.stop()
37
66
 
38
- def start(self) -> None:
67
+ def start(self):
39
68
  """Starts a node, call this method first.
40
69
 
41
70
  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.
@@ -48,7 +77,7 @@ class NodeLifecycle:
48
77
 
49
78
  # refresh to reflect changes (if any) in config.yaml
50
79
  self.effector.deref(self.identity.rid, refresh_cache=True)
51
-
80
+
52
81
  logger.debug("Waiting for kobj queue to empty")
53
82
  if self.use_kobj_processor_thread:
54
83
  self.processor.kobj_queue.join()
@@ -66,9 +95,7 @@ class NodeLifecycle:
66
95
  """Stops a node, call this method last.
67
96
 
68
97
  Finishes processing knowledge object queue. Saves event queues to storage.
69
- """
70
- logger.info("Stopping node...")
71
-
98
+ """
72
99
  if self.use_kobj_processor_thread:
73
100
  logger.info(f"Waiting for kobj queue to empty ({self.processor.kobj_queue.unfinished_tasks} tasks remaining)")
74
101
  self.processor.kobj_queue.join()
@@ -1,5 +1,5 @@
1
1
  from logging import getLogger
2
- from koi_net.protocol.errors import ErrorTypes
2
+ from koi_net.protocol.errors import ErrorType
3
3
  from koi_net.protocol.event import EventType
4
4
  from rid_lib.types import KoiNetNode
5
5
  from ..processor.interface import ProcessorInterface
@@ -36,15 +36,15 @@ class ErrorHandler:
36
36
 
37
37
  def handle_protocol_error(
38
38
  self,
39
- error_type: ErrorTypes,
39
+ error_type: ErrorType,
40
40
  node: KoiNetNode
41
41
  ):
42
42
  logger.info(f"Handling protocol error {error_type} for node {node!r}")
43
43
  match error_type:
44
- case ErrorTypes.UnknownNode:
44
+ case ErrorType.UnknownNode:
45
45
  logger.info("Peer doesn't know me, attempting handshake...")
46
46
  self.actor.handshake_with(node)
47
47
 
48
- case ErrorTypes.InvalidKey: ...
49
- case ErrorTypes.InvalidSignature: ...
50
- case ErrorTypes.InvalidTarget: ...
48
+ case ErrorType.InvalidKey: ...
49
+ case ErrorType.InvalidSignature: ...
50
+ case ErrorType.InvalidTarget: ...
koi_net/network/graph.py CHANGED
@@ -41,7 +41,7 @@ class NetworkGraph:
41
41
  logger.debug(f"Added edge {rid!r} ({edge_profile.source} -> {edge_profile.target})")
42
42
  logger.debug("Done")
43
43
 
44
- def get_edge(self, source: KoiNetNode, target: KoiNetNode,) -> EdgeProfile | None:
44
+ def get_edge(self, source: KoiNetNode, target: KoiNetNode,) -> KoiNetEdge | None:
45
45
  """Returns edge RID given the RIDs of a source and target node."""
46
46
  if (source, target) in self.dg.edges:
47
47
  edge_data = self.dg.get_edge_data(source, target)
koi_net/poller.py CHANGED
@@ -30,18 +30,11 @@ class NodePoller:
30
30
  self.processor.flush_kobj_queue()
31
31
 
32
32
  def run(self):
33
- try:
34
- self.lifecycle.start()
33
+ with self.lifecycle.run():
35
34
  while True:
36
35
  start_time = time.time()
37
36
  self.poll()
38
37
  elapsed = time.time() - start_time
39
38
  sleep_time = self.config.koi_net.polling_interval - elapsed
40
39
  if sleep_time > 0:
41
- time.sleep(sleep_time)
42
-
43
- except KeyboardInterrupt:
44
- logger.info("Polling interrupted by user.")
45
-
46
- finally:
47
- self.lifecycle.stop()
40
+ time.sleep(sleep_time)
@@ -1,51 +1,60 @@
1
1
  """Pydantic models for request and response/payload objects in the KOI-net API."""
2
2
 
3
- from pydantic import BaseModel
3
+ from typing import Literal
4
+ from pydantic import BaseModel, Field
4
5
  from rid_lib import RID, RIDType
5
6
  from rid_lib.ext import Bundle, Manifest
6
7
  from .event import Event
7
- from .errors import ErrorTypes
8
+ from .errors import ErrorType
8
9
 
9
10
 
10
11
  # REQUEST MODELS
11
12
 
12
13
  class PollEvents(BaseModel):
13
- rid: RID
14
+ type: Literal["poll_events"] = Field("poll_events")
14
15
  limit: int = 0
15
16
 
16
17
  class FetchRids(BaseModel):
18
+ type: Literal["fetch_rids"] = Field("fetch_rids")
17
19
  rid_types: list[RIDType] = []
18
20
 
19
21
  class FetchManifests(BaseModel):
22
+ type: Literal["fetch_manifests"] = Field("fetch_manifests")
20
23
  rid_types: list[RIDType] = []
21
24
  rids: list[RID] = []
22
25
 
23
26
  class FetchBundles(BaseModel):
27
+ type: Literal["fetch_bundles"] = Field("fetch_bundles")
24
28
  rids: list[RID]
25
29
 
26
30
 
27
31
  # RESPONSE/PAYLOAD MODELS
28
32
 
29
33
  class RidsPayload(BaseModel):
34
+ type: Literal["rids_payload"] = Field("rids_payload")
30
35
  rids: list[RID]
31
36
 
32
37
  class ManifestsPayload(BaseModel):
38
+ type: Literal["manifests_payload"] = Field("manifests_payload")
33
39
  manifests: list[Manifest]
34
40
  not_found: list[RID] = []
35
41
 
36
42
  class BundlesPayload(BaseModel):
43
+ type: Literal["bundles_payload"] = Field("bundles_payload")
37
44
  bundles: list[Bundle]
38
45
  not_found: list[RID] = []
39
46
  deferred: list[RID] = []
40
47
 
41
48
  class EventsPayload(BaseModel):
49
+ type: Literal["events_payload"] = Field("events_payload")
42
50
  events: list[Event]
43
51
 
44
52
 
45
53
  # ERROR MODELS
46
54
 
47
55
  class ErrorResponse(BaseModel):
48
- error: ErrorTypes
56
+ type: Literal["error_response"] = Field("error_response")
57
+ error: ErrorType
49
58
 
50
59
  # TYPES
51
60
 
@@ -27,7 +27,7 @@ class SignedEnvelope(BaseModel, Generic[T]):
27
27
  )
28
28
 
29
29
  logger.debug(f"Verifying envelope: {unsigned_envelope.model_dump_json()}")
30
-
30
+
31
31
  pub_key.verify(
32
32
  self.signature,
33
33
  unsigned_envelope.model_dump_json().encode()
@@ -1,23 +1,23 @@
1
1
  from enum import StrEnum
2
2
 
3
3
 
4
- class ErrorTypes(StrEnum):
4
+ class ErrorType(StrEnum):
5
5
  UnknownNode = "unknown_node"
6
6
  InvalidKey = "invalid_key"
7
7
  InvalidSignature = "invalid_signature"
8
8
  InvalidTarget = "invalid_target"
9
9
 
10
10
  class ProtocolError(Exception):
11
- error_type: ErrorTypes
11
+ error_type: ErrorType
12
12
 
13
13
  class UnknownNodeError(ProtocolError):
14
- error_type = ErrorTypes.UnknownNode
14
+ error_type = ErrorType.UnknownNode
15
15
 
16
16
  class InvalidKeyError(ProtocolError):
17
- error_type = ErrorTypes.InvalidKey
17
+ error_type = ErrorType.InvalidKey
18
18
 
19
19
  class InvalidSignatureError(ProtocolError):
20
- error_type = ErrorTypes.InvalidSignature
20
+ error_type = ErrorType.InvalidSignature
21
21
 
22
22
  class InvalidTargetError(ProtocolError):
23
- error_type = ErrorTypes.InvalidTarget
23
+ error_type = ErrorType.InvalidTarget
@@ -1,22 +1,64 @@
1
1
  import logging
2
- from base64 import urlsafe_b64decode, urlsafe_b64encode
2
+ from base64 import b64decode, b64encode
3
3
  from cryptography.hazmat.primitives import hashes
4
4
  from cryptography.hazmat.primitives.asymmetric import ec
5
5
  from cryptography.hazmat.primitives import serialization
6
6
  from rid_lib.ext.utils import sha256_hash
7
+ from cryptography.hazmat.primitives.asymmetric.utils import (
8
+ decode_dss_signature,
9
+ encode_dss_signature
10
+ )
7
11
 
8
12
  logger = logging.getLogger(__name__)
9
13
 
10
14
 
15
+ def der_to_raw_signature(der_signature: bytes, curve=ec.SECP256R1()) -> bytes:
16
+ """Convert a DER-encoded signature to raw r||s format."""
17
+
18
+ # Decode the DER signature to get r and s
19
+ r, s = decode_dss_signature(der_signature)
20
+
21
+ # Determine byte length based on curve bit size
22
+ byte_length = (curve.key_size + 7) // 8
23
+
24
+ # Convert r and s to big-endian byte arrays of fixed length
25
+ r_bytes = r.to_bytes(byte_length, byteorder='big')
26
+ s_bytes = s.to_bytes(byte_length, byteorder='big')
27
+
28
+ # Concatenate r and s
29
+ return r_bytes + s_bytes
30
+
31
+
32
+ def raw_to_der_signature(raw_signature: bytes, curve=ec.SECP256R1()) -> bytes:
33
+ """Convert a raw r||s signature to DER format."""
34
+
35
+ # Determine byte length based on curve bit size
36
+ byte_length = (curve.key_size + 7) // 8
37
+
38
+ # Split the raw signature into r and s components
39
+ if len(raw_signature) != 2 * byte_length:
40
+ raise ValueError(f"Raw signature must be {2 * byte_length} bytes for {curve.name}")
41
+
42
+ r_bytes = raw_signature[:byte_length]
43
+ s_bytes = raw_signature[byte_length:]
44
+
45
+ # Convert bytes to integers
46
+ r = int.from_bytes(r_bytes, byteorder='big')
47
+ s = int.from_bytes(s_bytes, byteorder='big')
48
+
49
+ # Encode as DER
50
+ return encode_dss_signature(r, s)
51
+
52
+
11
53
  class PrivateKey:
12
54
  priv_key: ec.EllipticCurvePrivateKey
13
55
 
14
56
  def __init__(self, priv_key):
15
57
  self.priv_key = priv_key
16
-
58
+
17
59
  @classmethod
18
60
  def generate(cls):
19
- return cls(priv_key=ec.generate_private_key(ec.SECP192R1()))
61
+ return cls(priv_key=ec.generate_private_key(ec.SECP256R1()))
20
62
 
21
63
  def public_key(self) -> "PublicKey":
22
64
  return PublicKey(self.priv_key.public_key())
@@ -40,12 +82,14 @@ class PrivateKey:
40
82
  def sign(self, message: bytes) -> str:
41
83
  hashed_message = sha256_hash(message.decode())
42
84
 
43
- signature = urlsafe_b64encode(
44
- self.priv_key.sign(
45
- data=message,
46
- signature_algorithm=ec.ECDSA(hashes.SHA256())
47
- )
48
- ).decode()
85
+ der_signature_bytes = self.priv_key.sign(
86
+ data=message,
87
+ signature_algorithm=ec.ECDSA(hashes.SHA256())
88
+ )
89
+
90
+ raw_signature_bytes = der_to_raw_signature(der_signature_bytes)
91
+
92
+ signature = b64encode(raw_signature_bytes).decode()
49
93
 
50
94
  logger.debug(f"Signing message with [{self.public_key().to_der()}]")
51
95
  logger.debug(f"hash: {hashed_message}")
@@ -78,29 +122,39 @@ class PublicKey:
78
122
  def from_der(cls, pub_key_der: str):
79
123
  return cls(
80
124
  pub_key=serialization.load_der_public_key(
81
- data=urlsafe_b64decode(pub_key_der)
125
+ data=b64decode(pub_key_der)
82
126
  )
83
127
  )
84
128
 
85
129
  def to_der(self) -> str:
86
- return urlsafe_b64encode(
130
+ return b64encode(
87
131
  self.pub_key.public_bytes(
88
132
  encoding=serialization.Encoding.DER,
89
133
  format=serialization.PublicFormat.SubjectPublicKeyInfo
90
134
  )
91
135
  ).decode()
92
136
 
137
+
93
138
  def verify(self, signature: str, message: bytes) -> bool:
94
- hashed_message = sha256_hash(message.decode())
139
+ # hashed_message = sha256_hash(message.decode())
140
+
141
+ # print(message.hex())
142
+ # print()
143
+ # print(hashed_message)
144
+ # print()
145
+ # print(message.decode())
95
146
 
96
- logger.debug(f"Verifying message with [{self.to_der()}]")
97
- logger.debug(f"hash: {hashed_message}")
98
- logger.debug(f"signature: {signature}")
147
+ # logger.debug(f"Verifying message with [{self.to_der()}]")
148
+ # logger.debug(f"hash: {hashed_message}")
149
+ # logger.debug(f"signature: {signature}")
150
+
151
+ raw_signature_bytes = b64decode(signature)
152
+ der_signature_bytes = raw_to_der_signature(raw_signature_bytes)
99
153
 
100
154
  # NOTE: throws cryptography.exceptions.InvalidSignature on failure
101
155
 
102
156
  self.pub_key.verify(
103
- signature=urlsafe_b64decode(signature),
157
+ signature=der_signature_bytes,
104
158
  data=message,
105
159
  signature_algorithm=ec.ECDSA(hashes.SHA256())
106
160
  )
koi_net/server.py CHANGED
@@ -54,8 +54,14 @@ class NodeServer:
54
54
  self._build_app()
55
55
 
56
56
  def _build_app(self):
57
+
58
+ @asynccontextmanager
59
+ async def lifespan(*args, **kwargs):
60
+ async with self.lifecycle.async_run():
61
+ yield
62
+
57
63
  self.app = FastAPI(
58
- lifespan=self.lifespan,
64
+ lifespan=lifespan,
59
65
  title="KOI-net Protocol API",
60
66
  version="1.0.0"
61
67
  )
@@ -85,12 +91,6 @@ class NodeServer:
85
91
  port=self.config.server.port
86
92
  )
87
93
 
88
- @asynccontextmanager
89
- async def lifespan(self, app: FastAPI):
90
- self.lifecycle.start()
91
- yield
92
- self.lifecycle.stop()
93
-
94
94
  def protocol_error_handler(self, request, exc: ProtocolError):
95
95
  logger.info(f"caught protocol error: {exc}")
96
96
  resp = ErrorResponse(error=exc.error_type)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: koi-net
3
- Version: 1.1.0b4
3
+ Version: 1.1.0b6
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>
@@ -1,19 +1,19 @@
1
1
  koi_net/__init__.py,sha256=b0Ze0pZmJAuygpWUFHM6Kvqo3DkU_uzmkptv1EpAArw,31
2
2
  koi_net/config.py,sha256=47XbQ59GRYFi4rlsoWKlnzMQATcnK70i3qmKTZAGOQk,4087
3
3
  koi_net/context.py,sha256=FwkzjmsyNqUeOeGCuZXtONqs5_MSl1R8-e3IxHuyzTI,1531
4
- koi_net/core.py,sha256=UFERsvnphUCWSQYunMgXlLpJ_XU5yD_1tcqlGO0BV9A,7100
4
+ koi_net/core.py,sha256=aO9caoS8dLafRGheJWzhbp_ym7o7bi_wxds683bly48,7150
5
5
  koi_net/default_actions.py,sha256=TkQR9oj9CpO37Gb5bZLmFNl-Q8n3OxGiX4dvxQR7SaA,421
6
6
  koi_net/effector.py,sha256=gSyZgRxQ91X04UL261e2pXWUfBHnQTGtjSHpc2JufxA,4097
7
7
  koi_net/identity.py,sha256=FvIWksGTqwM7HCevIwmo_6l-t-2tnYkaaR4CanZatL4,569
8
- koi_net/lifecycle.py,sha256=-J5y4i0JdernatLTed8hQbkK26Cs6rm1kZqlx3aQzZA,2727
9
- koi_net/poller.py,sha256=ZvQcZ0RQhViXVhbIpdTZb7_ql3nG0mTcdBamneZKLpA,1361
8
+ koi_net/lifecycle.py,sha256=GL2zltmh-aeBuNVg_rbIgsXMch672w7xkWAXCTjxst4,3550
9
+ koi_net/poller.py,sha256=bIrlqdac5vLQYAid35xiQJLDMR85GnOSPCXSTQ07-Mc,1173
10
10
  koi_net/secure.py,sha256=cGNF2assqCaYq0i0fhQBm7aREoAdpY-XVypDsE1ALaU,3970
11
- koi_net/server.py,sha256=5BVVrgn69r_HMC3c4npWywPsjBDqUnHFWLKdtnfRTbA,4250
11
+ koi_net/server.py,sha256=dZfSKrNHqIVD1qc9WkYRO30L4so-A7iW4IsX63oSmlE,4265
12
12
  koi_net/network/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  koi_net/network/behavior.py,sha256=NZLvWlrxR0uWriE3ZzCXmocUVccQthy7Xx8E_8KBwsg,1208
14
- koi_net/network/error_handler.py,sha256=CrmCpBY2oj4nl7uXrIYusUHDKxPZ1HDuQAtiBSZarRI,1623
14
+ koi_net/network/error_handler.py,sha256=_dAl2ovpUZEVhPc8_dcLA7I-FoMNqQNwFy0QLa4uTSY,1617
15
15
  koi_net/network/event_queue.py,sha256=DWs26C235iYkP4koKcdbhmIOHGZRJ48d072BoNWyiHo,7325
16
- koi_net/network/graph.py,sha256=NLstBsPa9By0luxcTjThnqVd3hxfQdFwn8tWgJ6u4l4,4144
16
+ koi_net/network/graph.py,sha256=60SLiR3aNXIOGe-URzMGgx8abmMEJtz37EJs6jeImEM,4143
17
17
  koi_net/network/request_handler.py,sha256=vBXw2GO5vGAYCs18flWbln4kO_HmY1G9uuFu9MYqedY,6852
18
18
  koi_net/network/resolver.py,sha256=coIp4M6k0-8sUfAy4h2NMx_7zCNroWlCHKOj3AXZVhc,5412
19
19
  koi_net/network/response_handler.py,sha256=__R_EvEpjaMz3PCDvkNgWF_EAHe2nePGk-zK_cT4C4g,2077
@@ -24,15 +24,15 @@ koi_net/processor/interface.py,sha256=ebDwqggznFRfp2PT8-UJPUAvCwX8nZaaQ68FUeWQvm
24
24
  koi_net/processor/knowledge_object.py,sha256=avQnsaeqqiJxy40P1VGljuQMtAGmJB-TBa4pmBXTaIs,3863
25
25
  koi_net/processor/knowledge_pipeline.py,sha256=i7FpCFl0UIOwCI5zhP1i8M4PX4A48VN28iV9jruvN5k,9486
26
26
  koi_net/protocol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
- koi_net/protocol/api_models.py,sha256=bHhbLeq8I7nVBuW-AXgKfb1O58_mLogHRG8A_LZt2IE,1188
27
+ koi_net/protocol/api_models.py,sha256=jzRZWW_ZB5YsBAiwCom882-WIbr0rPyelJxExRgHZGc,1755
28
28
  koi_net/protocol/consts.py,sha256=bisbVEojPIHlLhkLafBzfIhH25TjNfvTORF1g6YXzIM,243
29
29
  koi_net/protocol/edge.py,sha256=dQKtI0_eX2E6tD7kMExv6DeJMkqNo2cY-LxJMJbiK0E,963
30
- koi_net/protocol/envelope.py,sha256=W-K3rjwqwAL9wCXb2_gpAUwnc2xOVdZ1UWMoDlLrqJY,1690
31
- koi_net/protocol/errors.py,sha256=A83QiYe_fJdxW2lsNsLCujWxDr5sk1UmYYd3TGbSNJA,601
30
+ koi_net/protocol/envelope.py,sha256=BkBabUZXDOiGJnfBJm5BHTQZflsiQ3jBmG1gNOtmeO4,1698
31
+ koi_net/protocol/errors.py,sha256=uKPQ-TGLouZuK0xd2pXuCQoRTyu_JFsydSCLml13Cz8,595
32
32
  koi_net/protocol/event.py,sha256=eGgihEj1gliLoQRk8pVB2q_was0AGo-PbT3Hqnpn3oU,1379
33
33
  koi_net/protocol/node.py,sha256=7GQzHORFr9cP4BqJgir6EGSWCskL-yqmvJksIiLfcWU,409
34
- koi_net/protocol/secure.py,sha256=Reem9Z4le4uWXM9uczNOdmgVBg8p4YQav-7_c3pZ1CQ,3366
35
- koi_net-1.1.0b4.dist-info/METADATA,sha256=olgGAhiNBqMAwhtLYCaK0QNNS3s0LuNzHBwazVELAqk,37118
36
- koi_net-1.1.0b4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
37
- koi_net-1.1.0b4.dist-info/licenses/LICENSE,sha256=03mgCL5qth2aD9C3F3qNVs4sFJSpK9kjtYCyOwdSp7s,1069
38
- koi_net-1.1.0b4.dist-info/RECORD,,
34
+ koi_net/protocol/secure.py,sha256=6sRLWxG5EDF0QLBj29gk3hPmZnPXATrTTFdwx39wQfY,5127
35
+ koi_net-1.1.0b6.dist-info/METADATA,sha256=G2VwaYEW0oLstoINlbOJlT-f2yAhSXEOZP3shspdgqo,37118
36
+ koi_net-1.1.0b6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
37
+ koi_net-1.1.0b6.dist-info/licenses/LICENSE,sha256=03mgCL5qth2aD9C3F3qNVs4sFJSpK9kjtYCyOwdSp7s,1069
38
+ koi_net-1.1.0b6.dist-info/RECORD,,